diff --git a/src/api_server.cpp b/src/api_server.cpp index 37aaa8d6..b675307d 100644 --- a/src/api_server.cpp +++ b/src/api_server.cpp @@ -27,28 +27,37 @@ 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 { + if (req.verb() != photon::net::http::Verb::POST) { + resp.set_result(405); + std::string msg = R"({"success":false,"message":"Method not allowed"})"; + resp.headers.content_length(msg.size()); + return resp.write((void *)msg.data(), msg.size()) == (ssize_t)msg.size() ? 0 : -1; + } + 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); } - // auto query = req.query(); - LOG_INFO("Snapshot query: `", query); // string view, format: dev_id=${devID}&config=${config} - parse_params(query); + LOG_INFO("Snapshot request received"); + std::map params; + if (!parse_params(query, params)) { + resp.set_result(400); + std::string msg = R"({"success":false,"message":"Malformed or duplicate query parameters"})"; + resp.headers.content_length(msg.size()); + return resp.write((void *)msg.data(), msg.size()) == (ssize_t)msg.size() ? 0 : -1; + } 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; @@ -59,23 +68,24 @@ class ApiHandler : public photon::net::http::HTTPHandler { goto EXIT; } - img_file = imgservice->find_image_file(dev_id); - if (!img_file) { - code = 404; - msg = std::string(R"delimiter({ + { + int snap_ret = imgservice->create_snapshot_for_device(dev_id, config_path.c_str()); + if (snap_ret == -2) { + 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({ + goto EXIT; + } + if (snap_ret < 0) { + code = 500; + msg = std::string(R"delimiter({ "success": false, - "message": "Failed to create snapshot`" + "message": "Failed to create snapshot" })delimiter"); - goto EXIT; + goto EXIT; + } } code = 200; @@ -90,40 +100,46 @@ class ApiHandler : public photon::net::http::HTTPHandler { 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_ERRNO_RETURN(0, -1, "send body failed, target path /snapshot, `", VALUE(ret_w)); } LOG_DEBUG("send body done"); return 0; } - void parse_params(std::string_view query) { // format: dev_id=${devID}&config=${config}... + // Returns false on malformed input or duplicate keys. + static bool parse_params(std::string_view query, + std::map ¶ms) { if (query.empty()) - return; - + return true; + 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('='); + std::string decoded_key; + std::string decoded_value; 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; + decoded_key = photon::net::http::url_unescape(key); + decoded_value = photon::net::http::url_unescape(value); } else { - // key without value - auto key = photon::net::http::url_unescape(param); - params[key] = ""; + decoded_key = photon::net::http::url_unescape(param); + decoded_value = ""; } + if (decoded_key.empty()) + return false; + if (params.find(decoded_key) != params.end()) + return false; + params[decoded_key] = decoded_value; start = end + 1; } + return true; } }; diff --git a/src/image_file.cpp b/src/image_file.cpp index 20f2b1f9..109c57e5 100644 --- a/src/image_file.cpp +++ b/src/image_file.cpp @@ -549,44 +549,222 @@ void ImageFile::set_auth_failed() { } } -template -void ImageFile::set_failed(const Ts &...xs) { - if (m_status == 0) // only set exit in image boot phase - { - m_status = -1; - m_exception = estring().appends(xs...); +bool ImageFile::layer_config_match(ImageConfigNS::LayerConfig &a, + ImageConfigNS::LayerConfig &b) { + return a.gzipIndex() == b.gzipIndex() && a.file() == b.file() && + a.targetFile() == b.targetFile() && a.dir() == b.dir() && + a.digest() == b.digest() && a.targetDigest() == b.targetDigest() && + a.size() == b.size(); +} + +bool ImageFile::configs_match(ImageConfigNS::ImageConfig &a, + ImageConfigNS::ImageConfig &b) { + auto a_lowers = a.lowers(); + auto b_lowers = b.lowers(); + if (a_lowers.size() != b_lowers.size()) + return false; + for (size_t i = 0; i < a_lowers.size(); ++i) { + if (!layer_config_match(a_lowers[i], b_lowers[i])) + return false; + } + return a.upper().index() == b.upper().index() && a.upper().data() == b.upper().data(); +} + +int ImageFile::read_text_file(const std::string &path, std::string &out) { + int fd = ::open(path.c_str(), O_RDONLY); + if (fd < 0) + return -1; + DEFER(::close(fd)); + out.clear(); + char buf[4096]; + for (;;) { + ssize_t n = ::read(fd, buf, sizeof(buf)); + if (n < 0) + return -1; + if (n == 0) + break; + out.append(buf, n); } + while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) + out.pop_back(); + return 0; +} + +int ImageFile::write_text_file_fsync(const std::string &path, const std::string &data) { + int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) + return -1; + size_t off = 0; + while (off < data.size()) { + ssize_t n = ::write(fd, data.data() + off, data.size() - off); + if (n < 0) { + ::close(fd); + return -1; + } + off += (size_t)n; + } + if (::fsync(fd) != 0) { + ::close(fd); + return -1; + } + ::close(fd); + + // Fsync the parent directory so the create/replace is durable. + auto slash = path.find_last_of('/'); + std::string dir = (slash == std::string::npos) ? "." : path.substr(0, slash); + int dfd = ::open(dir.c_str(), O_RDONLY | O_DIRECTORY); + if (dfd >= 0) { + ::fsync(dfd); + ::close(dfd); + } + return 0; +} + +int ImageFile::install_canonical_config(const std::string &src_path) { + auto lfs = photon::fs::new_localfs_adaptor(); + if (lfs == nullptr) + return -1; + DEFER(delete lfs); + std::string tmp = config_path + ".restack.tmp"; + // Copy intent bytes to a same-dir temp then rename over canonical config. + std::string bytes; + if (read_text_file(src_path, bytes) != 0) + return -1; + if (write_text_file_fsync(tmp, bytes) != 0) { + unlink(tmp.c_str()); + return -1; + } + if (lfs->rename(tmp.c_str(), config_path.c_str()) != 0) { + unlink(tmp.c_str()); + return -1; + } + return 0; } int ImageFile::create_snapshot(const char *new_config_path) { // load new config file to get the snapshot layer path // open new upper layer // restack() current RW layer as snapshot layer + photon::scoped_rwlock lock(m_io_lock, photon::WLOCK); + if(!m_lower_file || !m_upper_file) LOG_ERROR_RETURN(0, -1, "Lower or upper layer is NULL."); + const std::string intent_path = config_path + ".restack.intent"; + const std::string phase_path = config_path + ".restack.phase"; + + // Finish a previously mutated restack whose canonical config publish may + // have been interrupted (lost response / crash after restack). + { + std::string phase; + if (read_text_file(phase_path, phase) == 0 && phase == "mutated") { + ImageConfigNS::ImageConfig intent_cfg; + if (!intent_cfg.ParseJSON(intent_path.c_str())) { + LOG_ERROR_RETURN(0, -1, + "Restack journal is mutated but intent config is unreadable; " + "refusing further restacks until repaired."); + } + if (configs_match(conf, intent_cfg)) { + if (install_canonical_config(intent_path) != 0) { + LOG_ERROR_RETURN(0, -1, "Failed to finish restack journal publish."); + } + unlink(phase_path.c_str()); + unlink(intent_path.c_str()); + return 0; + } + LOG_ERROR_RETURN(0, -1, + "Restack journal is mutated but live config does not match intent; " + "refusing a second restack."); + } + // Pre-mutation intent is safe to discard before starting a new attempt. + if (phase == "prepared") { + unlink(phase_path.c_str()); + unlink(intent_path.c_str()); + } + } + ImageConfigNS::ImageConfig new_cfg; LSMT::IFileRW *upper_file = nullptr; - LOG_INFO("Load new config `.", new_config_path); + LOG_INFO("Load new config for restack."); if (!new_cfg.ParseJSON(new_config_path)) { - LOG_ERROR_RETURN(0, -1, "Error parse new config json: `.", new_config_path); + LOG_ERROR_RETURN(0, -1, "Error parse new config json."); + } + + // Idempotent retry: canonical stack already matches the requested next config. + if (configs_match(conf, new_cfg)) { + LOG_INFO("Restack request already applied; returning success."); + unlink(phase_path.c_str()); + unlink(intent_path.c_str()); + return 0; + } + + auto current_lowers = conf.lowers(); + auto new_lowers = new_cfg.lowers(); + if (new_lowers.size() != current_lowers.size() + 1) { + LOG_ERROR_RETURN(0, -1, + "The new config must preserve all current lowers and append the current upper (current: `, new: `).", + current_lowers.size(), new_lowers.size()); + } + for (size_t i = 0; i < current_lowers.size(); ++i) { + auto ¤t = current_lowers[i]; + auto &next = new_lowers[i]; + if (next.gzipIndex() != current.gzipIndex() || next.file() != current.file() || + next.targetFile() != current.targetFile() || next.dir() != current.dir() || + next.digest() != current.digest() || next.targetDigest() != current.targetDigest() || + next.size() != current.size()) { + LOG_ERROR_RETURN(0, -1, "The new config changes existing lower layer `.", i); + } + } + + auto current_upper = conf.upper(); + auto &snapshot_lower = new_lowers.back(); + if (snapshot_lower.file() != current_upper.data() || !snapshot_lower.gzipIndex().empty() || + !snapshot_lower.targetFile().empty() || !snapshot_lower.dir().empty() || + !snapshot_lower.digest().empty() || !snapshot_lower.targetDigest().empty() || + snapshot_lower.size() != 0) { + LOG_ERROR_RETURN(0, -1, + "The newest lower must reference only the current upper data file."); } auto upper = new_cfg.upper(); - // auto lowers = new_cfg.lowers(); - // if(lowers[lowers.size()-1].file() != conf.upper().data()) - // LOG_ERROR_RETURN(0, -1, "The last lower layer(`) should be the same as old upper layer(`) after restack.", lowers[lowers.size()-1].file(), conf.upper().data()); if(upper.index() == conf.upper().index() || upper.data() == conf.upper().data()) - LOG_ERROR_RETURN(0, -1, "The new upper layer(`, `) should be different from the old upper layer(`, `).", upper.data(), upper.index(), conf.upper().data(), conf.upper().index()); + LOG_ERROR_RETURN(0, -1, "The new upper layer should be different from the old upper layer."); + + // Persist intent before mutating the live device so a lost response can + // distinguish pre-mutation retry from post-mutation repair. + std::string new_cfg_bytes; + if (read_text_file(new_config_path, new_cfg_bytes) != 0) { + LOG_ERROR_RETURN(0, -1, "Failed to read next config for restack intent."); + } + if (write_text_file_fsync(intent_path, new_cfg_bytes) != 0 || + write_text_file_fsync(phase_path, "prepared") != 0) { + unlink(intent_path.c_str()); + unlink(phase_path.c_str()); + LOG_ERROR_RETURN(0, -1, "Failed to persist restack intent."); + } upper_file = open_upper(upper); - if (!upper_file) + if (!upper_file) { + unlink(intent_path.c_str()); + unlink(phase_path.c_str()); LOG_ERROR_RETURN(0, -1, "Open upper layer failed."); - - if(((LSMT::IFileRW *)m_file)->restack(upper_file) != 0) + } + + if(((LSMT::IFileRW *)m_file)->restack(upper_file) != 0) { + delete upper_file; + unlink(intent_path.c_str()); + unlink(phase_path.c_str()); LOG_ERRNO_RETURN(0, -1, "Restack new rwlayer failed."); - + } + + // Mutation applied: further failure must not restack again. + if (write_text_file_fsync(phase_path, "mutated") != 0) { + LOG_ERROR_RETURN(0, -1, + "Restack succeeded but failed to mark journal mutated; " + "refusing further restacks until repaired."); + } + if(m_upper_file) { // transfer the sealed layer from m_upper_file to m_lower_file before m_upper_file is destructed auto sealed = ((LSMT::IFileRW *)m_upper_file)->get_file(0); @@ -603,22 +781,34 @@ int ImageFile::create_snapshot(const char *new_config_path) { m_upper_file = upper_file; - // overwrite the config file in use in case the old files are used again after the process restarts - auto lfs = photon::fs::new_localfs_adaptor(); - if (lfs == nullptr) { - LOG_ERRNO_RETURN(0, -1, "new localfs_adaptor failed"); - } - DEFER(delete lfs); - int ret = lfs->rename(new_config_path, this->config_path.c_str()); - if (ret != 0) { - LOG_ERRNO_RETURN(0, -1, "rename(`,`) failed", new_config_path, this->config_path); + // Prefer installing from the durable intent so retries use identical bytes. + if (install_canonical_config(intent_path) != 0) { + // Fall back to the caller-provided next config path if still present. + auto lfs = photon::fs::new_localfs_adaptor(); + if (lfs == nullptr) { + LOG_ERROR_RETURN(0, -1, + "Restack mutated but failed to publish canonical config; " + "refusing further restacks until repaired."); + } + DEFER(delete lfs); + if (lfs->rename(new_config_path, this->config_path.c_str()) != 0) { + LOG_ERROR_RETURN(0, -1, + "Restack mutated but failed to publish canonical config; " + "refusing further restacks until repaired."); + } } - LOG_INFO("rename(`,`) success", new_config_path, this->config_path); - + + conf.CopyFrom(new_cfg, conf.GetAllocator()); + unlink(phase_path.c_str()); + unlink(intent_path.c_str()); + // Caller next-config may still exist if install used intent; remove if leftover. + unlink(new_config_path); + return 0; } int ImageFile::resize(uint64_t target_size, bool resize_fs) { + photon::scoped_rwlock lock(m_io_lock, photon::WLOCK); if (read_only) { LOG_ERROR_RETURN(EROFS, -1, "cannot resize a read-only image (no upper layer)"); } diff --git a/src/image_file.h b/src/image_file.h index 196f3abf..98883285 100644 --- a/src/image_file.h +++ b/src/image_file.h @@ -46,7 +46,8 @@ class ImageFile : public photon::fs::ForwardFile { 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); + // Do not echo the device ID; registration is a global selector. + set_failed("duplicated live-snapshot device id"); return; } m_dev_id = dev_id; @@ -73,6 +74,7 @@ class ImageFile : public photon::fs::ForwardFile { } int fstat(struct stat *buf) override { + photon::scoped_rwlock lock(m_io_lock, photon::RLOCK); int ret = m_file->fstat(buf); block_size = buf->st_blksize; size = buf->st_size; @@ -83,6 +85,7 @@ class ImageFile : public photon::fs::ForwardFile { } ssize_t pwritev(const struct iovec *iov, int iovcnt, off_t offset) override { + photon::scoped_rwlock lock(m_io_lock, photon::RLOCK); if (read_only) { LOG_ERROR_RETURN(EROFS, -1, "writing read only file"); } @@ -90,14 +93,17 @@ class ImageFile : public photon::fs::ForwardFile { } ssize_t preadv(const struct iovec *iov, int iovcnt, off_t offset) override { + photon::scoped_rwlock lock(m_io_lock, photon::RLOCK); return m_file->preadv(iov, iovcnt, offset); } int fdatasync() override { + photon::scoped_rwlock lock(m_io_lock, photon::RLOCK); return m_file->fdatasync(); } int fallocate(int mode, off_t offset, off_t len) override { + photon::scoped_rwlock lock(m_io_lock, photon::RLOCK); return m_file->fallocate(mode, offset, len); } @@ -136,12 +142,30 @@ class ImageFile : public photon::fs::ForwardFile { photon::fs::IFile *m_lower_file = nullptr; photon::fs::IFile *m_upper_file = nullptr; std::string m_dev_id = ""; + photon::rwlock m_io_lock; int init_image_file(); - template void set_failed(const Ts&...xs); + template + void set_failed(const Ts &...xs) { + if (m_status == 0) // only set exit in image boot phase + { + m_status = -1; + m_exception = estring().appends(xs...); + } + } LSMT::IFileRO *open_lowers(std::vector &, bool &); LSMT::IFileRW *open_upper(ImageConfigNS::UpperConfig &); + // LayerConfig/ImageConfig accessors are non-const in the generated config + // helpers, so these helpers take mutable refs. + static bool layer_config_match(ImageConfigNS::LayerConfig &a, + ImageConfigNS::LayerConfig &b); + static bool configs_match(ImageConfigNS::ImageConfig &a, + ImageConfigNS::ImageConfig &b); + static int read_text_file(const std::string &path, std::string &out); + static int write_text_file_fsync(const std::string &path, const std::string &data); + int install_canonical_config(const std::string &src_path); + IFile *open_localfile(ImageConfigNS::LayerConfig &layer, std::string &opened); IFile *__open_ro_file(const std::string &); IFile *__open_ro_target_file(const std::string &); diff --git a/src/image_service.cpp b/src/image_service.cpp index e754770a..20839ddb 100644 --- a/src/image_service.cpp +++ b/src/image_service.cpp @@ -607,26 +607,40 @@ ImageFile *ImageService::create_image_file(const char *config_path, const std::s 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); + photon::scoped_rwlock lock(m_files_lock, photon::WLOCK); + if (m_image_files.find(dev_id) != m_image_files.end()) + LOG_ERROR_RETURN(0, -1, "dev id already registered"); m_image_files[dev_id] = file; - LOG_INFO("Registered image file for dev_id: `", dev_id); + LOG_INFO("Registered image file for device"); return 0; } int ImageService::unregister_image_file(const std::string& dev_id) { if (dev_id.empty()) return 0; + photon::scoped_rwlock lock(m_files_lock, photon::WLOCK); m_image_files.erase(dev_id); - LOG_INFO("Unregistered image file for dev_id: `", dev_id); + LOG_INFO("Unregistered image file for device"); return 0; } ImageFile* ImageService::find_image_file(const std::string& dev_id) { + photon::scoped_rwlock lock(m_files_lock, photon::RLOCK); auto it = m_image_files.find(dev_id); return (it != m_image_files.end()) ? it->second : nullptr; } +int ImageService::create_snapshot_for_device(const std::string& dev_id, + const char *config_path) { + // Exclusive lock: prevent unregister/delete while restack holds a raw pointer. + photon::scoped_rwlock lock(m_files_lock, photon::WLOCK); + auto it = m_image_files.find(dev_id); + if (it == m_image_files.end() || it->second == nullptr) { + LOG_ERROR_RETURN(0, -2, "Image file not found for device"); + } + return it->second->create_snapshot(config_path); +} + 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 a2278e23..f9e6856a 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 #include using namespace photon::fs; @@ -62,6 +63,9 @@ class ImageService { 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); + // Holds the device registry lock for the duration of the restack so the + // ImageFile* cannot be unregistered/freed mid-call. + int create_snapshot_for_device(const std::string& dev_id, const char *config_path); ImageConfigNS::GlobalConfig global_conf; @@ -76,6 +80,7 @@ class ImageService { void set_result_file(std::string &filename, std::string &data); std::string m_config_path; std::unordered_map m_image_files; // dev_id -> ImageFile* + photon::rwlock m_files_lock; }; ImageService *create_image_service(const char *config_path = nullptr); diff --git a/src/test/image_service_test.cpp b/src/test/image_service_test.cpp index ae1a8a5f..a2ff28f0 100644 --- a/src/test/image_service_test.cpp +++ b/src/test/image_service_test.cpp @@ -30,6 +30,7 @@ #include #include +#include #include "../image_service.cpp" #include "../image_service.h" @@ -278,7 +279,8 @@ class HTTPServerTest : public DevIDRegisterTest { DevIDRegisterTest::SetUp(); } - int request_snapshot(const char* request_url) { + int request_snapshot(const char* request_url, + photon::net::http::Verb verb = photon::net::http::Verb::POST) { // auto request = new photon::net::cURL(); // DEFER({ delete request; }); @@ -290,7 +292,7 @@ class HTTPServerTest : public DevIDRegisterTest { auto client = photon::net::http::new_http_client(); DEFER(delete client); - auto op = client->new_operation(photon::net::http::Verb::GET, request_url); + auto op = client->new_operation(verb, request_url); DEFER(delete op); op->req.headers.content_length(0); // std::cout << "op->req.target(): " << op->req.target() << " op->req.query(): " << op->req.query() << std::endl; @@ -309,10 +311,13 @@ TEST_F(HTTPServerTest, 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", + photon::net::http::Verb::GET), 405); 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"), 400); EXPECT_EQ(request_snapshot("http://localhost:9862/snapshot?dev_id=123&config=/tmp/overlaybd/config.json"), 500); delete imgfile; @@ -367,7 +372,7 @@ class CreateSnapshotTest : public DevIDRegisterTest { srand(154574045); } - void create_file_rw(char *data_name, char *index_name, bool sparse = false) { + void create_file_rw(const char *data_name, const char *index_name, bool sparse = false) { auto fdata = photon::fs::open_localfile_adaptor(data_name, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); auto findex = photon::fs::open_localfile_adaptor(index_name, O_RDWR | O_CREAT | O_TRUNC, S_IRWXU); LSMT::LayerInfo args(fdata, findex); @@ -378,6 +383,124 @@ class CreateSnapshotTest : public DevIDRegisterTest { } }; +TEST_F(CreateSnapshotTest, create_snapshot_twice_preserves_delta_chain) { + create_file_rw("/tmp/overlaybd/data0.lsmt", "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + create_file_rw("/tmp/overlaybd/data2.lsmt", "/tmp/overlaybd/index2.lsmt"); + + ImageFile* image = imgservice->create_image_file(image_config_path.c_str(), ""); + ASSERT_NE(image, nullptr); + + ALIGNED_MEM4K(first, 4096); + ALIGNED_MEM4K(second, 4096); + ALIGNED_MEM4K(third, 4096); + ALIGNED_MEM4K(actual, 4096); + memset(first, 0x11, 4096); + memset(second, 0x22, 4096); + memset(third, 0x33, 4096); + + ASSERT_EQ(PWRITEV_SINGLE(image, first, 4096, 0), 4096); + ASSERT_EQ(image->create_snapshot(new_image_config_path.c_str()), 0); + + ASSERT_EQ(PWRITEV_SINGLE(image, second, 4096, 4096), 4096); + new_image_config_content = R"delimiter({ + "lowers" : [ + { + "file" : "/opt/overlaybd/baselayers/ext4_64" + }, + { + "file" : "/tmp/overlaybd/data0.lsmt" + }, + { + "file" : "/tmp/overlaybd/data1.lsmt" + } + ], + "upper": { + "index": "/tmp/overlaybd/index2.lsmt", + "data": "/tmp/overlaybd/data2.lsmt" + } +})delimiter"; + ASSERT_EQ(system(("echo '" + new_image_config_content + "' > " + new_image_config_path).c_str()), 0); + ASSERT_EQ(image->create_snapshot(new_image_config_path.c_str()), 0); + + ASSERT_EQ(PWRITEV_SINGLE(image, third, 4096, 8192), 4096); + ASSERT_EQ(PREADV_SINGLE(image, actual, 4096, 0), 4096); + EXPECT_EQ(memcmp(actual, first, 4096), 0); + ASSERT_EQ(PREADV_SINGLE(image, actual, 4096, 4096), 4096); + EXPECT_EQ(memcmp(actual, second, 4096), 0); + ASSERT_EQ(PREADV_SINGLE(image, actual, 4096, 8192), 4096); + EXPECT_EQ(memcmp(actual, third, 4096), 0); + + ImageFile* reopened = imgservice->create_image_file(image_config_path.c_str(), ""); + ASSERT_NE(reopened, nullptr); + ASSERT_EQ(PREADV_SINGLE(reopened, actual, 4096, 0), 4096); + EXPECT_EQ(memcmp(actual, first, 4096), 0); + ASSERT_EQ(PREADV_SINGLE(reopened, actual, 4096, 4096), 4096); + EXPECT_EQ(memcmp(actual, second, 4096), 0); + ASSERT_EQ(PREADV_SINGLE(reopened, actual, 4096, 8192), 4096); + EXPECT_EQ(memcmp(actual, third, 4096), 0); + + delete reopened; + delete image; +} + +TEST_F(CreateSnapshotTest, create_snapshot_idempotent_retry) { + create_file_rw("/tmp/overlaybd/data0.lsmt", "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + + ImageFile* image = imgservice->create_image_file(image_config_path.c_str(), ""); + ASSERT_NE(image, nullptr); + + ALIGNED_MEM4K(buf, 4096); + memset(buf, 0xab, 4096); + ASSERT_EQ(PWRITEV_SINGLE(image, buf, 4096, 0), 4096); + ASSERT_EQ(image->create_snapshot(new_image_config_path.c_str()), 0); + + // Replay the same next-config after success (lost-response retry). + ASSERT_EQ(system(("echo '" + new_image_config_content + "' > " + new_image_config_path).c_str()), 0); + ASSERT_EQ(image->create_snapshot(new_image_config_path.c_str()), 0); + + ALIGNED_MEM4K(actual, 4096); + ASSERT_EQ(PREADV_SINGLE(image, actual, 4096, 0), 4096); + EXPECT_EQ(memcmp(actual, buf, 4096), 0); + delete image; +} + +TEST_F(CreateSnapshotTest, create_snapshot_rejects_lower_reordering) { + create_file_rw("/tmp/overlaybd/data0.lsmt", "/tmp/overlaybd/index0.lsmt"); + create_file_rw("/tmp/overlaybd/data1.lsmt", "/tmp/overlaybd/index1.lsmt"); + + ImageFile* image = imgservice->create_image_file(image_config_path.c_str(), ""); + ASSERT_NE(image, nullptr); + + std::string bad = R"delimiter({ + "lowers" : [ + { + "file" : "/tmp/overlaybd/data0.lsmt" + }, + { + "file" : "/opt/overlaybd/baselayers/ext4_64" + } + ], + "upper": { + "index": "/tmp/overlaybd/index1.lsmt", + "data": "/tmp/overlaybd/data1.lsmt" + } +})delimiter"; + ASSERT_EQ(system(("echo '" + bad + "' > " + new_image_config_path).c_str()), 0); + EXPECT_EQ(image->create_snapshot(new_image_config_path.c_str()), -1); + delete image; +} + +TEST_F(HTTPServerTest, rejects_duplicate_query_params) { + ImageFile* imgfile = imgservice->create_image_file(image_config_path.c_str(), "123"); + EXPECT_NE(imgfile, nullptr); + EXPECT_EQ(request_snapshot( + "http://localhost:9862/snapshot?dev_id=123&dev_id=456&config=/tmp/overlaybd/config.json"), + 400); + delete imgfile; +} + TEST_F(CreateSnapshotTest, create_snapshot) { // imagefile0->pwrite( buf, 0, 1MB) // imagefile0->restack(xxx) //s config.v1.json.new