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/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 ) 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(););