Implement vitastor-cli serve command to serve simple HTTP API
This commit is contained in:
@@ -27,6 +27,7 @@ add_library(vitastor_cli STATIC
|
||||
cli_pool_ls.cpp
|
||||
cli_pool_modify.cpp
|
||||
cli_pool_rm.cpp
|
||||
cli_serve.cpp
|
||||
)
|
||||
target_compile_options(vitastor_cli PUBLIC -fPIC)
|
||||
|
||||
|
||||
+50
-30
@@ -225,6 +225,14 @@ static const char* help_text =
|
||||
" -r|--reverse Sort in descending order\n"
|
||||
" -n|--count N Only list first N items\n"
|
||||
"\n"
|
||||
"vitastor-cli serve\n"
|
||||
" Start HTTP server able to handle CLI commands over a REST API. Options:\n"
|
||||
" --bind_address ADDR Specify server IP address or addresses, separated by space. Default is 127.0.0.1.\n"
|
||||
" --port 8080 Specify server port.\n"
|
||||
" --ssl_cert FILE Path to server SSL certificate file (PEM format).\n"
|
||||
" --ssl_key FILE Path to server SSL private key file.\n"
|
||||
" --ssl_ca FILE Path to file with SSL CA certificates used to validate client connections.\n"
|
||||
"\n"
|
||||
"Use vitastor-cli --help <command> for command details or vitastor-cli --help --all for all details.\n"
|
||||
"\n"
|
||||
"GLOBAL OPTIONS:\n"
|
||||
@@ -319,27 +327,24 @@ static json11::Json::object parse_args(int narg, const char *args[])
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
std::function<bool(cli_result_t &)> cli_tool_t::start(json11::Json::object cfg, cli_result_t & result)
|
||||
{
|
||||
cli_result_t result = {};
|
||||
p->is_command_line = true;
|
||||
p->parse_config(cfg);
|
||||
json11::Json::array cmd = cfg["command"].array_items();
|
||||
cfg.erase("command");
|
||||
std::function<bool(cli_result_t &)> action_cb;
|
||||
if (!cmd.size())
|
||||
{
|
||||
result = { .err = EINVAL, .text = "command is missing" };
|
||||
result = { .err = EOPNOTSUPP, .text = "command is missing" };
|
||||
}
|
||||
else if (cmd[0] == "status")
|
||||
{
|
||||
// Show cluster status
|
||||
action_cb = p->start_status(cfg);
|
||||
action_cb = start_status(cfg);
|
||||
}
|
||||
else if (cmd[0] == "df")
|
||||
{
|
||||
// Show pool space stats
|
||||
action_cb = p->start_pool_ls(cfg);
|
||||
action_cb = start_pool_ls(cfg);
|
||||
}
|
||||
else if (cmd[0] == "ls")
|
||||
{
|
||||
@@ -349,7 +354,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
cmd.erase(cmd.begin(), cmd.begin()+1);
|
||||
cfg["names"] = cmd;
|
||||
}
|
||||
action_cb = p->start_ls(cfg);
|
||||
action_cb = start_ls(cfg);
|
||||
}
|
||||
else if (cmd[0] == "snap-create")
|
||||
{
|
||||
@@ -364,7 +369,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cfg["image"] = name.substr(0, pos);
|
||||
cfg["snapshot"] = name.substr(pos + 1);
|
||||
action_cb = p->start_create(cfg);
|
||||
action_cb = start_create(cfg);
|
||||
}
|
||||
}
|
||||
else if (cmd[0] == "create")
|
||||
@@ -374,7 +379,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cfg["image"] = cmd[1];
|
||||
}
|
||||
action_cb = p->start_create(cfg);
|
||||
action_cb = start_create(cfg);
|
||||
}
|
||||
else if (cmd[0] == "modify")
|
||||
{
|
||||
@@ -383,12 +388,12 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cfg["image"] = cmd[1];
|
||||
}
|
||||
action_cb = p->start_modify(cfg);
|
||||
action_cb = start_modify(cfg);
|
||||
}
|
||||
else if (cmd[0] == "rm-data")
|
||||
{
|
||||
// Delete inode data
|
||||
action_cb = p->start_rm_data(cfg);
|
||||
action_cb = start_rm_data(cfg);
|
||||
}
|
||||
else if (cmd[0] == "rm-osd")
|
||||
{
|
||||
@@ -398,7 +403,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
cmd.erase(cmd.begin(), cmd.begin()+1);
|
||||
cfg["osd_id"] = cmd;
|
||||
}
|
||||
action_cb = p->start_rm_osd(cfg);
|
||||
action_cb = start_rm_osd(cfg);
|
||||
}
|
||||
else if (cmd[0] == "merge-data")
|
||||
{
|
||||
@@ -409,7 +414,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
if (cmd.size() > 2)
|
||||
cfg["to"] = cmd[2];
|
||||
}
|
||||
action_cb = p->start_merge(cfg);
|
||||
action_cb = start_merge(cfg);
|
||||
}
|
||||
else if (cmd[0] == "flatten")
|
||||
{
|
||||
@@ -418,7 +423,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cfg["image"] = cmd[1];
|
||||
}
|
||||
action_cb = p->start_flatten(cfg);
|
||||
action_cb = start_flatten(cfg);
|
||||
}
|
||||
else if (cmd[0] == "dd")
|
||||
{
|
||||
@@ -432,7 +437,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
cfg[arg.substr(0, p)] = arg.substr(p+1);
|
||||
}
|
||||
}
|
||||
action_cb = p->start_dd(cfg);
|
||||
action_cb = start_dd(cfg);
|
||||
}
|
||||
else if (cmd[0] == "rm")
|
||||
{
|
||||
@@ -441,7 +446,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cmd.erase(cmd.begin(), cmd.begin()+1);
|
||||
cfg["globs"] = cmd;
|
||||
action_cb = p->start_rm_wildcard(cfg);
|
||||
action_cb = start_rm_wildcard(cfg);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -451,41 +456,41 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
if (cmd.size() > 2)
|
||||
cfg["to"] = cmd[2];
|
||||
}
|
||||
action_cb = p->start_rm(cfg);
|
||||
action_cb = start_rm(cfg);
|
||||
}
|
||||
}
|
||||
else if (cmd[0] == "describe")
|
||||
{
|
||||
// Describe unclean objects
|
||||
action_cb = p->start_describe(cfg);
|
||||
action_cb = start_describe(cfg);
|
||||
}
|
||||
else if (cmd[0] == "fix")
|
||||
{
|
||||
// Fix inconsistent objects (by deleting some copies)
|
||||
action_cb = p->start_fix(cfg);
|
||||
action_cb = start_fix(cfg);
|
||||
}
|
||||
else if (cmd[0] == "alloc-osd")
|
||||
{
|
||||
// Allocate a new OSD number
|
||||
action_cb = p->start_alloc_osd(cfg);
|
||||
action_cb = start_alloc_osd(cfg);
|
||||
}
|
||||
else if (cmd[0] == "osd-tree")
|
||||
{
|
||||
// Print OSD tree
|
||||
action_cb = p->start_osd_tree(cfg);
|
||||
action_cb = start_osd_tree(cfg);
|
||||
}
|
||||
else if (cmd[0] == "osds" || cmd[0] == "ls-osds" || cmd[0] == "ls-osd" || cmd[0] == "osd-ls")
|
||||
{
|
||||
// Print OSD list
|
||||
cfg["flat"] = true;
|
||||
action_cb = p->start_osd_tree(cfg);
|
||||
action_cb = start_osd_tree(cfg);
|
||||
}
|
||||
else if (cmd[0] == "modify-osd")
|
||||
{
|
||||
// Modify OSD configuration
|
||||
if (cmd.size() > 1)
|
||||
cfg["osd_num"] = cmd[1];
|
||||
action_cb = p->start_modify_osd(cfg);
|
||||
action_cb = start_modify_osd(cfg);
|
||||
}
|
||||
else if (cmd[0] == "pg-list" || cmd[0] == "pg-ls" || cmd[0] == "list-pg" || cmd[0] == "ls-pg" || cmd[0] == "ls-pgs" || cmd[0] == "pgs")
|
||||
{
|
||||
@@ -495,7 +500,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
cmd.erase(cmd.begin(), cmd.begin()+1);
|
||||
cfg["pg_state"] = cmd;
|
||||
}
|
||||
action_cb = p->start_pg_list(cfg);
|
||||
action_cb = start_pg_list(cfg);
|
||||
}
|
||||
else if (cmd[0] == "create-pool" || cmd[0] == "pool-create")
|
||||
{
|
||||
@@ -504,7 +509,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cfg["name"] = cmd[1];
|
||||
}
|
||||
action_cb = p->start_pool_create(cfg);
|
||||
action_cb = start_pool_create(cfg);
|
||||
}
|
||||
else if (cmd[0] == "modify-pool" || cmd[0] == "pool-modify")
|
||||
{
|
||||
@@ -513,7 +518,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cfg["old_name"] = cmd[1];
|
||||
}
|
||||
action_cb = p->start_pool_modify(cfg);
|
||||
action_cb = start_pool_modify(cfg);
|
||||
}
|
||||
else if (cmd[0] == "rm-pool" || cmd[0] == "pool-rm")
|
||||
{
|
||||
@@ -522,7 +527,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cfg["pool"] = cmd[1];
|
||||
}
|
||||
action_cb = p->start_pool_rm(cfg);
|
||||
action_cb = start_pool_rm(cfg);
|
||||
}
|
||||
else if (cmd[0] == "ls-pool" || cmd[0] == "pool-ls" || cmd[0] == "ls-pools" || cmd[0] == "pools")
|
||||
{
|
||||
@@ -533,12 +538,26 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
cmd.erase(cmd.begin(), cmd.begin()+1);
|
||||
cfg["names"] = cmd;
|
||||
}
|
||||
action_cb = p->start_pool_ls(cfg);
|
||||
action_cb = start_pool_ls(cfg);
|
||||
}
|
||||
else if (cmd[0] == "serve")
|
||||
{
|
||||
// Start HTTP server
|
||||
action_cb = start_serve(cfg);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = { .err = EINVAL, .text = "unknown command: "+cmd[0].string_value() };
|
||||
result = { .err = EOPNOTSUPP, .text = "unknown command: "+cmd[0].string_value() };
|
||||
}
|
||||
return action_cb;
|
||||
}
|
||||
|
||||
static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
cli_result_t result = {};
|
||||
p->is_command_line = true;
|
||||
p->parse_config(cfg);
|
||||
auto action_cb = p->start(cfg, result);
|
||||
if (action_cb != NULL)
|
||||
{
|
||||
// Create client
|
||||
@@ -550,6 +569,7 @@ static int run(cli_tool_t *p, json11::Json::object cfg)
|
||||
{
|
||||
result = r;
|
||||
action_cb = NULL;
|
||||
p->ringloop->submit();
|
||||
});
|
||||
// Loop until it completes
|
||||
while (action_cb != NULL)
|
||||
|
||||
+4
-1
@@ -46,6 +46,7 @@ public:
|
||||
json11::Json etcd_result;
|
||||
|
||||
void parse_config(json11::Json::object & cfg);
|
||||
void parse_api_opts(json11::Json::object & cfg);
|
||||
json11::Json parse_tags(std::string tags);
|
||||
|
||||
void change_parent(inode_t cur, inode_t new_parent, cli_result_t *result);
|
||||
@@ -56,8 +57,10 @@ public:
|
||||
friend struct snap_flattener_t;
|
||||
friend struct snap_remover_t;
|
||||
|
||||
std::function<bool(cli_result_t &)> start(json11::Json::object cfg, cli_result_t & result);
|
||||
std::function<bool(cli_result_t &)> start_alloc_osd(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_create(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_dd(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_describe(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_fix(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_flatten(json11::Json);
|
||||
@@ -75,8 +78,8 @@ public:
|
||||
std::function<bool(cli_result_t &)> start_rm_data(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_rm_osd(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_rm_wildcard(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_serve(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_status(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_dd(json11::Json);
|
||||
|
||||
// Should be called like loop_and_wait(start_status(), <completion callback>)
|
||||
void loop_and_wait(std::function<bool(cli_result_t &)> loop_cb, std::function<void(const cli_result_t &)> complete_cb);
|
||||
|
||||
+11
-7
@@ -101,6 +101,16 @@ inode_config_t* cli_tool_t::get_inode_cfg(const std::string & name)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void cli_tool_t::parse_api_opts(json11::Json::object & cfg)
|
||||
{
|
||||
iodepth = cfg["iodepth"].uint64_value();
|
||||
if (!iodepth)
|
||||
iodepth = 32;
|
||||
parallel_osds = cfg["parallel_osds"].uint64_value();
|
||||
if (!parallel_osds)
|
||||
parallel_osds = 4;
|
||||
}
|
||||
|
||||
void cli_tool_t::parse_config(json11::Json::object & cfg)
|
||||
{
|
||||
for (auto kv_it = cfg.begin(); kv_it != cfg.end();)
|
||||
@@ -121,15 +131,10 @@ void cli_tool_t::parse_config(json11::Json::object & cfg)
|
||||
else
|
||||
color = isatty(1);
|
||||
json_output = cfg["json"].bool_value();
|
||||
iodepth = cfg["iodepth"].uint64_value();
|
||||
if (!iodepth)
|
||||
iodepth = 32;
|
||||
parallel_osds = cfg["parallel_osds"].uint64_value();
|
||||
if (!parallel_osds)
|
||||
parallel_osds = 4;
|
||||
log_level = cfg["log_level"].int64_value();
|
||||
progress = cfg["progress"].uint64_value() ? true : false;
|
||||
list_first = cfg["wait_list"].uint64_value() ? true : false;
|
||||
parse_api_opts(cfg);
|
||||
}
|
||||
|
||||
struct cli_result_looper_t
|
||||
@@ -153,7 +158,6 @@ void cli_tool_t::loop_and_wait(std::function<bool(cli_result_t &)> loop_cb, std:
|
||||
ringloop->unregister_consumer(&looper->consumer);
|
||||
looper->loop_cb = NULL;
|
||||
looper->complete_cb(looper->result);
|
||||
ringloop->submit();
|
||||
delete looper;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
// Copyright (c) Vitaliy Filippov, 2019+
|
||||
// License: VNPL-1.1 (see README.md for details)
|
||||
|
||||
#include <signal.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <stdexcept>
|
||||
#include "cli.h"
|
||||
#include "cluster_client.h"
|
||||
#include "epoll_manager.h"
|
||||
#include "http_client.h"
|
||||
#include "str_util.h"
|
||||
#include "json_util.h"
|
||||
#include "addr_util.h"
|
||||
|
||||
struct cli_serve_conn_t
|
||||
{
|
||||
int peer_fd = 0;
|
||||
std::string peer_addr;
|
||||
http_co_t *co = NULL;
|
||||
cli_tool_t *p = NULL;
|
||||
cli_result_t result;
|
||||
bool keepalive = false;
|
||||
bool closed = false;
|
||||
timespec request_time;
|
||||
std::string request_method;
|
||||
std::string request_path;
|
||||
std::string request_body;
|
||||
std::function<bool(cli_result_t &)> action_cb;
|
||||
};
|
||||
|
||||
struct cli_serve_path_t
|
||||
{
|
||||
std::string cmd;
|
||||
bool allow_get;
|
||||
};
|
||||
|
||||
// Serve vitastor-cli commands over HTTP in JSON format
|
||||
struct cli_serve_t
|
||||
{
|
||||
std::map<std::string, cli_serve_path_t> cmd_paths = {
|
||||
{"data/delete", {"rm-data", false}},
|
||||
{"data/describe", {"describe", true}},
|
||||
{"data/fix", {"fix", false}},
|
||||
{"data/merge", {"merge-data", false}},
|
||||
{"image/create", {"create", false}},
|
||||
{"image/delete", {"rm", false}},
|
||||
{"image/flatten", {"flatten", false}},
|
||||
{"image/list", {"ls", true}},
|
||||
{"image/modify", {"modify", false}},
|
||||
{"osd/alloc", {"alloc-osd", false}},
|
||||
{"osd/delete", {"rm-osd", false}},
|
||||
{"osd/list", {"ls-osd", true}},
|
||||
{"osd/modify", {"modify-osd", false}},
|
||||
{"pg/list", {"ls-pgs", true}},
|
||||
{"pool/create", {"create-pool", false}},
|
||||
{"pool/delete", {"rm-pool", false}},
|
||||
{"pool/list", {"pools", true}},
|
||||
{"pool/modify", {"modify-pool", false}},
|
||||
{"status", {"status", true}},
|
||||
};
|
||||
|
||||
cli_tool_t *parent = NULL;
|
||||
json11::Json options;
|
||||
cli_result_t result;
|
||||
|
||||
bool log_body = false;
|
||||
bool stop = false;
|
||||
std::vector<std::string> bind_addresses;
|
||||
int port = 0;
|
||||
int listen_backlog = 0;
|
||||
bool ssl = false;
|
||||
std::vector<int> listen_fds;
|
||||
http_context_t *http_ctx = NULL;
|
||||
std::set<cli_serve_conn_t*> connections;
|
||||
|
||||
int state = 0;
|
||||
|
||||
bool is_done()
|
||||
{
|
||||
return state == 100;
|
||||
}
|
||||
|
||||
void loop()
|
||||
{
|
||||
if (state == 1)
|
||||
goto resume_1;
|
||||
else if (state == 2)
|
||||
goto resume_2;
|
||||
else if (state == 100)
|
||||
return;
|
||||
if (options["bind_address"].is_string())
|
||||
bind_addresses = explode(" ", options["bind_address"].string_value(), true);
|
||||
else
|
||||
bind_addresses.push_back("127.0.0.1");
|
||||
port = options["port"].uint64_value();
|
||||
if (!port)
|
||||
port = 8080;
|
||||
else if (port < 0 || port > 65535)
|
||||
{
|
||||
result = (cli_result_t){ .err = EINVAL, .text = "HTTP port can't be larger than 65536" };
|
||||
state = 100;
|
||||
return;
|
||||
}
|
||||
listen_backlog = options["listen_backlog"].uint64_value();
|
||||
if (!listen_backlog)
|
||||
listen_backlog = 128;
|
||||
ssl = json_is_true(options["ssl"]);
|
||||
if (ssl)
|
||||
{
|
||||
std::string ssl_cert = options["ssl_cert"].string_value();
|
||||
std::string ssl_key = options["ssl_key"].string_value();
|
||||
std::string ssl_ca = options["ssl_ca"].string_value();
|
||||
std::string error;
|
||||
http_ctx = http_context_init(ssl_cert, ssl_key, ssl_ca, ssl_ca != "", error);
|
||||
if (error != "")
|
||||
{
|
||||
result = (cli_result_t){ .err = EINVAL, .text = error };
|
||||
state = 100;
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (auto & bind_address: bind_addresses)
|
||||
{
|
||||
int listen_fd = create_and_bind_socket(bind_address, port, listen_backlog, NULL);
|
||||
fcntl(listen_fd, F_SETFL, fcntl(listen_fd, F_GETFL, 0) | O_NONBLOCK);
|
||||
parent->epmgr->set_fd_handler(listen_fd, false, [this](int fd, int events)
|
||||
{
|
||||
accept_connections(fd);
|
||||
});
|
||||
listen_fds.push_back(listen_fd);
|
||||
}
|
||||
resume_1:
|
||||
if (!stop)
|
||||
{
|
||||
state = 1;
|
||||
return;
|
||||
}
|
||||
for (auto conn: connections)
|
||||
{
|
||||
http_close(conn->co);
|
||||
}
|
||||
resume_2:
|
||||
// Wait for all connections to finish
|
||||
if (connections.size() > 0)
|
||||
{
|
||||
state = 2;
|
||||
return;
|
||||
}
|
||||
if (http_ctx)
|
||||
{
|
||||
http_context_destroy(http_ctx);
|
||||
http_ctx = NULL;
|
||||
}
|
||||
state = 100;
|
||||
}
|
||||
|
||||
void accept_connections(int listen_fd)
|
||||
{
|
||||
sockaddr_storage addr;
|
||||
socklen_t peer_addr_size = sizeof(addr);
|
||||
int peer_fd;
|
||||
while ((peer_fd = accept(listen_fd, (sockaddr*)&addr, &peer_addr_size)) >= 0)
|
||||
{
|
||||
auto peer_addr_str = addr_to_string(addr);
|
||||
assert(peer_fd != 0);
|
||||
timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
printf("[%s.%03ju] New connection %d from %s\n", format_datetime(ts.tv_sec).c_str(), (uint64_t)ts.tv_nsec/1000000,
|
||||
peer_fd, peer_addr_str.c_str());
|
||||
fcntl(peer_fd, F_SETFL, fcntl(peer_fd, F_GETFL, 0) | O_NONBLOCK);
|
||||
int one = 1;
|
||||
setsockopt(peer_fd, SOL_TCP, TCP_NODELAY, &one, sizeof(one));
|
||||
cli_serve_conn_t *conn = new cli_serve_conn_t;
|
||||
conn->peer_fd = peer_fd;
|
||||
conn->peer_addr = peer_addr_str;
|
||||
conn->co = http_init(parent->epmgr->tfd, http_ctx);
|
||||
http_serve(conn->co, peer_fd, (http_options_t){ .ssl = ssl }, [this, conn](http_message_t *msg)
|
||||
{
|
||||
process_request(conn, msg);
|
||||
});
|
||||
connections.insert(conn);
|
||||
// Try to accept next connection
|
||||
peer_addr_size = sizeof(addr);
|
||||
}
|
||||
if (peer_fd == -1 && errno != EAGAIN)
|
||||
{
|
||||
throw std::runtime_error(std::string("accept: ") + strerror(errno));
|
||||
}
|
||||
}
|
||||
|
||||
int map_to_http(int err, std::string *text)
|
||||
{
|
||||
int code = 0;
|
||||
if (err == EINVAL)
|
||||
{
|
||||
code = 400;
|
||||
if (text)
|
||||
*text = "Bad Request";
|
||||
}
|
||||
else if (err == EOPNOTSUPP)
|
||||
{
|
||||
code = 404;
|
||||
if (text)
|
||||
*text = "Not Found";
|
||||
}
|
||||
else if (err == ENOSYS)
|
||||
{
|
||||
code = 405;
|
||||
if (text)
|
||||
*text = "Method Not Allowed";
|
||||
}
|
||||
else if (err == EAGAIN)
|
||||
{
|
||||
code = 409;
|
||||
if (text)
|
||||
*text = "Update Conflict";
|
||||
}
|
||||
else if (err == ENOTEMPTY || err == EEXIST || err == ENOENT || err == EBUSY)
|
||||
{
|
||||
code = 412;
|
||||
if (text)
|
||||
*text = "Precondition Failed";
|
||||
}
|
||||
else /*if (err == EIO || err == EBADF)*/
|
||||
{
|
||||
code = 500;
|
||||
if (text)
|
||||
*text = "Internal Server Error";
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
std::string cli_http_response(cli_serve_conn_t *conn)
|
||||
{
|
||||
timespec now;
|
||||
clock_gettime(CLOCK_REALTIME, &now);
|
||||
int code = 200;
|
||||
std::string response;
|
||||
if (conn->result.err)
|
||||
{
|
||||
std::string status_line;
|
||||
code = map_to_http(conn->result.err, &status_line);
|
||||
response = "HTTP/1.1 "+std::to_string(code)+" "+status_line+"\r\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
response = "HTTP/1.1 200 OK\r\n";
|
||||
}
|
||||
response += (conn->keepalive
|
||||
? "Connection: keep-alive\r\n"
|
||||
: "Connection: close\r\n");
|
||||
std::string body;
|
||||
if (!conn->result.data.is_null())
|
||||
{
|
||||
response += "Content-Type: application/json\r\n";
|
||||
body = conn->result.data.dump();
|
||||
}
|
||||
else
|
||||
{
|
||||
response += "Content-Type: text/plain; charset=utf-8\r\n";
|
||||
body = conn->result.text;
|
||||
}
|
||||
response += "Content-Length: "+std::to_string(body.size())+"\r\n\r\n";
|
||||
response += body;
|
||||
if (conn->request_method.find("\n") != std::string::npos)
|
||||
conn->request_method = str_replace(conn->request_method, "\n", "%0a");
|
||||
if (conn->request_method.find(" ") != std::string::npos)
|
||||
conn->request_method = str_replace(conn->request_method, " ", "%20");
|
||||
if (conn->request_path.find("\n") != std::string::npos)
|
||||
conn->request_path = str_replace(conn->request_path, "\n", "%0a");
|
||||
if (conn->request_path.find(" ") != std::string::npos)
|
||||
conn->request_path = str_replace(conn->request_path, " ", "%20");
|
||||
uint64_t response_time = (now.tv_sec-conn->request_time.tv_sec)*1000 + (now.tv_nsec-conn->request_time.tv_nsec)/1000000;
|
||||
printf("[%s.%03ju] %s %s %s %d %.03f sec\n",
|
||||
format_datetime(now.tv_sec).c_str(), (uint64_t)now.tv_nsec/1000000,
|
||||
conn->peer_addr.c_str(), conn->request_method.c_str(), conn->request_path.c_str(), code,
|
||||
response_time/1000.0);
|
||||
if (log_body)
|
||||
{
|
||||
if (conn->request_body.find("\n") != std::string::npos)
|
||||
conn->request_body = str_replace(conn->request_body, "\n", " ");
|
||||
if (conn->request_body.size())
|
||||
printf(" %s\n", conn->request_body.c_str());
|
||||
printf(" %s\n", body.c_str());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
void process_request(cli_serve_conn_t *conn, http_message_t *msg)
|
||||
{
|
||||
timespec ts;
|
||||
clock_gettime(CLOCK_REALTIME, &ts);
|
||||
if (!msg->error.empty())
|
||||
{
|
||||
// connection is closed
|
||||
fprintf(stderr, "[%s.%03ju] Connection %d closed: %s\n", format_datetime(ts.tv_sec).c_str(),
|
||||
(uint64_t)ts.tv_nsec/1000000, conn->peer_fd, msg->error.c_str());
|
||||
if (conn->p)
|
||||
conn->closed = true;
|
||||
else
|
||||
{
|
||||
connections.erase(conn);
|
||||
http_destroy(conn->co);
|
||||
delete conn;
|
||||
}
|
||||
return;
|
||||
}
|
||||
conn->keepalive = msg->headers.find("connection") != msg->headers.end() &&
|
||||
msg->headers.at("connection") == "keep-alive";
|
||||
conn->p = new cli_tool_t;
|
||||
conn->p->iodepth = parent->iodepth;
|
||||
conn->p->parallel_osds = parent->parallel_osds;
|
||||
conn->p->json_output = true;
|
||||
conn->p->ringloop = parent->ringloop;
|
||||
conn->p->epmgr = parent->epmgr;
|
||||
conn->p->cli = parent->cli;
|
||||
conn->p->is_command_line = false;
|
||||
// Parse request
|
||||
auto req_line = explode(" ", msg->status_line, true);
|
||||
if (req_line.size() < 2)
|
||||
{
|
||||
if (req_line[0] == "")
|
||||
req_line[0] = "-";
|
||||
req_line.push_back("-");
|
||||
}
|
||||
conn->request_time = ts;
|
||||
conn->request_method = std::move(req_line[0]);
|
||||
conn->request_path = std::move(req_line[1]);
|
||||
conn->request_body = std::move(msg->body);
|
||||
auto ctype = msg->headers["content-type"];
|
||||
if (conn->request_method != "GET" && conn->request_method != "POST")
|
||||
{
|
||||
conn->result = { .err = ENOSYS, .text = "Unsupported request method "+conn->request_method };
|
||||
}
|
||||
else if (ctype != (conn->request_method == "GET" ? "" : "application/json"))
|
||||
{
|
||||
conn->result = { .err = EINVAL, .text = "Unsupported Content-Type: "+ctype+" for "+conn->request_method+" requests" };
|
||||
}
|
||||
else
|
||||
{
|
||||
auto uri = explode("?", conn->request_path, true);
|
||||
uri[0] = trim(uri[0], "/");
|
||||
auto cmd_it = cmd_paths.find(uri[0]);
|
||||
if (uri[0] == "")
|
||||
{
|
||||
std::string text = "Supported APIs:\n\n";
|
||||
for (auto & pp: cmd_paths)
|
||||
{
|
||||
text += (pp.second.allow_get ? "- GET" : "- POST") + (" /" + pp.first) + "\n";
|
||||
}
|
||||
conn->result = { .text = text };
|
||||
}
|
||||
else if (cmd_it == cmd_paths.end())
|
||||
{
|
||||
conn->result = { .err = EOPNOTSUPP, .text = "unknown command: "+uri[0] };
|
||||
}
|
||||
else if (conn->request_method == "GET" && !cmd_it->second.allow_get)
|
||||
{
|
||||
conn->result = { .err = ENOSYS, .text = "method /"+uri[0]+" only allows POST requests" };
|
||||
}
|
||||
else
|
||||
{
|
||||
std::string error;
|
||||
json11::Json::object cfg;
|
||||
if (conn->request_method == "POST")
|
||||
{
|
||||
cfg = json11::Json::parse(conn->request_body, error).object_items();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Parse URI
|
||||
cfg = parse_uri_params(uri[1]);
|
||||
}
|
||||
if (error != "")
|
||||
{
|
||||
conn->result = { .err = EINVAL, .text = "Invalid JSON in body: "+error };
|
||||
}
|
||||
else
|
||||
{
|
||||
cfg["command"] = json11::Json::array{cmd_it->second.cmd};
|
||||
conn->p->parse_api_opts(cfg);
|
||||
conn->action_cb = conn->p->start(cfg, conn->result);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!conn->action_cb)
|
||||
{
|
||||
http_reply(conn->co, cli_http_response(conn));
|
||||
delete conn->p;
|
||||
conn->p = NULL;
|
||||
return;
|
||||
}
|
||||
conn->p->loop_and_wait(conn->action_cb, [this, conn](const cli_result_t & r)
|
||||
{
|
||||
conn->result = r;
|
||||
conn->action_cb = NULL;
|
||||
delete conn->p;
|
||||
conn->p = NULL;
|
||||
if (!conn->closed)
|
||||
http_reply(conn->co, cli_http_response(conn));
|
||||
else
|
||||
{
|
||||
connections.erase(conn);
|
||||
http_destroy(conn->co);
|
||||
delete conn;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
std::function<bool(cli_result_t &)> cli_tool_t::start_serve(json11::Json cfg)
|
||||
{
|
||||
auto server = new cli_serve_t();
|
||||
server->parent = this;
|
||||
server->options = cfg;
|
||||
return [server](cli_result_t & result)
|
||||
{
|
||||
server->loop();
|
||||
if (server->is_done())
|
||||
{
|
||||
result = server->result;
|
||||
delete server;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) Vitaliy Filippov, 2019+
|
||||
// License: VNPL-1.1 or GNU GPL-2.0+ (see README.md for details)
|
||||
|
||||
#include "str_util.h"
|
||||
#include "json_util.h"
|
||||
|
||||
std::map<std::string, std::string> json_to_string_map(const json11::Json::object & config)
|
||||
@@ -49,3 +50,36 @@ std::string implode(const std::string & sep, json11::Json array)
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static void json_array_add(json11::Json & to, json11::Json item)
|
||||
{
|
||||
if (to.is_null())
|
||||
to = item;
|
||||
else if (to.is_array())
|
||||
{
|
||||
auto a = to.array_items();
|
||||
a.push_back(item);
|
||||
to = a;
|
||||
}
|
||||
else
|
||||
{
|
||||
json11::Json::array a = {to};
|
||||
a.push_back(item);
|
||||
to = a;
|
||||
}
|
||||
}
|
||||
|
||||
json11::Json::object parse_uri_params(const std::string & params)
|
||||
{
|
||||
json11::Json::object obj;
|
||||
auto list = explode("&", params, true);
|
||||
for (auto & param: list)
|
||||
{
|
||||
auto pos = param.find("=");
|
||||
if (pos != std::string::npos)
|
||||
json_array_add(obj[urldecode(param.substr(0, pos))], urldecode(param.substr(pos+1)));
|
||||
else
|
||||
json_array_add(obj[urldecode(param)], true);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@ std::map<std::string, std::string> json_to_string_map(const json11::Json::object
|
||||
bool json_is_true(const json11::Json & val);
|
||||
bool json_is_false(const json11::Json & val);
|
||||
std::string implode(const std::string & sep, json11::Json array);
|
||||
json11::Json::object parse_uri_params(const std::string & params);
|
||||
|
||||
#pragma GCC visibility pop
|
||||
|
||||
@@ -503,3 +503,34 @@ bool is_zero(void *buf, size_t size)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
char hextochar(char h)
|
||||
{
|
||||
if (h >= 'a' && h <= 'f')
|
||||
return h-'a'+10;
|
||||
else if (h >= 'A' && h <= 'F')
|
||||
return h-'A'+10;
|
||||
else if (h >= '0' && h <= '9')
|
||||
return h-'0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string urldecode(const std::string & orig)
|
||||
{
|
||||
size_t len = orig.size();
|
||||
std::string res;
|
||||
res.reserve(len);
|
||||
for (size_t i = 0; i < len; i++)
|
||||
{
|
||||
if (orig[i] == '+')
|
||||
res.push_back(' ');
|
||||
else if (orig[i] == '%' && i < len-2)
|
||||
{
|
||||
res.push_back(hextochar(orig[i+1])*16 + hextochar(orig[i+2]));
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
res.push_back(orig[i]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -34,5 +34,6 @@ std::string addslashes(const std::string & str, const char *toescape = "\\\"");
|
||||
std::string realpath_str(std::string path, bool nofail = true);
|
||||
std::string format_datetime(uint64_t unixtime);
|
||||
bool is_zero(void *buf, size_t size);
|
||||
std::string urldecode(const std::string & orig);
|
||||
|
||||
#pragma GCC visibility pop
|
||||
|
||||
Reference in New Issue
Block a user