Implement vitastor-cli ls-user, modify-user, remove-user commands

This commit is contained in:
Vitaliy Filippov
2026-04-22 01:41:29 +03:00
parent 6cc899db84
commit 3551525f5e
7 changed files with 428 additions and 2 deletions
+3 -1
View File
@@ -15,7 +15,6 @@ add_custom_command(
add_library(vitastor_cli STATIC
cli_common.cpp
cli_alloc_osd.cpp
cli_status.cpp
cli_describe.cpp
cli_fix.cpp
cli_ls.cpp
@@ -23,6 +22,7 @@ add_library(vitastor_cli STATIC
cli_dd.cpp
cli_modify.cpp
cli_modify_osd.cpp
cli_modify_user.cpp
cli_osd_tree.cpp
cli_pg_ls.cpp
cli_flatten.cpp
@@ -37,6 +37,8 @@ add_library(vitastor_cli STATIC
cli_pool_modify.cpp
cli_pool_rm.cpp
cli_serve.cpp
cli_status.cpp
cli_user_ls.cpp
${OPENAPI_JSON_H}
)
target_compile_options(vitastor_cli PUBLIC -fPIC)
+40
View File
@@ -244,6 +244,17 @@ static const char* help_text =
" -r|--reverse Sort in descending order\n"
" -n|--count N Only list first N items\n"
"\n"
"vitastor-cli ls-users|user-ls|ls-user|list-users [<name> ...]\n"
" List users (only with specified names if passed).\n"
"\n"
"vitastor-cli modify-user --type <type> --groups group1,group2,... <username>\n"
" Create or update user permissions. User names match CN of their certificates.\n"
" --type TYPE Set user type: client, admin, mon or osd. Default is client.\n"
" --groups GROUPS Set user's groups.\n"
"\n"
"vitastor-cli rm-user|remove-user|delete-user <username>\n"
" Remove a user.\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"
@@ -574,6 +585,35 @@ std::function<bool(cli_result_t &)> cli_tool_t::start(json11::Json::object cfg,
}
action_cb = start_pool_ls(cfg);
}
else if (cmd[0] == "user-ls" || cmd[0] == "ls-user" || cmd[0] == "ls-users" || cmd[0] == "list-users")
{
// List users
if (cmd.size() > 1)
{
cmd.erase(cmd.begin(), cmd.begin()+1);
cfg["names"] = cmd;
}
action_cb = start_user_ls(cfg);
}
else if (cmd[0] == "modify-user" || cmd[0] == "user-modify")
{
// Create/update user
if (cmd.size() > 1)
{
cfg["name"] = cmd[1];
}
action_cb = start_modify_user(cfg);
}
else if (cmd[0] == "rm-user" || cmd[0] == "remove-user" || cmd[0] == "delete-user")
{
// Remove user
if (cmd.size() > 1)
{
cfg["name"] = cmd[1];
}
cfg["remove"] = true;
action_cb = start_modify_user(cfg);
}
else if (cmd[0] == "serve")
{
// Start HTTP server
+2
View File
@@ -69,6 +69,7 @@ public:
std::function<bool(cli_result_t &)> start_merge(json11::Json);
std::function<bool(cli_result_t &)> start_modify(json11::Json);
std::function<bool(cli_result_t &)> start_modify_osd(json11::Json);
std::function<bool(cli_result_t &)> start_modify_user(json11::Json);
std::function<bool(cli_result_t &)> start_osd_tree(json11::Json);
std::function<bool(cli_result_t &)> start_pg_list(json11::Json);
std::function<bool(cli_result_t &)> start_pool_create(json11::Json);
@@ -81,6 +82,7 @@ public:
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_user_ls(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);
+161
View File
@@ -0,0 +1,161 @@
// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)
#include "cli.h"
#include "cluster_client.h"
#include "str_util.h"
// Create/update/delete a user
struct cli_modify_user_t
{
cli_tool_t *parent;
std::string user_name;
std::string user_type;
json11::Json groups;
bool del = false;
int state = 0;
cli_result_t result;
etcd_kv_t kv;
json11::Json::object new_cfg;
bool is_done()
{
return state == 100;
}
void loop()
{
if (state == 1)
goto resume_1;
else if (state == 2)
goto resume_2;
if (user_type != "client" && user_type != "admin" && user_type != "mon" && user_type != "osd")
{
result = (cli_result_t){ .err = EINVAL, .text = "Unknown user type: "+user_type };
state = 100;
return;
}
if (groups.is_string())
{
groups = groups == "" ? std::vector<std::string>() : explode(",", groups.string_value(), true);
}
else if (groups.is_array())
{
for (auto & gr: groups.array_items())
{
if (!gr.is_string())
{
result = (cli_result_t){ .err = EINVAL, .text = "Group names must be strings" };
state = 100;
return;
}
}
}
else if (!groups.is_null())
{
result = (cli_result_t){ .err = EINVAL, .text = "Group names must be strings" };
state = 100;
return;
}
if (user_name == "")
{
result = (cli_result_t){ .err = EINVAL, .text = "User name must not be empty" };
state = 100;
return;
}
parent->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+"/config/user/"+user_name) },
} },
} } }
});
state = 1;
resume_1:
if (parent->waiting > 0)
return;
if (parent->etcd_err.err)
{
result = parent->etcd_err;
state = 100;
return;
}
kv = parent->cli->st_cli.parse_etcd_kv(parent->etcd_result["responses"][0]["response_range"]["kvs"][0]);
while (true)
{
new_cfg = kv.value.object_items();
if (!kv.mod_revision && del)
{
result = (cli_result_t){ .err = ENOENT, .text = "User "+user_name+" does not exist" };
state = 100;
break;
}
if (!groups.is_null())
new_cfg["groups"] = groups;
if (user_type != "")
new_cfg["type"] = user_type;
if (!new_cfg["type"].is_string())
new_cfg["type"] = "client";
parent->etcd_txn(json11::Json::object {
{ "compare", json11::Json::array { json11::Json::object {
{ "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) },
{ "target", kv.mod_revision ? "MOD" : "VERSION" },
{ kv.mod_revision ? "mod_revision" : "version", kv.mod_revision },
} } },
{ "success", json11::Json::array {
del ? json11::Json::object { { "request_delete_range", json11::Json::object {
{ "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) },
} } } : json11::Json::object { { "request_put", json11::Json::object {
{ "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) },
{ "value", base64_encode(json11::Json(new_cfg).dump()) }
} } },
} },
{ "failure", json11::Json::array { json11::Json::object {
{ "request_range", json11::Json::object {
{ "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) },
} },
} } },
});
state = 2;
resume_2:
if (parent->waiting > 0)
return;
if (parent->etcd_err.err)
{
result = parent->etcd_err;
state = 100;
return;
}
if (parent->etcd_result["succeeded"].bool_value())
break;
kv = parent->cli->st_cli.parse_etcd_kv(parent->etcd_result["responses"][0]["response_range"]["kvs"][0]);
}
state = 100;
result.text = del ? "User "+user_name+" removed" : "User "+user_name+" modified";
new_cfg["name"] = user_name;
result.data = del ? json11::Json::object{ { "ok", true } } : new_cfg;
}
};
std::function<bool(cli_result_t &)> cli_tool_t::start_modify_user(json11::Json cfg)
{
auto creator = new cli_modify_user_t();
creator->parent = this;
creator->user_name = cfg["name"].string_value();
creator->user_type = cfg["type"].string_value();
creator->groups = cfg["groups"];
creator->del = cfg["remove"].bool_value();
return [creator](cli_result_t & result)
{
creator->loop();
if (creator->is_done())
{
result = creator->result;
delete creator;
return true;
}
return false;
};
}
+3
View File
@@ -59,6 +59,9 @@ struct cli_serve_t
{"pool/delete", {"rm-pool", false}},
{"pool/list", {"pools", true}},
{"pool/modify", {"modify-pool", false}},
{"user/delete", {"remove-user", false}},
{"user/list", {"ls-user", false}},
{"user/modify", {"modify-user", false}},
{"status", {"status", true}},
};
+128
View File
@@ -0,0 +1,128 @@
// Copyright (c) Vitaliy Filippov, 2019+
// License: VNPL-1.1 (see README.md for details)
#include <algorithm>
#include "cli.h"
#include "cluster_client.h"
#include "str_util.h"
#include "json_util.h"
// List users
struct user_lister_t
{
cli_tool_t *parent;
std::vector<std::string> only_names;
int state = 0;
cli_result_t result;
json11::Json::array users;
bool is_done()
{
return state == 100;
}
void loop()
{
if (state == 1)
goto resume_1;
if (state == 100)
return;
{
json11::Json::array select;
if (!only_names.size())
{
select.push_back(json11::Json::object {
{ "request_range", json11::Json::object {
{ "key", base64_encode(
parent->cli->st_cli.etcd_prefix+"/config/user/"
) },
{ "range_end", base64_encode(
parent->cli->st_cli.etcd_prefix+"/config/user0"
) },
} },
});
}
else
{
for (auto & name: only_names)
{
select.push_back(json11::Json::object {
{ "request_range", json11::Json::object {
{ "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+name) },
} }
});
}
}
parent->etcd_txn(json11::Json::object {
{ "success", select },
});
}
state = 1;
resume_1:
if (parent->waiting > 0)
return;
if (parent->etcd_err.err)
{
result = parent->etcd_err;
state = 100;
return;
}
for (auto & response: parent->etcd_result["responses"].array_items())
{
for (auto & kv_item: response["response_range"]["kvs"].array_items())
{
auto kv = parent->cli->st_cli.parse_etcd_kv(kv_item);
auto user = kv.value.object_items();
user["name"] = kv.key.substr(parent->cli->st_cli.etcd_prefix.size()+13);
if (!parent->json_output)
user["groups_fmt"] = implode(",", user["groups"].array_items());
users.push_back(std::move(user));
}
}
if (parent->json_output)
{
// JSON output
result.data = users;
state = 100;
return;
}
// Table output: name, type, groups
json11::Json::array cols;
cols.push_back(json11::Json::object{
{ "key", "name" },
{ "title", "NAME" },
});
cols.push_back(json11::Json::object{
{ "key", "type" },
{ "title", "TYPE" },
});
cols.push_back(json11::Json::object{
{ "key", "groups_fmt" },
{ "title", "GROUPS" },
});
result.text = print_table(users, cols, parent->color);
state = 100;
}
};
std::function<bool(cli_result_t &)> cli_tool_t::start_user_ls(json11::Json cfg)
{
auto lister = new user_lister_t();
lister->parent = this;
if (cfg["names"].is_string())
lister->only_names.push_back(cfg["names"].string_value());
for (auto & item: cfg["names"].array_items())
lister->only_names.push_back(item.string_value());
return [lister](cli_result_t & result)
{
lister->loop();
if (lister->is_done())
{
result = lister->result;
delete lister;
return true;
}
return false;
};
}
+91 -1
View File
@@ -117,6 +117,7 @@
"responses": {
"200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Image" } } } },
"400": { "$ref": "#/components/responses/Invalid" },
"403": { "$ref": "#/components/responses/Forbidden" },
"409": { "$ref": "#/components/responses/UpdateConflict" },
"412": { "$ref": "#/components/responses/PreconditionFailed" }
}
@@ -159,6 +160,7 @@
"responses": {
"200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Image" } } } },
"400": { "$ref": "#/components/responses/Invalid" },
"403": { "$ref": "#/components/responses/Forbidden" },
"409": { "$ref": "#/components/responses/UpdateConflict" },
"412": { "$ref": "#/components/responses/PreconditionFailed" }
}
@@ -208,6 +210,7 @@
}
} } } },
"400": { "$ref": "#/components/responses/Invalid" },
"403": { "$ref": "#/components/responses/Forbidden" },
"409": { "$ref": "#/components/responses/UpdateConflict" },
"412": { "$ref": "#/components/responses/PreconditionFailed" }
}
@@ -227,6 +230,7 @@
"responses": {
"200": { "content": { "text/plain": { "schema": { "type": "string", "description": "Empty response" } } } },
"400": { "$ref": "#/components/responses/Invalid" },
"403": { "$ref": "#/components/responses/Forbidden" },
"409": { "$ref": "#/components/responses/UpdateConflict" },
"412": { "$ref": "#/components/responses/PreconditionFailed" }
}
@@ -419,7 +423,7 @@
}
} },
"/pool/delete": { "post": {
"summary": "Delete an pool",
"summary": "Delete a pool",
"operationId": "poolDelete",
"consumes": [ "application/json" ],
"produces": [ "application/json" ],
@@ -441,6 +445,66 @@
"412": { "$ref": "#/components/responses/PreconditionFailed" }
}
} },
"/user/list": { "get": {
"summary": "List users",
"operationId": "userListGet",
"produces": [ "application/json" ],
"parameters": [ {
"name": "params",
"in": "query",
"schema": { "$ref": "#/components/schemas/UserListParams" },
"style": "form",
"explode": true
} ],
"responses": {
"200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserList" } } } }
}
}, "post": {
"summary": "List users",
"operationId": "userListPost",
"consumes": [ "application/json" ],
"produces": [ "application/json" ],
"requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserListParams" } } } },
"responses": {
"200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserList" } } } }
}
} },
"/user/modify": { "post": {
"summary": "Modify a user",
"operationId": "userModify",
"consumes": [ "application/json" ],
"produces": [ "application/json" ],
"requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } },
"responses": {
"200": { "content": { "application/json": { "schema": {
"$ref": "#/components/schemas/User"
} } } },
"400": { "$ref": "#/components/responses/Invalid" },
"409": { "$ref": "#/components/responses/UpdateConflict" },
"412": { "$ref": "#/components/responses/PreconditionFailed" }
}
} },
"/user/delete": { "post": {
"summary": "Delete a user",
"operationId": "userDelete",
"consumes": [ "application/json" ],
"produces": [ "application/json" ],
"requestBody": { "content": { "application/json": { "schema": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "User name to delete" }
}
} } } },
"responses": {
"200": { "content": { "application/json": { "schema": {
"type": "object",
"properties": { "ok": { "type": "boolean" } }
} } } },
"400": { "$ref": "#/components/responses/Invalid" },
"409": { "$ref": "#/components/responses/UpdateConflict" },
"412": { "$ref": "#/components/responses/PreconditionFailed" }
}
} },
"/pg/list": { "get": {
"summary": "List PGs",
"operationId": "pgListGet",
@@ -611,6 +675,14 @@
}
}
},
"Forbidden": {
"description": "Access Denied (EACCES)",
"content": {
"text/plain": {
"schema": { "type": "string", "description": "Error text" }
}
}
},
"PreconditionFailed": {
"description": "Precondition Failed (ENOTEMPTY, EEXIST or ENOENT)",
"content": {
@@ -860,6 +932,24 @@
} } ]
}
},
"UserListParams": {
"type": "object",
"properties": {
"names": { "type": "array", "items": { "type": "string" }, "description": "User names to list" }
}
},
"UserList": {
"type": "array",
"items": { "$ref": "#/components/schemas/User" }
},
"User": {
"type": "object",
"properties": {
"name": { "type": "string", "description": "User name" },
"type": { "type": "string", "enum": [ "osd", "mon", "admin", "client" ], "description": "User type" },
"groups": { "type": "array", "items": { "type": "string" }, "description": "User group names" }
}
},
"PgListParams": {
"type": "object",
"properties": {