]> git.proxmox.com Git - ceph.git/blob - ceph/src/crimson/admin/admin_socket.cc
update ceph source to reef 18.2.1
[ceph.git] / ceph / src / crimson / admin / admin_socket.cc
1 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2 // vim: ts=8 sw=2 smarttab
3
4 #include "crimson/admin/admin_socket.h"
5
6 #include <boost/algorithm/string/join.hpp>
7 #include <fmt/format.h>
8 #include <fmt/ranges.h>
9 #include <seastar/net/api.hh>
10 #include <seastar/net/inet_address.hh>
11 #include <seastar/core/future-util.hh>
12 #include <seastar/core/reactor.hh>
13 #include <seastar/core/sleep.hh>
14 #include <seastar/core/thread.hh>
15 #include <seastar/util/std-compat.hh>
16
17 #include "common/options.h"
18 #include "common/version.h"
19 #include "messages/MCommand.h"
20 #include "messages/MCommandReply.h"
21 #include "crimson/common/log.h"
22 #include "crimson/net/Socket.h"
23
24 using namespace crimson::common;
25 using namespace std::literals;
26 using ceph::common::cmdmap_from_json;
27 using ceph::common::cmd_getval;
28 using ceph::common::bad_cmd_get;
29 using ceph::common::validate_cmd;
30 using ceph::common::dump_cmd_and_help_to_json;
31
32 namespace {
33 seastar::logger& logger()
34 {
35 return crimson::get_logger(ceph_subsys_osd);
36 }
37 } // namespace
38
39 using std::string;
40 using std::string_view;
41 using std::stringstream;
42 using std::unique_ptr;
43
44 namespace crimson::admin {
45
46 tell_result_t::tell_result_t(int ret, std::string&& err)
47 : ret{ret}, err(std::move(err))
48 {}
49
50 tell_result_t::tell_result_t(int ret, std::string&& err, ceph::bufferlist&& out)
51 : ret{ret}, err(std::move(err)), out(std::move(out))
52 {}
53
54 tell_result_t::tell_result_t(std::unique_ptr<Formatter> formatter)
55 {
56 formatter->flush(out);
57 }
58
59 void AdminSocket::register_command(std::unique_ptr<AdminSocketHook>&& hook)
60 {
61 auto prefix = hook->prefix;
62 auto [it, added] = hooks.emplace(prefix, std::move(hook));
63 assert(added);
64 logger().info("register_command(): {})", it->first);
65 }
66
67 auto AdminSocket::parse_cmd(const std::vector<std::string>& cmd)
68 -> std::variant<parsed_command_t, tell_result_t>
69 {
70 // preliminaries:
71 // - create the formatter specified by the cmd parameters
72 // - locate the "op-code" string (the 'prefix' segment)
73 // - prepare for command parameters extraction via cmdmap_t
74 cmdmap_t cmdmap;
75 ceph::bufferlist out;
76
77 try {
78 stringstream errss;
79 // note that cmdmap_from_json() may throw on syntax issues
80 if (!cmdmap_from_json(cmd, &cmdmap, errss)) {
81 logger().error("{}: incoming command error: {}", __func__, errss.str());
82 out.append("error:"s);
83 out.append(errss.str());
84 return tell_result_t{-EINVAL, "invalid json", std::move(out)};
85 }
86 } catch (const std::runtime_error& e) {
87 logger().error("{}: incoming command syntax: {}", __func__, cmd);
88 out.append(string{e.what()});
89 return tell_result_t{-EINVAL, "invalid json", std::move(out)};
90 }
91
92 string format;
93 string prefix;
94 try {
95 cmd_getval(cmdmap, "format", format);
96 cmd_getval(cmdmap, "prefix", prefix);
97 // tolerate old-style pg <pgid> command <args> style formatting
98 if (prefix == "pg") {
99 cmd_getval(cmdmap, "cmd", prefix);
100 }
101 } catch (const bad_cmd_get& e) {
102 logger().error("{}: invalid syntax: {}", __func__, cmd);
103 out.append(string{e.what()});
104 return tell_result_t{-EINVAL, "invalid json", std::move(out)};
105 }
106
107 // match the incoming op-code to one of the registered APIs
108 if (auto found = hooks.find(prefix); found != hooks.end()) {
109 return parsed_command_t{ cmdmap, format, *found->second };
110 } else {
111 return tell_result_t{-EINVAL,
112 fmt::format("unknown command '{}'", prefix),
113 std::move(out)};
114 }
115 }
116
117 seastar::future<> AdminSocket::finalize_response(
118 seastar::output_stream<char>& out, ceph::bufferlist&& msgs)
119 {
120 string outbuf_cont = msgs.to_str();
121 if (outbuf_cont.empty()) {
122 outbuf_cont = " {} ";
123 }
124 uint32_t response_length = htonl(outbuf_cont.length());
125 logger().info("asok response length: {}", outbuf_cont.length());
126
127 return out.write(reinterpret_cast<char*>(&response_length),
128 sizeof(response_length))
129 .then([&out, outbuf_cont] { return out.write(outbuf_cont.c_str()); });
130 }
131
132
133 seastar::future<> AdminSocket::handle_command(crimson::net::ConnectionRef conn,
134 boost::intrusive_ptr<MCommand> m)
135 {
136 return execute_command(m->cmd, std::move(m->get_data())).then(
137 [conn, tid=m->get_tid()](auto result) {
138 auto [ret, err, out] = std::move(result);
139 auto reply = crimson::make_message<MCommandReply>(ret, err);
140 reply->set_tid(tid);
141 reply->set_data(out);
142 return conn->send(std::move(reply));
143 });
144 }
145
146 seastar::future<> AdminSocket::execute_line(std::string cmdline,
147 seastar::output_stream<char>& out)
148 {
149 return execute_command({std::move(cmdline)}, {}).then([&out, this](auto result) {
150 auto [ret, stderr, stdout] = std::move(result);
151 if (ret < 0) {
152 stdout.append(fmt::format("ERROR: {}\n", cpp_strerror(ret)));
153 stdout.append(stderr);
154 }
155 return finalize_response(out, std::move(stdout));
156 });
157 }
158
159 auto AdminSocket::execute_command(const std::vector<std::string>& cmd,
160 ceph::bufferlist&& buf)
161 -> seastar::future<tell_result_t>
162 {
163 auto maybe_parsed = parse_cmd(cmd);
164 if (auto* parsed = std::get_if<parsed_command_t>(&maybe_parsed); parsed) {
165 stringstream os;
166 string desc{parsed->hook.desc};
167 if (!validate_cmd(desc, parsed->params, os)) {
168 logger().error("AdminSocket::execute_command: "
169 "failed to validate '{}': {}", cmd, os.str());
170 ceph::bufferlist out;
171 out.append(os);
172 return seastar::make_ready_future<tell_result_t>(
173 tell_result_t{-EINVAL, "invalid command json", std::move(out)});
174 }
175 return parsed->hook.call(parsed->params, parsed->format, std::move(buf));
176 } else {
177 auto& result = std::get<tell_result_t>(maybe_parsed);
178 return seastar::make_ready_future<tell_result_t>(std::move(result));
179 }
180 }
181
182 // an input_stream consumer that reads buffer into a std::string up to the first
183 // '\0' which indicates the end of command
184 struct line_consumer {
185 using tmp_buf = seastar::temporary_buffer<char>;
186 using consumption_result_type =
187 typename seastar::input_stream<char>::consumption_result_type;
188
189 seastar::future<consumption_result_type> operator()(tmp_buf&& buf) {
190 size_t consumed = 0;
191 for (auto c : buf) {
192 consumed++;
193 if (c == '\0') {
194 buf.trim_front(consumed);
195 return seastar::make_ready_future<consumption_result_type>(
196 consumption_result_type::stop_consuming_type(std::move(buf)));
197 } else {
198 line.push_back(c);
199 }
200 }
201 return seastar::make_ready_future<consumption_result_type>(
202 seastar::continue_consuming{});
203 }
204 std::string line;
205 };
206
207 seastar::future<> AdminSocket::handle_client(seastar::input_stream<char>& in,
208 seastar::output_stream<char>& out)
209 {
210 auto consumer = seastar::make_shared<line_consumer>();
211 return in.consume(*consumer).then([consumer, &out, this] {
212 logger().debug("AdminSocket::handle_client: incoming asok string: {}",
213 consumer->line);
214 return execute_line(consumer->line, out);
215 }).then([&out] {
216 return out.flush();
217 }).finally([&out] {
218 return out.close();
219 }).then([&in] {
220 return in.close();
221 }).handle_exception([](auto ep) {
222 logger().debug("exception on {}: {}", __func__, ep);
223 });
224 }
225
226 seastar::future<> AdminSocket::start(const std::string& path)
227 {
228 if (path.empty()) {
229 logger().error(
230 "{}: Admin Socket socket path missing from the configuration", __func__);
231 return seastar::now();
232 }
233
234 logger().debug("{}: asok socket path={}", __func__, path);
235 auto sock_path = seastar::socket_address{ seastar::unix_domain_addr{ path } };
236 try {
237 server_sock = seastar::engine().listen(sock_path);
238 } catch (const std::system_error& e) {
239 if (e.code() == std::errc::address_in_use) {
240 logger().debug("{}: Admin Socket socket path={} already exists, retrying",
241 __func__, path);
242 return seastar::remove_file(path).then([this, path] {
243 server_sock.reset();
244 return start(path);
245 });
246 }
247 logger().error("{}: unable to listen({}): {}", __func__, path, e.what());
248 server_sock.reset();
249 return seastar::make_ready_future<>();
250 }
251 // listen in background
252 task = seastar::keep_doing([this] {
253 return seastar::try_with_gate(stop_gate, [this] {
254 assert(!connected_sock.has_value());
255 return server_sock->accept().then([this](seastar::accept_result acc) {
256 connected_sock = std::move(acc.connection);
257 return seastar::do_with(connected_sock->input(),
258 connected_sock->output(),
259 [this](auto& input, auto& output) mutable {
260 return handle_client(input, output);
261 }).finally([this] {
262 assert(connected_sock.has_value());
263 connected_sock.reset();
264 });
265 }).handle_exception([this](auto ep) {
266 if (!stop_gate.is_closed()) {
267 logger().error("AdminSocket: terminated: {}", ep);
268 }
269 });
270 });
271 }).handle_exception_type([](const seastar::gate_closed_exception&) {
272 }).finally([path] {
273 return seastar::remove_file(path);
274 });
275 return seastar::make_ready_future<>();
276 }
277
278 seastar::future<> AdminSocket::stop()
279 {
280 if (!server_sock) {
281 return seastar::now();
282 }
283 server_sock->abort_accept();
284 if (connected_sock) {
285 connected_sock->shutdown_input();
286 connected_sock->shutdown_output();
287 }
288 return stop_gate.close().then([this] {
289 assert(task.has_value());
290 return task->then([] {
291 logger().info("AdminSocket: stopped");
292 return seastar::now();
293 });
294 });
295 }
296
297 /////////////////////////////////////////
298 // the internal hooks
299 /////////////////////////////////////////
300
301 class VersionHook final : public AdminSocketHook {
302 public:
303 VersionHook()
304 : AdminSocketHook{"version", "", "get ceph version"}
305 {}
306 seastar::future<tell_result_t> call(const cmdmap_t&,
307 std::string_view format,
308 ceph::bufferlist&&) const final
309 {
310 unique_ptr<Formatter> f{Formatter::create(format, "json-pretty", "json-pretty")};
311 f->open_object_section("version");
312 f->dump_string("version", ceph_version_to_str());
313 f->dump_string("release", ceph_release_to_str());
314 f->dump_string("release_type", ceph_release_type());
315 f->close_section();
316 return seastar::make_ready_future<tell_result_t>(std::move(f));
317 }
318 };
319
320 /**
321 Note that the git_version command is expected to return a 'version' JSON
322 segment.
323 */
324 class GitVersionHook final : public AdminSocketHook {
325 public:
326 GitVersionHook()
327 : AdminSocketHook{"git_version", "", "get git sha1"}
328 {}
329 seastar::future<tell_result_t> call(const cmdmap_t&,
330 std::string_view format,
331 ceph::bufferlist&&) const final
332 {
333 unique_ptr<Formatter> f{Formatter::create(format, "json-pretty", "json-pretty")};
334 f->open_object_section("version");
335 f->dump_string("git_version", git_version_to_str());
336 f->close_section();
337 return seastar::make_ready_future<tell_result_t>(std::move(f));
338 }
339 };
340
341 class HelpHook final : public AdminSocketHook {
342 const AdminSocket& m_as;
343
344 public:
345 explicit HelpHook(const AdminSocket& as) :
346 AdminSocketHook{"help", "", "list available commands"},
347 m_as{as}
348 {}
349
350 seastar::future<tell_result_t> call(const cmdmap_t&,
351 std::string_view format,
352 ceph::bufferlist&&) const final
353 {
354 unique_ptr<Formatter> f{Formatter::create(format,
355 "json-pretty", "json-pretty")};
356 f->open_object_section("help");
357 for (const auto& [prefix, hook] : m_as) {
358 if (!hook->help.empty()) {
359 f->dump_string(prefix.data(), hook->help);
360 }
361 }
362 f->close_section();
363 return seastar::make_ready_future<tell_result_t>(std::move(f));
364 }
365 };
366
367 class GetdescsHook final : public AdminSocketHook {
368 const AdminSocket& m_as;
369
370 public:
371 explicit GetdescsHook(const AdminSocket& as) :
372 AdminSocketHook{"get_command_descriptions",
373 "",
374 "list available commands"},
375 m_as{ as } {}
376
377 seastar::future<tell_result_t> call(const cmdmap_t& cmdmap,
378 std::string_view format,
379 ceph::bufferlist&&) const final
380 {
381 unique_ptr<Formatter> f{Formatter::create(format, "json-pretty", "json-pretty")};
382 int cmdnum = 0;
383 f->open_object_section("command_descriptions");
384 for (const auto& [prefix, hook] : m_as) {
385 auto secname = fmt::format("cmd {:>03}", cmdnum);
386 auto cmd = fmt::format("{} {}", hook->prefix, hook->desc);
387 dump_cmd_and_help_to_json(f.get(), CEPH_FEATURES_ALL, secname,
388 cmd, std::string{hook->help});
389 cmdnum++;
390 }
391 f->close_section();
392 return seastar::make_ready_future<tell_result_t>(std::move(f));
393 }
394 };
395
396 class InjectArgsHook final : public AdminSocketHook {
397 public:
398 InjectArgsHook()
399 : AdminSocketHook{"injectargs",
400 "name=injected_args,type=CephString,n=N",
401 "inject configuration arguments into running daemon"}
402 {}
403 seastar::future<tell_result_t> call(const cmdmap_t& cmdmap,
404 std::string_view format,
405 ceph::bufferlist&&) const final
406 {
407 std::vector<std::string> argv;
408 if (!cmd_getval(cmdmap, "injected_args", argv)) {
409 return seastar::make_ready_future<tell_result_t>();
410 }
411 const std::string args = boost::algorithm::join(argv, " ");
412 return local_conf().inject_args(args).then([] {
413 return seastar::make_ready_future<tell_result_t>();
414 }).handle_exception_type([] (const std::invalid_argument& e) {
415 return seastar::make_ready_future<tell_result_t>(
416 tell_result_t{-EINVAL, e.what()});
417 });
418 }
419 };
420
421 /**
422 * listing the configuration values
423 */
424 class ConfigShowHook : public AdminSocketHook {
425 public:
426 ConfigShowHook() :
427 AdminSocketHook{"config show",
428 "",
429 "dump current config settings"}
430 {}
431 seastar::future<tell_result_t> call(const cmdmap_t&,
432 std::string_view format,
433 ceph::bufferlist&& input) const final
434 {
435 unique_ptr<Formatter> f{Formatter::create(format, "json-pretty", "json-pretty")};
436 f->open_object_section("config_show");
437 local_conf().show_config(f.get());
438 f->close_section();
439 return seastar::make_ready_future<tell_result_t>(std::move(f));
440 }
441 };
442
443 /**
444 * fetching the value of a specific configuration item
445 */
446 class ConfigGetHook : public AdminSocketHook {
447 public:
448 ConfigGetHook() :
449 AdminSocketHook("config get",
450 "name=var,type=CephString",
451 "config get <field>: get the config value")
452 {}
453 seastar::future<tell_result_t> call(const cmdmap_t& cmdmap,
454 std::string_view format,
455 ceph::bufferlist&& input) const final
456 {
457 std::string var;
458 [[maybe_unused]] bool found = cmd_getval(cmdmap, "var", var);
459 assert(found);
460 std::string conf_val;
461 if (int r = local_conf().get_val(var, &conf_val); r < 0) {
462 return seastar::make_ready_future<tell_result_t>(
463 tell_result_t{r, fmt::format("error getting {}: {}",
464 var, cpp_strerror(r))});
465 }
466 unique_ptr<Formatter> f{Formatter::create(format,
467 "json-pretty",
468 "json-pretty")};
469 f->open_object_section("config_get");
470 f->dump_string(var, conf_val);
471 f->close_section();
472 return seastar::make_ready_future<tell_result_t>(std::move(f));
473 }
474 };
475
476 /**
477 * setting the value of a specific configuration item (an example:
478 * {"prefix": "config set", "var":"debug_osd", "val": ["30/20"]} )
479 */
480 class ConfigSetHook : public AdminSocketHook {
481 public:
482 ConfigSetHook()
483 : AdminSocketHook("config set",
484 "name=var,type=CephString "
485 "name=val,type=CephString,n=N",
486 "config set <field> <val> [<val> ...]: set a config variable")
487 {}
488 seastar::future<tell_result_t> call(const cmdmap_t& cmdmap,
489 std::string_view format,
490 ceph::bufferlist&&) const final
491 {
492 std::string var;
493 std::vector<std::string> new_val;
494 cmd_getval(cmdmap, "var", var);
495 cmd_getval(cmdmap, "val", new_val);
496 // val may be multiple words
497 const std::string joined_values = boost::algorithm::join(new_val, " ");
498 return local_conf().set_val(var, joined_values).then([format] {
499 unique_ptr<Formatter> f{Formatter::create(format, "json-pretty", "json-pretty")};
500 f->open_object_section("config_set");
501 f->dump_string("success", "");
502 f->close_section();
503 return seastar::make_ready_future<tell_result_t>(std::move(f));
504 }).handle_exception_type([](std::invalid_argument& e) {
505 return seastar::make_ready_future<tell_result_t>(
506 tell_result_t{-EINVAL, e.what()});
507 });
508 }
509 };
510
511 /**
512 * listing the configuration values
513 */
514 class ConfigHelpHook : public AdminSocketHook {
515 public:
516 ConfigHelpHook() :
517 AdminSocketHook{"config help",
518 "",
519 "get config setting schema and descriptions"}
520 {}
521 seastar::future<tell_result_t> call(const cmdmap_t&,
522 std::string_view format,
523 ceph::bufferlist&& input) const final
524 {
525 unique_ptr<Formatter> f{Formatter::create(format, "json-pretty", "json-pretty")};
526 // Output all
527 f->open_array_section("options");
528 for (const auto &option : ceph_options) {
529 f->dump_object("option", option);
530 }
531 f->close_section();
532 return seastar::make_ready_future<tell_result_t>(std::move(f));
533 }
534 };
535
536 /// the hooks that are served directly by the admin_socket server
537 void AdminSocket::register_admin_commands()
538 {
539 register_command(std::make_unique<VersionHook>());
540 register_command(std::make_unique<GitVersionHook>());
541 register_command(std::make_unique<HelpHook>(*this));
542 register_command(std::make_unique<GetdescsHook>(*this));
543 register_command(std::make_unique<ConfigGetHook>());
544 register_command(std::make_unique<ConfigSetHook>());
545 register_command(std::make_unique<ConfigShowHook>());
546 register_command(std::make_unique<ConfigHelpHook>());
547 register_command(std::make_unique<InjectArgsHook>());
548 }
549
550 } // namespace crimson::admin