From b878dcc1c3e4fc9c10b02bdd4948759f67c439e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Botond=20D=C3=A9nes?= Date: Wed, 25 Oct 2023 06:13:58 -0400 Subject: [PATCH 1/5] tools/scylla-nodetool: log responses with trace level With this, both requests and responses to/from the remote are logged when trace-level logging is enabled. This should greatly simplify debugging any problems. --- tools/scylla-nodetool.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/scylla-nodetool.cc b/tools/scylla-nodetool.cc index 7f5ec481b9..96521b40de 100644 --- a/tools/scylla-nodetool.cc +++ b/tools/scylla-nodetool.cc @@ -58,8 +58,10 @@ class scylla_rest_client { } if (res.empty()) { + nlog.trace("Got empty response"); return rjson::null_value(); } else { + nlog.trace("Got response:\n{}", res); return rjson::parse(res); } } From 7e3a78d73d01881872d02526ecf41e9534f19ad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Botond=20D=C3=A9nes?= Date: Wed, 25 Oct 2023 07:37:20 -0400 Subject: [PATCH 2/5] test/nodetool: rest_api_mock: add more options for multiple requests Change the current bool multiple param to a weak enum, allowing for a third value: ANY, which allows for 0 matches too. --- test/nodetool/rest_api_mock.py | 31 ++++++++++++++++++++----------- test/nodetool/test_compact.py | 21 ++++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/test/nodetool/rest_api_mock.py b/test/nodetool/rest_api_mock.py index e3b8381aa9..a6d3f523d9 100644 --- a/test/nodetool/rest_api_mock.py +++ b/test/nodetool/rest_api_mock.py @@ -20,7 +20,11 @@ logger = logging.getLogger(__name__) class expected_request: - def __init__(self, method: str, path: str, params: dict = {}, multiple: bool = False, + ANY = -1 # allow for any number of requests (including no requests at all), similar to the `*` quantity in regexp + ONE = 0 # exactly one request is allowed + MULTIPLE = 1 # one or more request is allowed + + def __init__(self, method: str, path: str, params: dict = {}, multiple: int = ONE, response: Dict[str, Any] = None, response_status: int = 200): self.method = method self.path = path @@ -52,7 +56,7 @@ def _make_expected_request(req_json): req_json["method"], req_json["path"], params=req_json.get("params", dict()), - multiple=req_json.get("multiple", False), + multiple=req_json.get("multiple", expected_request.ONE), response=req_json.get("response"), response_status=req_json.get("response_status", 200)) @@ -130,19 +134,24 @@ class rest_server(aiohttp.abc.AbstractRouter): return aiohttp.web.Response(status=500, text="Expected no requests, got {this_req}") expected_req = self.expected_requests[0] - if this_req != expected_req: - if expected_req.multiple and expected_req.hit > 0 and \ - len(self.expected_requests) > 1 and self.expected_requests[1] == this_req: + while this_req != expected_req: + if expected_req.multiple == expected_request.ANY or ( + expected_req.multiple >= expected_request.MULTIPLE and expected_req.hit >= expected_req.multiple): + logger.info(f"popping multi request {expected_req}") del self.expected_requests[0] expected_req = self.expected_requests[0] - else: - logger.error(f"unexpected request, expected {expected_req}, got {this_req}") - return aiohttp.web.Response(status=500, text="Expected {expected_req}, got {this_req}") - if not expected_req.multiple: + if len(self.expected_requests) > 0: + expected_req = self.expected_requests[0] + continue + + logger.error(f"unexpected request, expected {expected_req}, got {this_req}") + return aiohttp.web.Response(status=500, text="Expected {expected_req}, got {this_req}") + + if expected_req.multiple == expected_request.ONE: del self.expected_requests[0] - - expected_req.hit += 1 + else: + expected_req.hit += 1 if expected_req.response is None: logger.info(f"expected_request: {expected_req}, no response") diff --git a/test/nodetool/test_compact.py b/test/nodetool/test_compact.py index e095034ae7..f58563b2ba 100644 --- a/test/nodetool/test_compact.py +++ b/test/nodetool/test_compact.py @@ -10,14 +10,16 @@ import utils def test_all_keyspaces(nodetool): nodetool("compact", expected_requests=[ - expected_request("GET", "/storage_service/keyspaces", multiple=True, response=["system", "system_schema"]), + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["system", "system_schema"]), expected_request("POST", "/storage_service/keyspace_compaction/system"), expected_request("POST", "/storage_service/keyspace_compaction/system_schema")]) def test_keyspace(nodetool): nodetool("compact", "system_schema", expected_requests=[ - expected_request("GET", "/storage_service/keyspaces", multiple=True, response=["system", "system_schema"]), + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["system", "system_schema"]), expected_request("POST", "/storage_service/keyspace_compaction/system_schema")]) @@ -26,7 +28,8 @@ def test_nonexistent_keyspace(nodetool): nodetool, ("compact", "non_existent_ks"), {"expected_requests": [ - expected_request("GET", "/storage_service/keyspaces", multiple=True, response=["system"]), + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["system"]), expected_request("POST", "/storage_service/keyspace_compaction/non_existent_ks")]}, ["nodetool: Keyspace [non_existent_ks] does not exist.", "error processing arguments: keyspace non_existent_ks does not exist"]) @@ -34,11 +37,13 @@ def test_nonexistent_keyspace(nodetool): def test_table(nodetool): nodetool("compact", "system_schema", "columns", expected_requests=[ - expected_request("GET", "/storage_service/keyspaces", multiple=True, response=["system", "system_schema"]), + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["system", "system_schema"]), expected_request("POST", "/storage_service/keyspace_compaction/system_schema", params={"cf": "columns"})]) nodetool("compact", "system_schema", "columns", "computed_columns", expected_requests=[ - expected_request("GET", "/storage_service/keyspaces", multiple=True, response=["system", "system_schema"]), + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["system", "system_schema"]), expected_request("POST", "/storage_service/keyspace_compaction/system_schema", params={"cf": "columns,computed_columns"})]) @@ -46,7 +51,8 @@ def test_table(nodetool): def test_split_output_compatibility_argument(nodetool): dummy_request = [ - expected_request("GET", "/storage_service/keyspaces", multiple=True, response=["system", "system_schema"]), + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["system", "system_schema"]), expected_request("POST", "/storage_service/keyspace_compaction/system_schema")] nodetool("compact", "system_schema", "-s", expected_requests=dummy_request) @@ -55,7 +61,8 @@ def test_split_output_compatibility_argument(nodetool): def test_token_range_compatibility_argument(nodetool): dummy_request = [ - expected_request("GET", "/storage_service/keyspaces", multiple=True, response=["system", "system_schema"]), + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["system", "system_schema"]), expected_request("POST", "/storage_service/keyspace_compaction/system_schema")] nodetool("compact", "system_schema", "-st", "0", "-et", "1000", expected_requests=dummy_request) From b32ee54ba08de49bb38553785ebcc7e92ddefcb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Botond=20D=C3=A9nes?= Date: Wed, 25 Oct 2023 06:46:59 -0400 Subject: [PATCH 3/5] tools/scylla-nodetool: implement the cleanup command The --jobs command-line argument is accepted but ignored, just like the current nodetool does. --- test/nodetool/test_cleanup.py | 73 +++++++++++++++++++++++++++++++++++ tools/scylla-nodetool.cc | 64 +++++++++++++++++++++++++++--- 2 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 test/nodetool/test_cleanup.py diff --git a/test/nodetool/test_cleanup.py b/test/nodetool/test_cleanup.py new file mode 100644 index 0000000000..370d5408e2 --- /dev/null +++ b/test/nodetool/test_cleanup.py @@ -0,0 +1,73 @@ +# +# Copyright 2023-present ScyllaDB +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# + +from rest_api_mock import expected_request +import utils + + +def test_cleanup(nodetool): + nodetool("cleanup", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", params={"type": "non_local_strategy"}, + response=["ks1", "ks2"]), + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.ANY, + response=["ks1", "ks2", "system"]), + expected_request("POST", "/storage_service/keyspace_cleanup/ks1", response=0), + expected_request("POST", "/storage_service/keyspace_cleanup/ks2", response=0), + ]) + + +def test_cleanup_keyspace(nodetool): + nodetool("cleanup", "ks1", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["ks1", "ks2", "system"]), + expected_request("POST", "/storage_service/keyspace_cleanup/ks1", response=0), + ]) + + +def test_cleanup_table(nodetool): + nodetool("cleanup", "ks1", "tbl1", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["ks1", "ks2", "system"]), + expected_request("POST", "/storage_service/keyspace_cleanup/ks1", params={"cf": "tbl1"}, response=0), + ]) + + +def test_cleanup_tables(nodetool): + nodetool("cleanup", "ks1", "tbl1", "tbl2", "tbl3", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["ks1", "ks2", "system"]), + expected_request("POST", "/storage_service/keyspace_cleanup/ks1", params={"cf": "tbl1,tbl2,tbl3"}, response=0), + ]) + + +def test_cleanup_nonexistent_keyspace(nodetool): + utils.check_nodetool_fails_with( + nodetool, + ("cleanup", "non_existent_ks"), + {"expected_requests": [ + expected_request("GET", "/storage_service/keyspaces", response=["ks1", "ks2", "system"])]}, + ["nodetool: Keyspace [non_existent_ks] does not exist.", + "error processing arguments: keyspace non_existent_ks does not exist"]) + + +def test_cleanup_jobs_arg(nodetool): + nodetool("cleanup", "ks1", "-j", "0", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["ks1", "ks2", "system"]), + expected_request("POST", "/storage_service/keyspace_cleanup/ks1", response=0), + ]) + + nodetool("cleanup", "ks1", "--jobs", "2", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["ks1", "ks2", "system"]), + expected_request("POST", "/storage_service/keyspace_cleanup/ks1", response=0), + ]) + + nodetool("cleanup", "ks1", "--jobs=1", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.MULTIPLE, + response=["ks1", "ks2", "system"]), + expected_request("POST", "/storage_service/keyspace_cleanup/ks1", response=0), + ]) diff --git a/tools/scylla-nodetool.cc b/tools/scylla-nodetool.cc index 96521b40de..40e3e5be56 100644 --- a/tools/scylla-nodetool.cc +++ b/tools/scylla-nodetool.cc @@ -92,20 +92,51 @@ public: } }; +std::vector get_keyspaces(scylla_rest_client& client, std::optional type = {}) { + std::unordered_map params; + if (type) { + params["type"] = *type; + } + auto keyspaces_json = client.get("/storage_service/keyspaces", std::move(params)); + std::vector keyspaces; + for (const auto& keyspace_json : keyspaces_json.GetArray()) { + keyspaces.emplace_back(rjson::to_string_view(keyspace_json)); + } + return keyspaces; +} + using operation_func = void(*)(scylla_rest_client&, const bpo::variables_map&); std::map get_operations_with_func(); +void cleanup_operation(scylla_rest_client& client, const bpo::variables_map& vm) { + if (vm.count("cleanup_arg")) { + const auto all_keyspaces = get_keyspaces(client); + + auto args = vm["cleanup_arg"].as>(); + std::unordered_map params; + const auto keyspace = args[0]; + if (std::ranges::find(all_keyspaces, keyspace) == all_keyspaces.end()) { + throw std::invalid_argument(fmt::format("keyspace {} does not exist", keyspace)); + } + + if (args.size() > 1) { + params["cf"] = fmt::to_string(fmt::join(args.begin() + 1, args.end(), ",")); + } + client.post(format("/storage_service/keyspace_cleanup/{}", keyspace), std::move(params)); + } else { + for (const auto& keyspace : get_keyspaces(client, "non_local_strategy")) { + client.post(format("/storage_service/keyspace_cleanup/{}", keyspace)); + } + } +} + void compact_operation(scylla_rest_client& client, const bpo::variables_map& vm) { if (vm.count("user-defined")) { throw std::invalid_argument("--user-defined flag is unsupported"); } - auto keyspaces_json = client.get("/storage_service/keyspaces", {}); - std::vector all_keyspaces; - for (const auto& keyspace_json : keyspaces_json.GetArray()) { - all_keyspaces.emplace_back(rjson::to_string_view(keyspace_json)); - } + const auto all_keyspaces = get_keyspaces(client); if (vm.count("compaction_arg")) { auto args = vm["compaction_arg"].as>(); @@ -418,6 +449,29 @@ const std::map option_substitutions{ std::map get_operations_with_func() { const static std::map operations_with_func { + { + { + "cleanup", + "Triggers removal of data that the node no longer owns", +R"( +You should run nodetool cleanup whenever you scale-out (expand) your cluster, and +new nodes are added to the same DC. The scale out process causes the token ring +to get re-distributed. As a result, some of the nodes will have replicas for +tokens that they are no longer responsible for (taking up disk space). This data +continues to consume diskspace until you run nodetool cleanup. The cleanup +operation deletes these replicas and frees up disk space. + +Fore more information, see: https://opensource.docs.scylladb.com/stable/operating-scylla/nodetool-commands/cleanup.html +)", + { + typed_option("jobs,j", "The number of compaction jobs to be used for the cleanup (unused)"), + }, + { + typed_option>("cleanup_arg", "[ ...]", -1), + } + }, + cleanup_operation + }, { { "compact", From 27854a50be093d22a77c7397ed6a58034ee1644c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Botond=20D=C3=A9nes?= Date: Wed, 25 Oct 2023 07:55:39 -0400 Subject: [PATCH 4/5] tools/scylla-nodetool: implement clearsnapshot command --- test/nodetool/test_snapshot.py | 61 ++++++++++++++++++++++++++++++++++ tools/scylla-nodetool.cc | 43 ++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 test/nodetool/test_snapshot.py diff --git a/test/nodetool/test_snapshot.py b/test/nodetool/test_snapshot.py new file mode 100644 index 0000000000..e155baa368 --- /dev/null +++ b/test/nodetool/test_snapshot.py @@ -0,0 +1,61 @@ +# +# Copyright 2023-present ScyllaDB +# +# SPDX-License-Identifier: AGPL-3.0-or-later +# + +from rest_api_mock import expected_request +import utils + + +def test_clearnapshot(nodetool): + nodetool("clearsnapshot", expected_requests=[ + expected_request("DELETE", "/storage_service/snapshots") + ]) + + +def test_clearnapshot_keyspace(nodetool): + nodetool("clearsnapshot", "ks1", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.ANY, + response=["ks1", "ks2"]), + expected_request("DELETE", "/storage_service/snapshots", params={"kn": "ks1"}) + ]) + + +def test_clearnapshot_keyspaces(nodetool): + nodetool("clearsnapshot", "ks1", "ks2", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.ANY, + response=["ks1", "ks2"]), + expected_request("DELETE", "/storage_service/snapshots", params={"kn": "ks1,ks2"}) + ]) + + +def test_clearnapshot_nonexistent_keyspaces(nodetool, scylla_only): + utils.check_nodetool_fails_with( + nodetool, + ("clearsnapshot", "non_existent_ks"), + {"expected_requests": [ + expected_request("GET", "/storage_service/keyspaces", response=["ks1", "ks2"])]}, + ["error processing arguments: keyspace non_existent_ks does not exist"]) + + +def test_clearnapshot_tag(nodetool): + nodetool("clearsnapshot", "-t", "snapshot_name", expected_requests=[ + expected_request("DELETE", "/storage_service/snapshots", params={"tag": "snapshot_name"}) + ]) + + +def test_clearnapshot_tag_and_keyspace(nodetool): + nodetool("clearsnapshot", "-t", "snapshot_name", "ks1", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.ANY, + response=["ks1", "ks2"]), + expected_request("DELETE", "/storage_service/snapshots", params={"kn": "ks1", "tag": "snapshot_name"}) + ]) + + +def test_clearnapshot_tag_and_keyspaces(nodetool): + nodetool("clearsnapshot", "-t", "snapshot_name", "ks1", "ks2", expected_requests=[ + expected_request("GET", "/storage_service/keyspaces", multiple=expected_request.ANY, + response=["ks1", "ks2"]), + expected_request("DELETE", "/storage_service/snapshots", params={"kn": "ks1,ks2", "tag": "snapshot_name"}) + ]) diff --git a/tools/scylla-nodetool.cc b/tools/scylla-nodetool.cc index 40e3e5be56..aaf5a98b36 100644 --- a/tools/scylla-nodetool.cc +++ b/tools/scylla-nodetool.cc @@ -131,6 +131,31 @@ void cleanup_operation(scylla_rest_client& client, const bpo::variables_map& vm) } } +void clearsnapshot_operation(scylla_rest_client& client, const bpo::variables_map& vm) { + std::unordered_map params; + + if (vm.count("keyspaces")) { + std::vector keyspaces; + const auto all_keyspaces = get_keyspaces(client); + for (const auto& keyspace : vm["keyspaces"].as>()) { + if (std::ranges::find(all_keyspaces, keyspace) == all_keyspaces.end()) { + throw std::invalid_argument(fmt::format("keyspace {} does not exist", keyspace)); + } + keyspaces.push_back(keyspace); + } + + if (!keyspaces.empty()) { + params["kn"] = fmt::to_string(fmt::join(keyspaces.begin(), keyspaces.end(), ",")); + } + } + + if (vm.count("tag")) { + params["tag"] = vm["tag"].as(); + } + + client.del("/storage_service/snapshots", std::move(params)); +} + void compact_operation(scylla_rest_client& client, const bpo::variables_map& vm) { if (vm.count("user-defined")) { throw std::invalid_argument("--user-defined flag is unsupported"); @@ -472,6 +497,24 @@ Fore more information, see: https://opensource.docs.scylladb.com/stable/operatin }, cleanup_operation }, + { + { + "clearsnapshot", + "Remove snapshots", +R"( +By default all snapshots are removed for all keyspaces. + +Fore more information, see: https://opensource.docs.scylladb.com/stable/operating-scylla/nodetool-commands/clearsnapshot.html +)", + { + typed_option("tag,t", "The snapshot to remove"), + }, + { + typed_option>("keyspaces", "[...]", -1), + } + }, + clearsnapshot_operation + }, { { "compact", From 16ce212c31283c0920dbaac0672e9fdf6872bfc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Botond=20D=C3=A9nes?= Date: Wed, 25 Oct 2023 09:58:21 -0400 Subject: [PATCH 5/5] tools/scylla-nodetool: implement the listsnapshots command The output is changed slightly, compared to the current nodetool: * Number columns are aligned to the right * Number columns don't have decimal places * There are no trailing whitespaces --- test/nodetool/test_snapshot.py | 40 +++++++++++++++++++++ tools/scylla-nodetool.cc | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/test/nodetool/test_snapshot.py b/test/nodetool/test_snapshot.py index e155baa368..11ad6004b4 100644 --- a/test/nodetool/test_snapshot.py +++ b/test/nodetool/test_snapshot.py @@ -59,3 +59,43 @@ def test_clearnapshot_tag_and_keyspaces(nodetool): response=["ks1", "ks2"]), expected_request("DELETE", "/storage_service/snapshots", params={"kn": "ks1,ks2", "tag": "snapshot_name"}) ]) + + +def test_listsnapshots(nodetool, request): + res = nodetool("listsnapshots", expected_requests=[ + expected_request("GET", "/storage_service/snapshots", response=[ + {"key": "1698236289867", "value": [{"ks": "ks1", "cf": "tbl1", "total": 45056, "live": 0}, + {"ks": "ks1", "cf": "tbl2", "total": 40956, "live": 0}]}, + {"key": "1698236070745", "value": [{"ks": "ks1", "cf": "tbl1", "total": 35056, "live": 0}, + {"ks": "ks1", "cf": "tbl2", "total": 20956, "live": 0}]}, + ]), + expected_request("GET", "/storage_service/snapshots/size/true", response=945235), + ]) + + cassandra_expected_output =\ +"""Snapshot Details: +Snapshot name Keyspace name Column family name True size Size on disk +1698236289867 ks1 tbl1 0 bytes 44 KB +1698236289867 ks1 tbl2 0 bytes 40 KB +1698236070745 ks1 tbl1 0 bytes 34.23 KB +1698236070745 ks1 tbl2 0 bytes 20.46 KB + +Total TrueDiskSpaceUsed: 923.08 KiB + +""" + scylla_expected_output =\ +"""Snapshot Details: +Snapshot name Keyspace name Column family name True size Size on disk +1698236289867 ks1 tbl1 0 B 44 KiB +1698236289867 ks1 tbl2 0 B 40 KiB +1698236070745 ks1 tbl1 0 B 34 KiB +1698236070745 ks1 tbl2 0 B 20 KiB + +Total TrueDiskSpaceUsed: 923 KiB + +""" + + if request.config.getoption("nodetool") == "scylla": + assert res == scylla_expected_output + else: + assert res == cassandra_expected_output diff --git a/tools/scylla-nodetool.cc b/tools/scylla-nodetool.cc index aaf5a98b36..717b493635 100644 --- a/tools/scylla-nodetool.cc +++ b/tools/scylla-nodetool.cc @@ -20,6 +20,7 @@ #include "log.hh" #include "tools/utils.hh" #include "utils/http.hh" +#include "utils/human_readable.hh" #include "utils/rjson.hh" #include "utils/UUID.hh" @@ -338,6 +339,54 @@ void gettraceprobability_operation(scylla_rest_client& client, const bpo::variab fmt::print(std::cout, "Current trace probability: {}\n", res.GetDouble()); } +void listsnapshots_operation(scylla_rest_client& client, const bpo::variables_map& vm) { + const auto snapshots = client.get("/storage_service/snapshots"); + const auto true_size = client.get("/storage_service/snapshots/size/true").GetInt64(); + + std::array header_row{"Snapshot name", "Keyspace name", "Column family name", "True size", "Size on disk"}; + std::array max_column_length{}; + for (size_t c = 0; c < header_row.size(); ++c) { + max_column_length[c] = header_row[c].size(); + } + + auto format_hr_size = [] (const utils::human_readable_value hrv) { + if (!hrv.suffix || hrv.suffix == 'B') { + return fmt::format("{} B ", hrv.value); + } + return fmt::format("{} {}iB", hrv.value, hrv.suffix); + }; + + std::vector> rows; + for (const auto& snapshot_by_name : snapshots.GetArray()) { + const auto snapshot_name = std::string(rjson::to_string_view(snapshot_by_name.GetObject()["key"])); + for (const auto& snapshot : snapshot_by_name.GetObject()["value"].GetArray()) { + rows.push_back({ + snapshot_name, + std::string(rjson::to_string_view(snapshot["ks"])), + std::string(rjson::to_string_view(snapshot["cf"])), + format_hr_size(utils::to_hr_size(snapshot["live"].GetInt64())), + format_hr_size(utils::to_hr_size(snapshot["total"].GetInt64()))}); + + for (size_t c = 0; c < rows.back().size(); ++c) { + max_column_length[c] = std::max(max_column_length[c], rows.back()[c].size()); + } + } + } + + const auto header_row_format = fmt::format("{{:<{}}} {{:<{}}} {{:<{}}} {{:<{}}} {{:<{}}}\n", max_column_length[0], + max_column_length[1], max_column_length[2], max_column_length[3], max_column_length[4]); + const auto regular_row_format = fmt::format("{{:<{}}} {{:<{}}} {{:<{}}} {{:>{}}} {{:>{}}}\n", max_column_length[0], + max_column_length[1], max_column_length[2], max_column_length[3], max_column_length[4]); + + fmt::print(std::cout, "Snapshot Details:\n"); + fmt::print(std::cout, fmt::runtime(header_row_format.c_str()), header_row[0], header_row[1], header_row[2], header_row[3], header_row[4]); + for (const auto& r : rows) { + fmt::print(std::cout, fmt::runtime(regular_row_format.c_str()), r[0], r[1], r[2], r[3], r[4]); + } + + fmt::print(std::cout, "\nTotal TrueDiskSpaceUsed: {}\n\n", format_hr_size(utils::to_hr_size(true_size))); +} + void help_operation(const tool_app_template::config& cfg, const bpo::variables_map& vm) { if (vm.count("command")) { const auto command = vm["command"].as(); @@ -644,6 +693,20 @@ Fore more information, see: https://opensource.docs.scylladb.com/stable/operatin }, [] (scylla_rest_client&, const bpo::variables_map&) {} }, + { + { + "listsnapshots", + "Lists all the snapshots along with the size on disk and true size", +R"( +Dropped tables (column family) will not be part of the listsnapshots. + +Fore more information, see: https://opensource.docs.scylladb.com/stable/operating-scylla/nodetool-commands/listsnapshots.html +)", + { }, + { }, + }, + listsnapshots_operation + }, { { "settraceprobability",