Skip to content

Commit 6aa64b6

Browse files
committed
feat: add live snapshot API with device ID tracking and layer restacking
This commit introduces a live snapshot feature for overlaybd with the following key capabilities: - Add HTTP API server (/snapshot endpoint) for creating snapshots on demand - Implement create_snapshot() to dynamically restack RW layers - Add device ID tracking system for image file management - Support config_path;dev_id format in configuration strings - Extend LSMT layer management with restack(), index manipulation, and file operations - Add comprehensive tests for restacking, device registration, and snapshot creation Co-authored-by: Xun Chen <xunchen@hust.edu.cn> Co-authored-by: Yifan Yuan <tuji.yyf@alibaba-inc.com> Signed-off-by: Yifan Yuan <tuji.yyf@alibaba-inc.com>
1 parent 222aa97 commit 6aa64b6

File tree

20 files changed

+918
-22
lines changed

20 files changed

+918
-22
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ To build overlaybd from source code, the following dependencies are required:
6060
* Libaio, libcurl, libnl3, glib2 and openssl runtime and development libraries.
6161
* CentOS 7/Fedora: `sudo yum install libaio-devel libcurl-devel openssl-devel libnl3-devel libzstd-static e2fsprogs-devel`
6262
* CentOS 8: `sudo yum install libaio-devel libcurl-devel openssl-devel libnl3-devel libzstd-devel e2fsprogs-devel`
63-
* Debian/Ubuntu: `sudo apt install libcurl4-openssl-dev libssl-dev libaio-dev libnl-3-dev libnl-genl-3-dev libgflags-dev libzstd-dev libext2fs-dev`
63+
* Debian/Ubuntu: `sudo apt install libcurl4-openssl-dev libssl-dev libaio-dev libnl-3-dev libnl-genl-3-dev libgflags-dev libzstd-dev libext2fs-dev pkg-config automake libtool # libgtest-dev // for test`
6464
* Mariner/AzureLinux: `sudo yum install libaio-devel libcurl-devel openssl-devel libnl3-devel e2fsprogs-devel glibc-devel libzstd-devel binutils ca-certificates-microsoft build-essential`
6565

6666
#### Build

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ add_library(overlaybd_image_lib
1515
prefetch.cpp
1616
tools/sha256file.cpp
1717
tools/comm_func.cpp
18+
api_server.cpp
1819
)
1920
target_include_directories(overlaybd_image_lib PUBLIC
2021
${CURL_INCLUDE_DIRS}

src/api_server.cpp

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
Copyright The Overlaybd Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#include <photon/net/http/url.h>
18+
#include <string_view>
19+
#include "image_service.h"
20+
#include "image_file.h"
21+
#include "api_server.h"
22+
23+
int ApiHandler::handle_request(photon::net::http::Request& req,
24+
photon::net::http::Response& resp,
25+
std::string_view) {
26+
auto target = req.target(); // string view, format: /snapshot?dev_id=${devID}&config=${config}
27+
std::string_view query("");
28+
auto pos = target.find('?');
29+
if (pos != std::string_view::npos) {
30+
query = target.substr(pos + 1);
31+
}
32+
LOG_DEBUG("Snapshot query: `", query); // string view, format: dev_id=${devID}&config=${config}
33+
parse_params(query);
34+
auto dev_id = params["dev_id"];
35+
auto config_path = params["config"];
36+
LOG_DEBUG("dev_id: `, config: `", dev_id, config_path);
37+
38+
int code;
39+
std::string msg;
40+
ImageFile* img_file = nullptr;
41+
42+
if (dev_id.empty() || config_path.empty()) {
43+
code = 400;
44+
msg = std::string(R"delimiter({
45+
"success": false,
46+
"message": "Missing dev_id or config in snapshot request"
47+
})delimiter");
48+
goto EXIT;
49+
}
50+
51+
img_file = imgservice->find_image_file(dev_id);
52+
if (!img_file) {
53+
code = 404;
54+
msg = std::string(R"delimiter({
55+
"success": false,
56+
"message": "Image file not found"
57+
})delimiter");
58+
goto EXIT;
59+
}
60+
61+
if (img_file->create_snapshot(config_path.c_str()) < 0) {
62+
code = 500;
63+
msg = std::string(R"delimiter({
64+
"success": false,
65+
"message": "Failed to create snapshot`"
66+
})delimiter");
67+
goto EXIT;
68+
}
69+
70+
code = 200;
71+
msg = std::string(R"delimiter({
72+
"success": true,
73+
"message": "Snapshot created successfully"
74+
})delimiter");
75+
76+
EXIT:
77+
resp.set_result(code);
78+
resp.headers.content_length(msg.size());
79+
resp.keep_alive(true);
80+
auto ret_w = resp.write((void*)msg.c_str(), msg.size());
81+
if (ret_w != (ssize_t)msg.size()) {
82+
LOG_ERRNO_RETURN(0, -1, "send body failed, target: `, `", req.target(), VALUE(ret_w));
83+
}
84+
LOG_DEBUG("send body done");
85+
return 0;
86+
}
87+
88+
void ApiHandler::parse_params(std::string_view query) { // format: dev_id=${devID}&config=${config}...
89+
if (query.empty())
90+
return;
91+
92+
size_t start = 0;
93+
while (start < query.length()) {
94+
auto end = query.find('&', start);
95+
if (end == std::string_view::npos) { // last one
96+
end = query.length();
97+
}
98+
99+
auto param = query.substr(start, end - start);
100+
auto eq_pos = param.find('=');
101+
if (eq_pos != std::string_view::npos) {
102+
auto key = param.substr(0, eq_pos);
103+
auto value = param.substr(eq_pos + 1);
104+
105+
// url decode
106+
auto decoded_key = photon::net::http::url_unescape(key);
107+
auto decoded_value = photon::net::http::url_unescape(value);
108+
params[decoded_key] = decoded_value;
109+
} else {
110+
// key without value
111+
auto key = photon::net::http::url_unescape(param);
112+
params[key] = "";
113+
}
114+
start = end + 1;
115+
}
116+
}
117+
118+
ApiServer::ApiServer(const std::string &addr, ApiHandler* handler) {
119+
photon::net::http::URL url(addr);
120+
std::string host = url.host().data(); // the string pointed by data() doesn't end up with '\0'
121+
auto pos = host.find(":");
122+
if (pos != host.npos) {
123+
host.resize(pos);
124+
}
125+
tcpserver = photon::net::new_tcp_socket_server();
126+
tcpserver->setsockopt(SOL_SOCKET, SO_REUSEPORT, 1);
127+
if(tcpserver->bind(url.port(), photon::net::IPAddr(host.c_str())) < 0)
128+
LOG_ERRNO_RETURN(0, , "Failed to bind api server port `", url.port());
129+
if(tcpserver->listen() < 0)
130+
LOG_ERRNO_RETURN(0, , "Failed to listen api server port `", url.port());
131+
httpserver = photon::net::http::new_http_server();
132+
httpserver->add_handler(handler, false, "/snapshot");
133+
tcpserver->set_handler(httpserver->get_connection_handler());
134+
tcpserver->start_loop();
135+
ready = true;
136+
LOG_DEBUG("Api server listening on `:`, path: `", host, url.port(), "/snapshot");
137+
}
138+
139+
ApiServer::~ApiServer() {
140+
delete tcpserver;
141+
delete httpserver;
142+
}

src/api_server.h

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright The Overlaybd Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <map>
20+
#include <string>
21+
#include <photon/net/http/server.h>
22+
#include <photon/net/socket.h>
23+
24+
class ImageService;
25+
26+
class ApiHandler : public photon::net::http::HTTPHandler {
27+
public:
28+
ImageService *imgservice;
29+
std::map<std::string, std::string> params;
30+
31+
ApiHandler(ImageService *imgservice) : imgservice(imgservice) {}
32+
int handle_request(photon::net::http::Request& req,
33+
photon::net::http::Response& resp,
34+
std::string_view) override;
35+
void parse_params(std::string_view query);
36+
};
37+
38+
struct ApiServer {
39+
photon::net::ISocketServer* tcpserver = nullptr;
40+
photon::net::http::HTTPServer* httpserver = nullptr;
41+
bool ready = false;
42+
43+
ApiServer(const std::string &addr, ApiHandler* handler);
44+
~ApiServer();
45+
};

src/config.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,12 @@ struct CertConfig : public ConfigUtils::Config {
131131
APPCFG_PARA(keyFile, std::string, "");
132132
};
133133

134+
struct ServiceConfig : public ConfigUtils::Config {
135+
APPCFG_CLASS
136+
APPCFG_PARA(enable, bool, false);
137+
APPCFG_PARA(address, std::string, "http://127.0.0.1:9862");
138+
};
139+
134140
struct GlobalConfig : public ConfigUtils::Config {
135141
APPCFG_CLASS
136142

@@ -155,6 +161,7 @@ struct GlobalConfig : public ConfigUtils::Config {
155161
APPCFG_PARA(prefetchConfig, PrefetchConfig);
156162
APPCFG_PARA(certConfig, CertConfig);
157163
APPCFG_PARA(userAgent, std::string, OVERLAYBD_VERSION);
164+
APPCFG_PARA(serviceConfig, ServiceConfig);
158165
};
159166

160167
struct AuthConfig : public ConfigUtils::Config {
@@ -170,4 +177,5 @@ struct ImageAuthResponse : public ConfigUtils::Config {
170177
APPCFG_PARA(data, AuthConfig);
171178
};
172179

180+
173181
} // namespace ImageConfigNS

src/example_config/overlaybd.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,9 @@
3636
},
3737
"enableAudit": true,
3838
"auditPath": "/var/log/overlaybd-audit.log",
39-
"registryFsVersion": "v2"
39+
"registryFsVersion": "v2",
40+
"serviceConfig": {
41+
"enable": false,
42+
"address": "http://127.0.0.1:9862"
43+
}
4044
}

src/image_file.cpp

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,12 +334,12 @@ LSMT::IFileRO *ImageFile::open_lowers(std::vector<ImageConfigNS::LayerConfig> &l
334334
return NULL;
335335

336336
photon::join_handle *ths[PARALLEL_LOAD_INDEX];
337-
std::vector<IFile *> files;
337+
std::vector<IFile *> files; // layer0 ... layerN-1
338338
files.resize(lowers.size(), nullptr);
339339
auto n = std::min(PARALLEL_LOAD_INDEX, (int)lowers.size());
340340
LOG_DEBUG("create ` photon threads to open lowers", n);
341341

342-
ParallelOpenTask tm(files, lowers.size(), lowers);
342+
ParallelOpenTask tm(files, lowers.size(), lowers); // layer0 ... layerN-1
343343
for (auto i = 0; i < n; ++i) {
344344
ths[i] =
345345
photon::thread_enable_join(photon::thread_create11(&do_parallel_open_files, this, tm));
@@ -443,7 +443,7 @@ int ImageFile::init_image_file() {
443443
ImageConfigNS::UpperConfig upper;
444444
bool record_no_download = false;
445445
bool has_error = false;
446-
auto lowers = conf.lowers();
446+
auto lowers = conf.lowers(); // layer0 ... layerN-1
447447
auto concurrency = image_service.global_conf.prefetchConfig().concurrency();
448448

449449
if (conf.accelerationLayer() && !conf.recordTracePath().empty()) {
@@ -554,3 +554,51 @@ void ImageFile::set_failed(const Ts &...xs) {
554554
m_exception = estring().appends(xs...);
555555
}
556556
}
557+
558+
int ImageFile::create_snapshot(const char *config_path) {
559+
// load new config file to get the snapshot layer path
560+
// open new upper layer
561+
// restack() current RW layer as snapshot layer
562+
if(!m_lower_file || !m_upper_file)
563+
LOG_ERROR_RETURN(0, -1, "Lower or upper layer is NULL.");
564+
565+
ImageConfigNS::ImageConfig new_cfg;
566+
LSMT::IFileRW *upper_file = nullptr;
567+
568+
LOG_INFO("Load new config `.", config_path);
569+
if (!new_cfg.ParseJSON(config_path)) {
570+
LOG_ERROR_RETURN(0, -1, "Error parse new config json: `.", config_path);
571+
}
572+
573+
auto upper = new_cfg.upper();
574+
auto lowers = new_cfg.lowers();
575+
// if(lowers[lowers.size()-1].file() != conf.upper().data())
576+
// 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());
577+
if(upper.index() == conf.upper().index() || upper.data() == conf.upper().data())
578+
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());
579+
580+
upper_file = open_upper(upper);
581+
if (!upper_file)
582+
LOG_ERROR_RETURN(0, -1, "Open upper layer failed.");
583+
584+
if(((LSMT::IFileRW *)m_file)->restack(upper_file) != 0)
585+
LOG_ERRNO_RETURN(0, -1, "Restack new rwlayer failed.");
586+
587+
if(m_upper_file) {
588+
// transfer the sealed layer from m_upper_file to m_lower_file before m_upper_file is destructed
589+
auto sealed = ((LSMT::IFileRW *)m_upper_file)->get_file(0);
590+
((LSMT::IFileRO *)m_lower_file)->insert_file(sealed);
591+
((LSMT::IFileRW *)m_upper_file)->clear_files();
592+
delete m_upper_file;
593+
}
594+
// set m_lower_file->m_index = m_file->m_index->m_backing_index because m_file is not responsible for the destruction of m_backing_index
595+
auto combo_index = (LSMT::IComboIndex *)((LSMT::IFileRW *)m_file)->index(); // m_file->m_index
596+
((LSMT::IFileRO *)m_lower_file)->index(combo_index->backing_index());
597+
// set m_file->m_index->m_index0 = upper_file->m_index
598+
auto upper_file_index = (LSMT::IMemoryIndex0 *)((LSMT::IFileRW *)upper_file)->index(); // upper_file->m_index
599+
combo_index->front_index(upper_file_index);
600+
601+
m_upper_file = upper_file;
602+
603+
return 0;
604+
}

src/image_file.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,15 @@ static std::string SEALED_FILE_NAME = "overlaybd.sealed";
4141

4242
class ImageFile : public photon::fs::ForwardFile {
4343
public:
44-
ImageFile(ImageConfigNS::ImageConfig &_conf, ImageService &is)
44+
ImageFile(ImageConfigNS::ImageConfig &_conf, ImageService &is, const std::string &dev_id)
4545
: ForwardFile(nullptr), image_service(is), m_lower_file(nullptr) {
4646
conf.CopyFrom(_conf, conf.GetAllocator());
4747
m_exception = "";
48+
if(image_service.register_image_file(dev_id, this) != 0) { // register itself
49+
set_failed("duplicated dev id: " + dev_id);
50+
return;
51+
}
52+
m_dev_id = dev_id;
4853
m_status = init_image_file();
4954
if (m_status == 1) {
5055
struct stat st;
@@ -55,6 +60,7 @@ class ImageFile : public photon::fs::ForwardFile {
5560

5661
~ImageFile() {
5762
m_status = -1;
63+
image_service.unregister_image_file(m_dev_id); // unregister itself
5864
if (dl_thread_jh != nullptr)
5965
photon::thread_join(dl_thread_jh);
6066
delete m_prefetcher;
@@ -113,6 +119,8 @@ class ImageFile : public photon::fs::ForwardFile {
113119

114120
int compact(IFile *as);
115121

122+
int create_snapshot(const char *config_path);
123+
116124
private:
117125
Prefetcher *m_prefetcher = nullptr;
118126
ImageConfigNS::ImageConfig conf;
@@ -121,6 +129,7 @@ class ImageFile : public photon::fs::ForwardFile {
121129
ImageService &image_service;
122130
photon::fs::IFile *m_lower_file = nullptr;
123131
photon::fs::IFile *m_upper_file = nullptr;
132+
std::string m_dev_id = "";
124133

125134
int init_image_file();
126135
template<typename...Ts> void set_failed(const Ts&...xs);

0 commit comments

Comments
 (0)