From 6406a6f9eab3c4562606986e097296cdbe835c32 Mon Sep 17 00:00:00 2001 From: Yifan Yuan Date: Fri, 28 Nov 2025 22:17:40 +0800 Subject: [PATCH 1/4] [develop] add create_snapshot() interface Signed-off-by: Yifan Yuan --- src/config.h | 9 +++++ src/example_config/overlaybd.json | 6 +++- src/image_file.h | 6 ++++ src/image_service.cpp | 14 ++++++++ src/overlaybd/lsmt/file.cpp | 55 +++++++++++++++++++++++++++- src/overlaybd/lsmt/file.h | 2 ++ src/overlaybd/lsmt/index.cpp | 25 +++++++++++++ src/overlaybd/lsmt/index.h | 7 ++-- src/overlaybd/lsmt/test/test.cpp | 59 +++++++++++++++++++++++++++++++ 9 files changed, 179 insertions(+), 4 deletions(-) diff --git a/src/config.h b/src/config.h index 33ce1d80..260406d1 100644 --- a/src/config.h +++ b/src/config.h @@ -131,6 +131,13 @@ struct CertConfig : public ConfigUtils::Config { APPCFG_PARA(keyFile, std::string, ""); }; +struct ServiceConfig : public ConfigUtils::Config { + APPCFG_CLASS + APPCFG_PARA(enable, bool, false); + APPCFG_PARA(domainSocket, std::string, ""); +}; + + struct GlobalConfig : public ConfigUtils::Config { APPCFG_CLASS @@ -155,6 +162,7 @@ struct GlobalConfig : public ConfigUtils::Config { APPCFG_PARA(prefetchConfig, PrefetchConfig); APPCFG_PARA(certConfig, CertConfig); APPCFG_PARA(userAgent, std::string, OVERLAYBD_VERSION); + APPCFG_PARA(serviceConfig, ServiceConfig); }; struct AuthConfig : public ConfigUtils::Config { @@ -170,4 +178,5 @@ struct ImageAuthResponse : public ConfigUtils::Config { APPCFG_PARA(data, AuthConfig); }; + } // namespace ImageConfigNS diff --git a/src/example_config/overlaybd.json b/src/example_config/overlaybd.json index 225bc879..3b729583 100644 --- a/src/example_config/overlaybd.json +++ b/src/example_config/overlaybd.json @@ -36,5 +36,9 @@ }, "enableAudit": true, "auditPath": "/var/log/overlaybd-audit.log", - "registryFsVersion": "v2" + "registryFsVersion": "v2", + "serviceConfig": { + "enable": false, + "domainSocket": "/var/run/overlaybd.sock" + } } diff --git a/src/image_file.h b/src/image_file.h index 86d44df2..c4eea1c3 100644 --- a/src/image_file.h +++ b/src/image_file.h @@ -113,6 +113,12 @@ class ImageFile : public photon::fs::ForwardFile { int compact(IFile *as); + int create_snapshot(const char *config_path) { + // load new config file to get the snapshot layer path + // open new upper layer + // restack() current RW layer as snapshot layer + } + private: Prefetcher *m_prefetcher = nullptr; ImageConfigNS::ImageConfig conf; diff --git a/src/image_service.cpp b/src/image_service.cpp index a5e2cda5..2c124e44 100644 --- a/src/image_service.cpp +++ b/src/image_service.cpp @@ -440,6 +440,20 @@ int ImageService::init() { 10000000, (uint64_t)1048576 * 4096, global_fs.io_alloc); } } + if (global_conf.serviceConfig().enable()) { + auto sock_path = global_conf.serviceConfig().domainSocket(); + if (access(sock_path.c_str(), 0) == 0) { + if (unlink(sock_path.c_str()) != 0) { + LOG_ERRNO_RETURN(0, -1, "failed to remove old socket file"); + } + } + // listen the domainSocket and create a HTTP SERVER + /* + handler definition: + - create a live snapshot for a imageFile + /snapshot?dev_id=${devID}&config=${config} + */ + } return 0; } diff --git a/src/overlaybd/lsmt/file.cpp b/src/overlaybd/lsmt/file.cpp index 1a2af5b0..1cacc05e 100644 --- a/src/overlaybd/lsmt/file.cpp +++ b/src/overlaybd/lsmt/file.cpp @@ -680,6 +680,8 @@ class LSMTReadOnlyFile : public IFileRW { reverse(files.begin(), files.end()); return merge_files_ro(files, args); } + + UNIMPLEMENTED(int restack(IFileRW *upper) override); }; class LSMTFile : public LSMTReadOnlyFile { @@ -946,7 +948,7 @@ class LSMTFile : public LSMTReadOnlyFile { } auto p = new LSMTReadOnlyFile; p->m_index = new_index; - p->m_files = {m_files.back()}; + p->m_files = {nullptr, m_files.back()}; p->m_vsize = m_vsize; p->m_file_ownership = m_file_ownership; m_file_ownership = false; @@ -1003,6 +1005,54 @@ class LSMTFile : public LSMTReadOnlyFile { CompactOptions opts(&m_files, (SegmentMapping*)(pmi->buffer()), pmi->size(), m_vsize, &args); return compact(opts, _no_use_var); } + + int reserve_top_layer(LSMTFile *top_layer) + { + std::vector pmappings; // temp index for reserved layer + /* ==== close_seal the top RW layer and reopen it. ==== */ + IFileRO* gc_layer = nullptr; + auto fseal = (LSMTFile*)open_file_rw(m_files[m_rw_tag], m_findex, false); + if (fseal==nullptr){ + return -1; + } + if (fseal->close_seal(&gc_layer)!=0){ + LOG_ERROR_RETURN(0, -1, "close seal top RW layer failed."); + } + if (gc_layer == nullptr){ + LOG_ERROR_RETURN(0, -1, "reopen sealed RW layer failed."); + } + /* ==== reserve m_files[0] for new layer. ==== */ + auto u = top_layer; + + LOG_INFO("m_files.insert new layer: file ptr: 0x`", u->m_files[0]); + // m_files[m_rw_tag] = ((LSMTReadOnlyFile*)gc_layer)->m_files[1]; + m_files.insert(m_files.begin(), ((LSMTReadOnlyFile*)gc_layer)->m_files[1]); + m_rw_tag++; + m_files[m_rw_tag] = u->m_files[0]; // fnew_layer; + if (m_file_ownership){ + LOG_INFO("delete original m_findex."); + m_findex->close(); + safe_delete(m_findex); + } + LOG_DEBUG("m_files.size(): `, rw_tag: `", m_files.size(), m_rw_tag); + m_findex = u->m_findex; + m_vsize = u->m_vsize; + ((IComboIndex *)m_index)->commit_index0(); + return 0; + } + + + int restack(IFileRW* upper_layer) override { + Lock _(m_rw_mtx); + LOG_INFO("restack new rwlayer, seal old."); + int ret = reserve_top_layer((LSMTFile*)upper_layer); + if (ret != 0) { + LOG_ERRNO_RETURN(0, -1, "restack new rwlayer failed."); + } + LOG_INFO("current ro layers count: `", m_files.size() - 1); + return ret; + } + }; class LSMTSparseFile : public LSMTFile { public: @@ -1102,6 +1152,7 @@ class LSMTSparseFile : public LSMTFile { m_files[m_rw_tag]->ftruncate(vsize + HeaderTrailer::SPACE); return 0; } + }; class LSMTWarpFile : public LSMTFile { @@ -1696,6 +1747,7 @@ static IMemoryIndex *load_merge_index(vector &files, vector &uuid return pmi; } +// layer0 ... layerN-1 IFileRO *open_files_ro(IFile **files, size_t n, bool ownership) { if (n > MAX_STACK_LAYERS) { LOG_ERROR_RETURN(0, 0, "open too many files (` > `)", n, MAX_STACK_LAYERS); @@ -1707,6 +1759,7 @@ IFileRO *open_files_ro(IFile **files, size_t n, bool ownership) { vector m_files(files, files + n); vector m_uuid(n); auto pmi = load_merge_index(m_files, m_uuid, vsize); + // reverse files: layerN-1 ... layer0 if (!pmi) return nullptr; diff --git a/src/overlaybd/lsmt/file.h b/src/overlaybd/lsmt/file.h index 1fc7b5bc..4c6dbe0b 100644 --- a/src/overlaybd/lsmt/file.h +++ b/src/overlaybd/lsmt/file.h @@ -101,6 +101,8 @@ class IFileRW : public IFileRO { }; virtual DataStat data_stat() const = 0; + // close_seal current RW layer (change it to RO layer) and re-stack with upper layer + virtual int restack(IFileRW *upper) = 0; }; // create a new writable LSMT file constitued by a data file and an index file, diff --git a/src/overlaybd/lsmt/index.cpp b/src/overlaybd/lsmt/index.cpp index d4dadf9f..af8303a7 100644 --- a/src/overlaybd/lsmt/index.cpp +++ b/src/overlaybd/lsmt/index.cpp @@ -580,6 +580,7 @@ class Index0 : public IComboIndex { virtual IMemoryIndex *make_read_only_index() const override { auto rst = new Index(); + rst->ownership = false; // set false to avoid free *pbegin & vector::mapping rst->mapping.reserve(size()); rst->assign(mapping.begin(), mapping.end()); return rst; @@ -617,6 +618,7 @@ class Index0 : public IComboIndex { return this; } UNIMPLEMENTED(size_t vsize() const override); + UNIMPLEMENTED(int commit_index0() override); }; static void merge_indexes(uint8_t level, vector &mapping, const Index **pindexes, @@ -744,6 +746,29 @@ class ComboIndex : public Index0 { return new Index(std::move(mappings)); } + virtual int commit_index0() override { + // Merge index0 (mapping) and backing_index + m_backing_index->increase_tag(1); + auto merged_index = create_memory_index0(m_backing_index->buffer(), m_backing_index->size(), 0, UINT64_MAX); + vector dumped; + dumped.assign(mapping.begin(), mapping.end()); + auto idx_size = compress_raw_index(&dumped[0], dumped.size()); + // for (auto it : mapping) { + for (size_t i = 0; i < idx_size; i++) { + auto p = dumped[i]; + p.tag = 0; + merged_index->insert(p); + } + delete m_backing_index; + m_backing_index = (Index*)(merged_index->make_read_only_index()); // set ownership=false + delete merged_index; + LOG_INFO("rebuild backing index done. {count: `}", m_backing_index->size()); + // Clear original index0 + mapping.clear(); + return 0; + } + + }; //======================== END OF ComboIndex =============================// diff --git a/src/overlaybd/lsmt/index.h b/src/overlaybd/lsmt/index.h index 2de95695..748d448a 100644 --- a/src/overlaybd/lsmt/index.h +++ b/src/overlaybd/lsmt/index.h @@ -155,8 +155,11 @@ class IComboIndex : public IMemoryIndex0 { // and then clear the original index0. // virtual IMemoryIndex0* gc_index() = 0; virtual IMemoryIndex *load_range_index(int, int) const = 0; - - + + // commit index0 (upper index) to backing index (lower index) + // rebuild backing index before async-compact. + // merge the image's backing_index and gc_idx( dump from original index0 ) for pread() + virtual int commit_index0() = 0; }; // create writable level 0 memory index from an array of mappings; diff --git a/src/overlaybd/lsmt/test/test.cpp b/src/overlaybd/lsmt/test/test.cpp index 1197d12f..0bc85240 100644 --- a/src/overlaybd/lsmt/test/test.cpp +++ b/src/overlaybd/lsmt/test/test.cpp @@ -758,6 +758,65 @@ TEST_F(FileTest3, stack_files_with_zfile_checksum) { delete file; } +TEST_F(FileTest3, restack_simple) { + CleanUp(); + auto lower = create_image(1); + auto upper = create_a_layer(); + auto file = stack_files(upper, lower, 0, true); + randwrite(file, FLAGS_nwrites); + auto upper1 = create_file_rw(); + EXPECT_EQ(0, file->restack(upper1)); + cout << "restack top layer & verify" <restack(upper1)); + randwrite(file, FLAGS_nwrites); + cout << "restack top layer 2 & verify" <restack(upper2)); + randwrite(file, FLAGS_nwrites); + verify_file(file); + delete file; +} + + +TEST_F(FileTest3, restack_sparse) { + CleanUp(); + cout << "generating " << FLAGS_layers << " RO layers by randwrite()" << endl; + auto lowers = create_image(FLAGS_layers); + auto upper = create_a_layer(true); + cout<<"stack files"<restack(upper1)); + randwrite(file, FLAGS_nwrites); + cout << "restack top layer 2 & verify" <restack(upper2)); + randwrite(file, FLAGS_nwrites); + verify_file(file); + delete file; +} + TEST_F(FileTest3, photon_verify) { reset_verify_file(); printf("create image..\n"); From 25f60cdbe1b6da21b709cbcf9886427d9d341c9f Mon Sep 17 00:00:00 2001 From: Xun Chen Date: Wed, 10 Dec 2025 20:39:06 +0800 Subject: [PATCH 2/4] Add dev_id support for ImageFile Signed-off-by: Xun Chen --- src/image_file.h | 10 +++- src/image_service.cpp | 27 ++++++++- src/image_service.h | 7 ++- src/main.cpp | 14 +++-- src/overlaybd/lsmt/file.cpp | 6 +- src/test/image_service_test.cpp | 97 ++++++++++++++++++++++++++++++++- src/tools/comm_func.cpp | 16 +++++- src/tools/comm_func.h | 2 + 8 files changed, 166 insertions(+), 13 deletions(-) diff --git a/src/image_file.h b/src/image_file.h index c4eea1c3..786f3347 100644 --- a/src/image_file.h +++ b/src/image_file.h @@ -41,10 +41,15 @@ static std::string SEALED_FILE_NAME = "overlaybd.sealed"; class ImageFile : public photon::fs::ForwardFile { public: - ImageFile(ImageConfigNS::ImageConfig &_conf, ImageService &is) + ImageFile(ImageConfigNS::ImageConfig &_conf, ImageService &is, const std::string &dev_id) : ForwardFile(nullptr), image_service(is), m_lower_file(nullptr) { conf.CopyFrom(_conf, conf.GetAllocator()); m_exception = ""; + if(image_service.register_image_file(dev_id, this) != 0) { // register itself + set_failed("duplicated dev id: " + dev_id); + return; + } + m_dev_id = dev_id; m_status = init_image_file(); if (m_status == 1) { struct stat st; @@ -55,6 +60,7 @@ class ImageFile : public photon::fs::ForwardFile { ~ImageFile() { m_status = -1; + image_service.unregister_image_file(m_dev_id); // unregister itself if (dl_thread_jh != nullptr) photon::thread_join(dl_thread_jh); delete m_prefetcher; @@ -117,6 +123,7 @@ class ImageFile : public photon::fs::ForwardFile { // load new config file to get the snapshot layer path // open new upper layer // restack() current RW layer as snapshot layer + return 0; } private: @@ -127,6 +134,7 @@ class ImageFile : public photon::fs::ForwardFile { ImageService &image_service; photon::fs::IFile *m_lower_file = nullptr; photon::fs::IFile *m_upper_file = nullptr; + std::string m_dev_id = ""; int init_image_file(); template void set_failed(const Ts&...xs); diff --git a/src/image_service.cpp b/src/image_service.cpp index 2c124e44..df918b07 100644 --- a/src/image_service.cpp +++ b/src/image_service.cpp @@ -470,7 +470,7 @@ bool ImageService::enable_acceleration() { } } -ImageFile *ImageService::create_image_file(const char *config_path) { +ImageFile *ImageService::create_image_file(const char *config_path, const std::string &dev_id) { ImageConfigNS::GlobalConfig defaultDlCfg; if (!defaultDlCfg.ParseJSON(m_config_path)) { LOG_WARN("default download config parse failed, ignore"); @@ -493,7 +493,7 @@ ImageFile *ImageService::create_image_file(const char *config_path) { } auto resFile = cfg.resultFile(); - ImageFile *ret = new ImageFile(cfg, *this); + ImageFile *ret = new ImageFile(cfg, *this, dev_id); if (ret->m_status <= 0) { std::string data = "failed:" + ret->m_exception; set_result_file(resFile, data); @@ -505,6 +505,29 @@ ImageFile *ImageService::create_image_file(const char *config_path) { return ret; } +int ImageService::register_image_file(const std::string& dev_id, ImageFile* file) { + if (dev_id.empty()) + return 0; + if(find_image_file(dev_id) != nullptr) + LOG_ERROR_RETURN(0, -1, "dev id exists: `", dev_id); + m_image_files[dev_id] = file; + LOG_INFO("Registered image file for dev_id: `", dev_id); + return 0; +} + +int ImageService::unregister_image_file(const std::string& dev_id) { + if (dev_id.empty()) + return 0; + m_image_files.erase(dev_id); + LOG_INFO("Unregistered image file for dev_id: `", dev_id); + return 0; +} + +ImageFile* ImageService::find_image_file(const std::string& dev_id) { + auto it = m_image_files.find(dev_id); + return (it != m_image_files.end()) ? it->second : nullptr; +} + ImageService::ImageService(const char *config_path) { m_config_path = config_path ? config_path : DEFAULT_CONFIG_PATH; } diff --git a/src/image_service.h b/src/image_service.h index 0bbbd2c5..cf9d39dd 100644 --- a/src/image_service.h +++ b/src/image_service.h @@ -22,6 +22,7 @@ #include "overlaybd/cache/gzip_cache/cached_fs.h" #include #include +#include using namespace photon::fs; @@ -54,9 +55,12 @@ class ImageService { ImageService(const char *config_path = nullptr); ~ImageService(); int init(); - ImageFile *create_image_file(const char *image_config_path); + ImageFile *create_image_file(const char *image_config_path, const std::string &dev_id); // bool enable_acceleration(GlobalFs *global_fs, ImageConfigNS::P2PConfig conf); bool enable_acceleration(); + int register_image_file(const std::string& dev_id, ImageFile* file); + int unregister_image_file(const std::string& dev_id); + ImageFile* find_image_file(const std::string& dev_id); ImageConfigNS::GlobalConfig global_conf; @@ -69,6 +73,7 @@ class ImageService { std::pair reload_auth(const char *remote_path); void set_result_file(std::string &filename, std::string &data); std::string m_config_path; + std::unordered_map m_image_files; // dev_id -> ImageFile* }; ImageService *create_image_service(const char *config_path = nullptr); diff --git a/src/main.cpp b/src/main.cpp index 0498e4d1..9b0fe680 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,6 +16,7 @@ #include "version.h" #include "image_file.h" #include "image_service.h" +#include "tools/comm_func.h" #include #include #include @@ -34,6 +35,7 @@ #include #include #include +#include class TCMUDevLoop; @@ -46,6 +48,7 @@ struct obd_dev { uint32_t inflight; std::thread *work; photon::semaphore start, end; + std::string dev_id; }; struct handle_args { @@ -303,7 +306,7 @@ class TCMUDevLoop { }; static char *tcmu_get_path(struct tcmu_device *dev) { - char *config = strchr(tcmu_dev_get_cfgstring(dev), '/'); + char *config = strchr(tcmu_dev_get_cfgstring(dev), '/'); // dev_config=overlaybd/[;] if (!config) { LOG_ERROR("no configuration found in cfgstring"); return NULL; @@ -314,16 +317,18 @@ static char *tcmu_get_path(struct tcmu_device *dev) { } static int dev_open(struct tcmu_device *dev) { - char *config = tcmu_get_path(dev); + char *config = tcmu_get_path(dev); // [;] LOG_INFO("dev open `", config); if (!config) { LOG_ERROR_RETURN(0, -EPERM, "get image config path failed"); } + std::string config_path, dev_id; + parse_config_and_dev_id(config, config_path, dev_id); struct timeval start; gettimeofday(&start, NULL); - ImageFile *file = imgservice->create_image_file(config); + ImageFile *file = imgservice->create_image_file(config_path.c_str(), dev_id); if (file == nullptr) { LOG_ERROR_RETURN(0, -EPERM, "create image file failed"); } @@ -332,6 +337,7 @@ static int dev_open(struct tcmu_device *dev) { odev->aio_pending_wakeups = 0; odev->inflight = 0; odev->file = file; + odev->dev_id = dev_id; tcmu_dev_set_private(dev, odev); tcmu_dev_set_block_size(dev, file->block_size); @@ -366,7 +372,7 @@ static int dev_open(struct tcmu_device *dev) { gettimeofday(&end, NULL); uint64_t elapsed = 1000000UL * (end.tv_sec - start.tv_sec) + end.tv_usec - start.tv_usec; - LOG_INFO("dev opened `, time cost ` ms", config, elapsed / 1000); + LOG_INFO("dev opened `, time cost ` ms", config_path.c_str(), elapsed / 1000); return 0; } diff --git a/src/overlaybd/lsmt/file.cpp b/src/overlaybd/lsmt/file.cpp index 1cacc05e..8b0b728d 100644 --- a/src/overlaybd/lsmt/file.cpp +++ b/src/overlaybd/lsmt/file.cpp @@ -948,7 +948,7 @@ class LSMTFile : public LSMTReadOnlyFile { } auto p = new LSMTReadOnlyFile; p->m_index = new_index; - p->m_files = {nullptr, m_files.back()}; + p->m_files = {m_files.back()};//p->m_files = {nullptr, m_files.back()}; p->m_vsize = m_vsize; p->m_file_ownership = m_file_ownership; m_file_ownership = false; @@ -1025,8 +1025,8 @@ class LSMTFile : public LSMTReadOnlyFile { auto u = top_layer; LOG_INFO("m_files.insert new layer: file ptr: 0x`", u->m_files[0]); - // m_files[m_rw_tag] = ((LSMTReadOnlyFile*)gc_layer)->m_files[1]; - m_files.insert(m_files.begin(), ((LSMTReadOnlyFile*)gc_layer)->m_files[1]); + // m_files[m_rw_tag] = ((LSMTReadOnlyFile*)gc_layer)->m_files[0]; + m_files.insert(m_files.begin(), ((LSMTReadOnlyFile*)gc_layer)->m_files[0]); m_rw_tag++; m_files[m_rw_tag] = u->m_files[0]; // fnew_layer; if (m_file_ownership){ diff --git a/src/test/image_service_test.cpp b/src/test/image_service_test.cpp index a5b81a17..392f94b1 100644 --- a/src/test/image_service_test.cpp +++ b/src/test/image_service_test.cpp @@ -26,11 +26,15 @@ #include "photon/net/curl.h" #include "../version.h" #include +#include - +#include #include #include "../image_service.cpp" +#include "../image_service.h" +#include "../image_file.h" +#include "../tools/comm_func.h" char *test_ua = nullptr; @@ -160,7 +164,98 @@ TEST(http_client, user_agent) { EXPECT_EQ(true, buf == "success"); } +class DevIDTest : public ::testing::Test { +public: + ImageService *imgservice; + const std::string test_dir = "/tmp/overlaybd"; + const std::string global_config_path = test_dir + "/global_config.json"; + const std::string image_config_path = test_dir + "/image_config.json"; + const std::string global_config_content = R"delimiter({ + "enableAudit": false, + "logPath": "", + "p2pConfig": { + "enable": false, + "address": "localhost:64210" + } +})delimiter"; + const std::string image_config_content = R"delimiter({ + "lowers" : [ + { + "file" : "/opt/overlaybd/baselayers/ext4_64" + } + ] +})delimiter"; + + virtual void SetUp() override { + // set_log_output_level(0); + system(("mkdir -p " + test_dir).c_str()); + + system(("echo \'" + global_config_content + "\' > " + global_config_path).c_str()); + LOG_INFO("Global config file: "); + system(("cat " + global_config_path).c_str()); + + system(("echo \'" + image_config_content + "\' > " + image_config_path).c_str()); + LOG_INFO("Image config file: "); + system(("cat " + image_config_path).c_str()); + + imgservice = create_image_service(global_config_path.c_str()); + if(imgservice == nullptr) { + LOG_ERROR("failed to create image service"); + exit(-1); + } + } + virtual void TearDown() override { + delete imgservice; + system(("rm -rf " + test_dir).c_str()); + } +}; + +TEST_F(DevIDTest, parse_config_with_dev_id) { + std::string config_path, dev_id; + parse_config_and_dev_id("path/to/config.v1.json;123", config_path, dev_id); + EXPECT_EQ(config_path, "path/to/config.v1.json"); + EXPECT_EQ(dev_id, "123"); +} + +TEST_F(DevIDTest, parse_config_without_dev_id) { + std::string config_path, dev_id; + parse_config_and_dev_id("path/to/config.v1.json", config_path, dev_id); + EXPECT_EQ(config_path, "path/to/config.v1.json"); + EXPECT_EQ(dev_id, ""); +} + +TEST_F(DevIDTest, registers) { + ImageFile* imagefile0 = imgservice->create_image_file(image_config_path.c_str(), ""); + ImageFile* imagefile1 = imgservice->create_image_file(image_config_path.c_str(), "111"); + ImageFile* imagefile2 = imgservice->create_image_file(image_config_path.c_str(), "222"); + ImageFile* imagefile3 = imgservice->create_image_file(image_config_path.c_str(), "333"); + EXPECT_NE(imagefile0, nullptr); + EXPECT_NE(imagefile1, nullptr); + EXPECT_NE(imagefile2, nullptr); + EXPECT_NE(imagefile3, nullptr); + + EXPECT_EQ(imgservice->find_image_file(""), nullptr); + EXPECT_EQ(imgservice->find_image_file("111"), imagefile1); + EXPECT_EQ(imgservice->find_image_file("222"), imagefile2); + EXPECT_EQ(imgservice->find_image_file("333"), imagefile3); + + delete imagefile2; + + EXPECT_EQ(imgservice->find_image_file(""), nullptr); + EXPECT_EQ(imgservice->find_image_file("111"), imagefile1); + EXPECT_EQ(imgservice->find_image_file("222"), nullptr); + EXPECT_EQ(imgservice->find_image_file("333"), imagefile3); + + ImageFile* dup = imgservice->create_image_file(image_config_path.c_str(), "111"); + + EXPECT_EQ(dup, nullptr); + EXPECT_EQ(imgservice->find_image_file("111"), imagefile1); + + delete imagefile0; + delete imagefile1; + delete imagefile3; +} int main(int argc, char** argv) { photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_DEFAULT); diff --git a/src/tools/comm_func.cpp b/src/tools/comm_func.cpp index 6e362661..3c44ccc3 100644 --- a/src/tools/comm_func.cpp +++ b/src/tools/comm_func.cpp @@ -24,6 +24,8 @@ #include "../image_service.h" #include "../image_file.h" #include "../overlaybd/tar/erofs/erofs_fs.h" +#include +#include using namespace std; @@ -51,7 +53,7 @@ int create_overlaybd(const std::string &srv_config, const std::string &dev_confi fprintf(stderr, "failed to create image service\n"); exit(-1); } - imgfile = imgservice->create_image_file(dev_config.c_str()); + imgfile = imgservice->create_image_file(dev_config.c_str(), ""); if (imgfile == nullptr) { fprintf(stderr, "failed to create image file\n"); exit(-1); @@ -112,3 +114,15 @@ IFile *create_uploader(ZFile::CompressArgs *zfile_args, IFile *src, } return upload_builder; } + +void parse_config_and_dev_id(const char *raw_config, std::string &config_path, std::string &dev_id) { + const char *semicolon = strchr(raw_config, ';'); + if(!semicolon) { + config_path = std::string(raw_config); + dev_id = std::string(""); + } + else { + config_path = std::string(raw_config, semicolon - raw_config); + dev_id = std::string(semicolon + 1); + } +} diff --git a/src/tools/comm_func.h b/src/tools/comm_func.h index 7c6618f0..d4ac2783 100644 --- a/src/tools/comm_func.h +++ b/src/tools/comm_func.h @@ -50,3 +50,5 @@ photon::fs::IFileSystem *create_ext4fs(photon::fs::IFile *imgfile, bool mkfs, bool is_erofs_fs(const photon::fs::IFile *imgfile); photon::fs::IFileSystem *create_erofs_fs(photon::fs::IFile *imgfile, uint64_t blksz); + +void parse_config_and_dev_id(const char *raw_config, std::string &config_path, std::string &dev_id); From 4e3a2b0f2bc3822282b0925d22611bdc895dc413 Mon Sep 17 00:00:00 2001 From: Xun Chen Date: Mon, 15 Dec 2025 22:53:20 +0800 Subject: [PATCH 3/4] Add http service for creating snapshot Signed-off-by: Xun Chen --- src/CMakeLists.txt | 1 + src/api_server.cpp | 142 ++++++++++++++++++++++++++++++ src/api_server.h | 45 ++++++++++ src/config.h | 3 +- src/example_config/overlaybd.json | 2 +- src/image_file.h | 1 + src/image_service.cpp | 21 +++-- src/image_service.h | 4 + src/test/image_service_test.cpp | 96 ++++++++++++++------ 9 files changed, 279 insertions(+), 36 deletions(-) create mode 100644 src/api_server.cpp create mode 100644 src/api_server.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4d0e696e..242238f6 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -15,6 +15,7 @@ add_library(overlaybd_image_lib prefetch.cpp tools/sha256file.cpp tools/comm_func.cpp + api_server.cpp ) target_include_directories(overlaybd_image_lib PUBLIC ${CURL_INCLUDE_DIRS} diff --git a/src/api_server.cpp b/src/api_server.cpp new file mode 100644 index 00000000..1c7d876f --- /dev/null +++ b/src/api_server.cpp @@ -0,0 +1,142 @@ +/* + Copyright The Overlaybd Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include +#include "image_service.h" +#include "image_file.h" +#include "api_server.h" + +int ApiHandler::handle_request(photon::net::http::Request& req, + photon::net::http::Response& resp, + std::string_view) { + auto target = req.target(); // string view, format: /snapshot?dev_id=${devID}&config=${config} + std::string_view query(""); + auto pos = target.find('?'); + if (pos != std::string_view::npos) { + query = target.substr(pos + 1); + } + LOG_DEBUG("Snapshot query: `", query); // string view, format: dev_id=${devID}&config=${config} + parse_params(query); + auto dev_id = params["dev_id"]; + auto config_path = params["config"]; + LOG_DEBUG("dev_id: `, config: `", dev_id, config_path); + + int code; + std::string msg; + ImageFile* img_file = nullptr; + + if (dev_id.empty() || config_path.empty()) { + code = 400; + msg = std::string(R"delimiter({ + "success": false, + "message": "Missing dev_id or config in snapshot request" +})delimiter"); + goto EXIT; + } + + img_file = imgservice->find_image_file(dev_id); + if (!img_file) { + code = 404; + msg = std::string(R"delimiter({ + "success": false, + "message": "Image file not found" +})delimiter"); + goto EXIT; + } + + if (img_file->create_snapshot(config_path.c_str()) < 0) { + code = 500; + msg = std::string(R"delimiter({ + "success": false, + "message": "Failed to create snapshot`" +})delimiter"); + goto EXIT; + } + + code = 200; + msg = std::string(R"delimiter({ + "success": true, + "message": "Snapshot created successfully" +})delimiter"); + +EXIT: + resp.set_result(code); + resp.headers.content_length(msg.size()); + resp.keep_alive(true); + auto ret_w = resp.write((void*)msg.c_str(), msg.size()); + if (ret_w != (ssize_t)msg.size()) { + LOG_ERRNO_RETURN(0, -1, "send body failed, target: `, `", req.target(), VALUE(ret_w)); + } + LOG_DEBUG("send body done"); + return 0; +} + +void ApiHandler::parse_params(std::string_view query) { // format: dev_id=${devID}&config=${config}... + if (query.empty()) + return; + + size_t start = 0; + while (start < query.length()) { + auto end = query.find('&', start); + if (end == std::string_view::npos) { // last one + end = query.length(); + } + + auto param = query.substr(start, end - start); + auto eq_pos = param.find('='); + if (eq_pos != std::string_view::npos) { + auto key = param.substr(0, eq_pos); + auto value = param.substr(eq_pos + 1); + + // url decode + auto decoded_key = photon::net::http::url_unescape(key); + auto decoded_value = photon::net::http::url_unescape(value); + params[decoded_key] = decoded_value; + } else { + // key without value + auto key = photon::net::http::url_unescape(param); + params[key] = ""; + } + start = end + 1; + } +} + +ApiServer::ApiServer(const std::string &addr, ApiHandler* handler) { + photon::net::http::URL url(addr); + std::string host = url.host().data(); // the string pointed by data() doesn't end up with '\0' + auto pos = host.find(":"); + if (pos != host.npos) { + host.resize(pos); + } + tcpserver = photon::net::new_tcp_socket_server(); + tcpserver->setsockopt(SOL_SOCKET, SO_REUSEPORT, 1); + if(tcpserver->bind(url.port(), photon::net::IPAddr(host.c_str())) < 0) + LOG_ERRNO_RETURN(0, , "Failed to bind api server port `", url.port()); + if(tcpserver->listen() < 0) + LOG_ERRNO_RETURN(0, , "Failed to listen api server port `", url.port()); + httpserver = photon::net::http::new_http_server(); + httpserver->add_handler(handler, false, "/snapshot"); + tcpserver->set_handler(httpserver->get_connection_handler()); + tcpserver->start_loop(); + ready = true; + LOG_DEBUG("Api server listening on `:`, path: `", host, url.port(), "/snapshot"); +} + +ApiServer::~ApiServer() { + delete tcpserver; + delete httpserver; +} \ No newline at end of file diff --git a/src/api_server.h b/src/api_server.h new file mode 100644 index 00000000..1b8c0641 --- /dev/null +++ b/src/api_server.h @@ -0,0 +1,45 @@ +/* + Copyright The Overlaybd Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include +#include +#include +#include + +class ImageService; + +class ApiHandler : public photon::net::http::HTTPHandler { +public: + ImageService *imgservice; + std::map params; + + ApiHandler(ImageService *imgservice) : imgservice(imgservice) {} + int handle_request(photon::net::http::Request& req, + photon::net::http::Response& resp, + std::string_view) override; + void parse_params(std::string_view query); +}; + +struct ApiServer { + photon::net::ISocketServer* tcpserver = nullptr; + photon::net::http::HTTPServer* httpserver = nullptr; + bool ready = false; + + ApiServer(const std::string &addr, ApiHandler* handler); + ~ApiServer(); +}; \ No newline at end of file diff --git a/src/config.h b/src/config.h index 260406d1..d2640194 100644 --- a/src/config.h +++ b/src/config.h @@ -134,10 +134,9 @@ struct CertConfig : public ConfigUtils::Config { struct ServiceConfig : public ConfigUtils::Config { APPCFG_CLASS APPCFG_PARA(enable, bool, false); - APPCFG_PARA(domainSocket, std::string, ""); + APPCFG_PARA(address, std::string, "http://127.0.0.1:9862"); }; - struct GlobalConfig : public ConfigUtils::Config { APPCFG_CLASS diff --git a/src/example_config/overlaybd.json b/src/example_config/overlaybd.json index 3b729583..2fe5cd26 100644 --- a/src/example_config/overlaybd.json +++ b/src/example_config/overlaybd.json @@ -39,6 +39,6 @@ "registryFsVersion": "v2", "serviceConfig": { "enable": false, - "domainSocket": "/var/run/overlaybd.sock" + "address": "http://127.0.0.1:9862" } } diff --git a/src/image_file.h b/src/image_file.h index 786f3347..12e12aac 100644 --- a/src/image_file.h +++ b/src/image_file.h @@ -123,6 +123,7 @@ class ImageFile : public photon::fs::ForwardFile { // load new config file to get the snapshot layer path // open new upper layer // restack() current RW layer as snapshot layer + LOG_INFO("call create_snapshot, dev_id: `", m_dev_id); return 0; } diff --git a/src/image_service.cpp b/src/image_service.cpp index df918b07..20e648f5 100644 --- a/src/image_service.cpp +++ b/src/image_service.cpp @@ -16,6 +16,7 @@ #include "image_service.h" #include "config.h" #include "image_file.h" +#include "api_server.h" #include #include #include @@ -441,18 +442,22 @@ int ImageService::init() { } } if (global_conf.serviceConfig().enable()) { - auto sock_path = global_conf.serviceConfig().domainSocket(); - if (access(sock_path.c_str(), 0) == 0) { - if (unlink(sock_path.c_str()) != 0) { - LOG_ERRNO_RETURN(0, -1, "failed to remove old socket file"); - } - } + // auto sock_path = global_conf.serviceConfig().domainSocket(); + // if (access(sock_path.c_str(), 0) == 0) { + // if (unlink(sock_path.c_str()) != 0) { + // LOG_ERRNO_RETURN(0, -1, "failed to remove old socket file"); + // } + // } // listen the domainSocket and create a HTTP SERVER /* handler definition: - create a live snapshot for a imageFile /snapshot?dev_id=${devID}&config=${config} */ + api_handler.reset(new ApiHandler(this)); + api_server = new ApiServer(global_conf.serviceConfig().address(), api_handler.get()); + if(!api_server->ready) + LOG_ERROR_RETURN(0, -1, "Failed to start http server for live snapshot"); } return 0; } @@ -540,6 +545,8 @@ ImageService::~ImageService() { delete global_fs.srcfs; delete global_fs.io_alloc; delete exporter; + delete api_server; + LOG_INFO("image service is fully stopped"); } @@ -550,4 +557,4 @@ ImageService *create_image_service(const char *config_path) { return nullptr; } return ret; -} +} \ No newline at end of file diff --git a/src/image_service.h b/src/image_service.h index cf9d39dd..df260e9f 100644 --- a/src/image_service.h +++ b/src/image_service.h @@ -49,6 +49,8 @@ struct ImageAuthResponse : public ConfigUtils::Config { }; struct ImageFile; +class ApiHandler; +struct ApiServer; class ImageService { public: @@ -67,6 +69,8 @@ class ImageService { struct GlobalFs global_fs; std::unique_ptr metrics; ExporterServer *exporter = nullptr; + std::unique_ptr api_handler; + ApiServer *api_server = nullptr; private: int read_global_config_and_set(); diff --git a/src/test/image_service_test.cpp b/src/test/image_service_test.cpp index 392f94b1..f665b042 100644 --- a/src/test/image_service_test.cpp +++ b/src/test/image_service_test.cpp @@ -26,7 +26,6 @@ #include "photon/net/curl.h" #include "../version.h" #include -#include #include #include @@ -164,21 +163,38 @@ TEST(http_client, user_agent) { EXPECT_EQ(true, buf == "success"); } -class DevIDTest : public ::testing::Test { +class DevIDGetTest : public ::testing::Test { +public: + virtual void SetUp() override {} + virtual void TearDown() override {} +}; + +TEST_F(DevIDGetTest, get_dev_id) { + std::string config_path, dev_id; + parse_config_and_dev_id("path/to/config.v1.json;123", config_path, dev_id); + EXPECT_EQ(config_path, "path/to/config.v1.json"); + EXPECT_EQ(dev_id, "123"); + + parse_config_and_dev_id("path/to/config.v1.json", config_path, dev_id); + EXPECT_EQ(config_path, "path/to/config.v1.json"); + EXPECT_EQ(dev_id, ""); +} + +class DevIDRegisterTest : public DevIDGetTest { public: ImageService *imgservice; const std::string test_dir = "/tmp/overlaybd"; const std::string global_config_path = test_dir + "/global_config.json"; const std::string image_config_path = test_dir + "/image_config.json"; - const std::string global_config_content = R"delimiter({ - "enableAudit": false, - "logPath": "", - "p2pConfig": { - "enable": false, - "address": "localhost:64210" - } + std::string global_config_content = R"delimiter({ + "enableAudit": false, + "logPath": "", + "p2pConfig": { + "enable": false, + "address": "localhost:64210" + } })delimiter"; - const std::string image_config_content = R"delimiter({ + std::string image_config_content = R"delimiter({ "lowers" : [ { "file" : "/opt/overlaybd/baselayers/ext4_64" @@ -187,7 +203,6 @@ class DevIDTest : public ::testing::Test { })delimiter"; virtual void SetUp() override { - // set_log_output_level(0); system(("mkdir -p " + test_dir).c_str()); system(("echo \'" + global_config_content + "\' > " + global_config_path).c_str()); @@ -210,21 +225,7 @@ class DevIDTest : public ::testing::Test { } }; -TEST_F(DevIDTest, parse_config_with_dev_id) { - std::string config_path, dev_id; - parse_config_and_dev_id("path/to/config.v1.json;123", config_path, dev_id); - EXPECT_EQ(config_path, "path/to/config.v1.json"); - EXPECT_EQ(dev_id, "123"); -} - -TEST_F(DevIDTest, parse_config_without_dev_id) { - std::string config_path, dev_id; - parse_config_and_dev_id("path/to/config.v1.json", config_path, dev_id); - EXPECT_EQ(config_path, "path/to/config.v1.json"); - EXPECT_EQ(dev_id, ""); -} - -TEST_F(DevIDTest, registers) { +TEST_F(DevIDRegisterTest, register_dev_id) { ImageFile* imagefile0 = imgservice->create_image_file(image_config_path.c_str(), ""); ImageFile* imagefile1 = imgservice->create_image_file(image_config_path.c_str(), "111"); ImageFile* imagefile2 = imgservice->create_image_file(image_config_path.c_str(), "222"); @@ -257,6 +258,49 @@ TEST_F(DevIDTest, registers) { delete imagefile3; } +class SnapshotTest : public DevIDRegisterTest { +public: + virtual void SetUp() override { + global_config_content = R"delimiter({ + "enableAudit": false, + "logLevel": 1, + "logPath": "", + "p2pConfig": { + "enable": false, + "address": "localhost:64210" + }, + "serviceConfig": { + "enable": true + } +})delimiter"; + + DevIDRegisterTest::SetUp(); + } + long request_snapshot(const char* request_url) { + auto request = new photon::net::cURL(); + DEFER({ delete request; }); + + LOG_INFO("request url: `", request_url); + photon::net::StringWriter writer; + auto ret = request->POST(request_url, &writer, (int64_t)1000000); + LOG_INFO("response: `", writer.string); + return ret; + } +}; + +TEST_F(SnapshotTest, http_server) { + ImageFile* imgfile = imgservice->create_image_file(image_config_path.c_str(), "123"); + EXPECT_NE(imgfile, nullptr); + + EXPECT_EQ(request_snapshot("http://localhost:9862/snapshot"), 400); + EXPECT_EQ(request_snapshot("http://localhost:9862/snapshot?V#RNWQC&*@#"), 400); + EXPECT_EQ(request_snapshot("http://localhost:9862/snapshot?dev_id=&config=/tmp/overlaybd/config.json"), 400); + EXPECT_EQ(request_snapshot("http://localhost:9862/snapshot?dev_id=456&config=/tmp/overlaybd/config.json"), 404); + EXPECT_EQ(request_snapshot("http://localhost:9862/snapshot?dev_id=123&config=/tmp/overlaybd/config.json"), 200); + + delete imgfile; +} + int main(int argc, char** argv) { photon::init(photon::INIT_EVENT_DEFAULT, photon::INIT_IO_DEFAULT); DEFER(photon::fini();); From 02600d1e660f10e4ba118271137206008fe0e4c2 Mon Sep 17 00:00:00 2001 From: Xun Chen Date: Wed, 24 Dec 2025 11:54:48 +0800 Subject: [PATCH 4/4] build: switch erofs-utils fetch repository to GitHub Signed-off-by: Xun Chen --- src/overlaybd/tar/erofs/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/overlaybd/tar/erofs/CMakeLists.txt b/src/overlaybd/tar/erofs/CMakeLists.txt index 6027102e..57d5b762 100644 --- a/src/overlaybd/tar/erofs/CMakeLists.txt +++ b/src/overlaybd/tar/erofs/CMakeLists.txt @@ -2,7 +2,7 @@ include(FetchContent) FetchContent_Declare( erofs-utils - GIT_REPOSITORY https://git.kernel.org/pub/scm/linux/kernel/git/xiang/erofs-utils.git + GIT_REPOSITORY https://github.com/erofs/erofs-utils.git GIT_TAG eec6f7a2755dfccc8f655aa37cf6f26db9164e60 )