diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2879990b..4ccc6a53 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -153,7 +153,7 @@ target_link_libraries(vitastor-nbd # vitastor-cli add_executable(vitastor-cli - cli.cpp cli_alloc_osd.cpp cli_simple_offsets.cpp + cli.cpp cli_alloc_osd.cpp cli_simple_offsets.cpp cli_df.cpp cli_ls.cpp cli_create.cpp cli_modify.cpp cli_flatten.cpp cli_merge.cpp cli_rm.cpp cli_snap_rm.cpp ) target_link_libraries(vitastor-cli diff --git a/src/cli.cpp b/src/cli.cpp index 70bba641..1e3688ca 100644 --- a/src/cli.cpp +++ b/src/cli.cpp @@ -86,6 +86,9 @@ void cli_tool_t::help() "(c) Vitaliy Filippov, 2019+ (VNPL-1.1)\n" "\n" "USAGE:\n" + "%s df\n" + " Show pool space statistics\n" + "\n" "%s ls [-l] [-p POOL] [--sort FIELD] [-r] [-n N] [ ...]\n" " List images (only matching patterns if passed).\n" " -p|--pool POOL Filter images by pool ID or name\n" @@ -152,7 +155,8 @@ void cli_tool_t::help() " --no-color Disable colored output\n" " --json JSON output\n" , - exe_name, exe_name, exe_name, exe_name, exe_name, exe_name, exe_name, exe_name, exe_name, exe_name, exe_name + exe_name, exe_name, exe_name, exe_name, exe_name, exe_name, + exe_name, exe_name, exe_name, exe_name, exe_name, exe_name ); exit(0); } @@ -246,6 +250,11 @@ void cli_tool_t::run(json11::Json cfg) fprintf(stderr, "command is missing\n"); exit(1); } + else if (cmd[0] == "df") + { + // Show pool space stats + action_cb = start_df(cfg); + } else if (cmd[0] == "ls") { // List images diff --git a/src/cli.h b/src/cli.h index 3c1c10f7..90007a33 100644 --- a/src/cli.h +++ b/src/cli.h @@ -50,6 +50,7 @@ public: friend struct snap_flattener_t; friend struct snap_remover_t; + std::function start_df(json11::Json); std::function start_ls(json11::Json); std::function start_create(json11::Json); std::function start_modify(json11::Json); @@ -61,5 +62,14 @@ public: std::function simple_offsets(json11::Json cfg); }; -std::string format_size(uint64_t size); uint64_t parse_size(std::string size_str); + +std::string print_table(json11::Json items, json11::Json header, bool use_esc); + +std::string format_size(uint64_t size); + +std::string format_lat(uint64_t lat); + +std::string format_q(double depth); + +bool stupid_glob(const std::string str, const std::string glob); diff --git a/src/cli_df.cpp b/src/cli_df.cpp new file mode 100644 index 00000000..fa82a4d1 --- /dev/null +++ b/src/cli_df.cpp @@ -0,0 +1,229 @@ +// Copyright (c) Vitaliy Filippov, 2019+ +// License: VNPL-1.1 (see README.md for details) + +#include "cli.h" +#include "cluster_client.h" +#include "base64.h" + +// List pools with space statistics +struct pool_lister_t +{ + cli_tool_t *parent; + + int state = 0; + json11::Json space_info; + std::map pool_stats; + + bool is_done() + { + return state == 100; + } + + void get_stats() + { + if (state == 1) + goto resume_1; + // Space statistics - pool/stats/ + parent->waiting++; + parent->cli->st_cli.etcd_txn(json11::Json::object { + { "success", json11::Json::array { + json11::Json::object { + { "request_range", json11::Json::object { + { "key", base64_encode( + parent->cli->st_cli.etcd_prefix+"/pool/stats/" + ) }, + { "range_end", base64_encode( + parent->cli->st_cli.etcd_prefix+"/pool/stats0" + ) }, + } }, + }, + json11::Json::object { + { "request_range", json11::Json::object { + { "key", base64_encode( + parent->cli->st_cli.etcd_prefix+"/osd/stats/" + ) }, + { "range_end", base64_encode( + parent->cli->st_cli.etcd_prefix+"/osd/stats0" + ) }, + } }, + }, + } }, + }, ETCD_SLOW_TIMEOUT, [this](std::string err, json11::Json res) + { + parent->waiting--; + if (err != "") + { + fprintf(stderr, "Error reading from etcd: %s\n", err.c_str()); + exit(1); + } + space_info = res; + parent->ringloop->wakeup(); + }); + state = 1; +resume_1: + if (parent->waiting > 0) + return; + std::map osd_free; + for (auto & kv_item: space_info["responses"][0]["response_range"]["kvs"].array_items()) + { + auto kv = parent->cli->st_cli.parse_etcd_kv(kv_item); + // pool ID + pool_id_t pool_id; + char null_byte = 0; + sscanf(kv.key.substr(parent->cli->st_cli.etcd_prefix.length()).c_str(), "/pool/stats/%u%c", &pool_id, &null_byte); + if (!pool_id || pool_id >= POOL_ID_MAX || null_byte != 0) + { + fprintf(stderr, "Invalid key in etcd: %s\n", kv.key.c_str()); + continue; + } + // pool/stats/ + pool_stats[pool_id] = kv.value.object_items(); + } + for (auto & kv_item: space_info["responses"][1]["response_range"]["kvs"].array_items()) + { + auto kv = parent->cli->st_cli.parse_etcd_kv(kv_item); + // osd ID + osd_num_t osd_num; + char null_byte = 0; + sscanf(kv.key.substr(parent->cli->st_cli.etcd_prefix.length()).c_str(), "/osd/stats/%lu%c", &osd_num, &null_byte); + if (!osd_num || osd_num >= POOL_ID_MAX || null_byte != 0) + { + fprintf(stderr, "Invalid key in etcd: %s\n", kv.key.c_str()); + continue; + } + // osd/stats/::free + osd_free[osd_num] = kv.value["free"].uint64_value(); + } + // Calculate max_avail for each pool + for (auto & pp: parent->cli->st_cli.pool_config) + { + auto & pool_cfg = pp.second; + uint64_t pool_avail = UINT64_MAX; + std::map pg_per_osd; + for (auto & pgp: pool_cfg.pg_config) + { + for (auto pg_osd: pgp.second.target_set) + { + if (pg_osd != 0) + { + pg_per_osd[pg_osd]++; + } + } + } + for (auto pg_per_pair: pg_per_osd) + { + uint64_t pg_free = osd_free[pg_per_pair.first] * pool_cfg.pg_count / pg_per_pair.second; + if (pool_avail > pg_free) + { + pool_avail = pg_free; + } + } + if (pool_cfg.scheme != POOL_SCHEME_REPLICATED) + { + pool_avail = pool_avail * (pool_cfg.pg_size - pool_cfg.parity_chunks) / pool_stats[pool_cfg.id]["pg_real_size"].uint64_value(); + } + pool_stats[pool_cfg.id] = json11::Json::object { + { "name", pool_cfg.name }, + { "pg_count", pool_cfg.pg_count }, + { "scheme", pool_cfg.scheme == POOL_SCHEME_REPLICATED ? "replicated" : "jerasure" }, + { "scheme_name", pool_cfg.scheme == POOL_SCHEME_REPLICATED + ? std::to_string(pool_cfg.pg_size)+"/"+std::to_string(pool_cfg.pg_minsize) + : "EC "+std::to_string(pool_cfg.pg_size-pool_cfg.parity_chunks)+"+"+std::to_string(pool_cfg.parity_chunks) }, + { "used_raw", (uint64_t)(pool_stats[pool_cfg.id]["used_raw_tb"].number_value() * (1l<<40)) }, + { "total_raw", (uint64_t)(pool_stats[pool_cfg.id]["total_raw_tb"].number_value() * (1l<<40)) }, + { "max_available", pool_avail }, + { "raw_to_usable", pool_stats[pool_cfg.id]["raw_to_usable"].number_value() }, + { "space_efficiency", pool_stats[pool_cfg.id]["space_efficiency"].number_value() }, + { "pg_real_size", pool_stats[pool_cfg.id]["pg_real_size"].uint64_value() }, + { "failure_domain", pool_cfg.failure_domain }, + }; + } + } + + json11::Json::array to_list() + { + json11::Json::array list; + for (auto & kv: pool_stats) + { + list.push_back(kv.second); + } + return list; + } + + void loop() + { + get_stats(); + if (parent->waiting > 0) + return; + if (parent->json_output) + { + // JSON output + printf("%s\n", json11::Json(to_list()).dump().c_str()); + state = 100; + return; + } + // Table output: name, scheme_name, pg_count, total, used, max_avail, used%, efficiency + json11::Json::array cols; + cols.push_back(json11::Json::object{ + { "key", "name" }, + { "title", "NAME" }, + }); + cols.push_back(json11::Json::object{ + { "key", "scheme_name" }, + { "title", "SCHEME" }, + }); + cols.push_back(json11::Json::object{ + { "key", "pg_count" }, + { "title", "PGS" }, + }); + cols.push_back(json11::Json::object{ + { "key", "total_fmt" }, + { "title", "TOTAL" }, + }); + cols.push_back(json11::Json::object{ + { "key", "used_fmt" }, + { "title", "USED" }, + }); + cols.push_back(json11::Json::object{ + { "key", "max_avail_fmt" }, + { "title", "AVAILABLE" }, + }); + cols.push_back(json11::Json::object{ + { "key", "used_pct" }, + { "title", "USED%" }, + }); + cols.push_back(json11::Json::object{ + { "key", "eff_fmt" }, + { "title", "EFFICIENCY" }, + }); + json11::Json::array list; + for (auto & kv: pool_stats) + { + kv.second["total_fmt"] = format_size(kv.second["total_raw"].uint64_value() / kv.second["raw_to_usable"].number_value()); + kv.second["used_fmt"] = format_size(kv.second["used_raw"].uint64_value() / kv.second["raw_to_usable"].number_value()); + kv.second["max_avail_fmt"] = format_size(kv.second["max_available"].uint64_value()); + kv.second["used_pct"] = format_q(100 - 100*kv.second["max_available"].uint64_value() * + kv.second["raw_to_usable"].number_value() / kv.second["total_raw"].uint64_value())+"%"; + kv.second["eff_fmt"] = format_q(kv.second["space_efficiency"].number_value()*100)+"%"; + } + printf("%s", print_table(to_list(), cols, parent->color).c_str()); + state = 100; + } +}; + +std::function cli_tool_t::start_df(json11::Json cfg) +{ + json11::Json::array cmd = cfg["command"].array_items(); + auto lister = new pool_lister_t(); + lister->parent = this; + return [lister]() + { + lister->loop(); + if (lister->is_done()) + { + delete lister; + return true; + } + return false; + }; +} diff --git a/src/cli_ls.cpp b/src/cli_ls.cpp index 6ae33fb6..9a6c68a8 100644 --- a/src/cli_ls.cpp +++ b/src/cli_ls.cpp @@ -6,18 +6,6 @@ #include "cluster_client.h" #include "base64.h" -#define MIN(a, b) ((a) < (b) ? (b) : (a)) - -std::string print_table(json11::Json items, json11::Json header, bool use_esc); - -std::string format_size(uint64_t size); - -std::string format_lat(uint64_t lat); - -std::string format_q(double depth); - -bool stupid_glob(const std::string str, const std::string glob); - // List existing images // // Again, you can just look into etcd, but this console tool incapsulates it