From 77937ed44162b3e6cdc53b644649a68640cd4a64 Mon Sep 17 00:00:00 2001 From: Jason Dillaman Date: Fri, 18 Sep 2015 16:24:20 -0400 Subject: [PATCH] rbd: support libraries for switching CLI processing to boost Added new classes for registering CLI commands and associated arguments and dynamically generating help messages. Signed-off-by: Jason Dillaman --- src/CMakeLists.txt | 3 + src/tools/Makefile-client.am | 13 +- src/tools/rbd/IndentStream.cc | 59 +++++++ src/tools/rbd/IndentStream.h | 60 +++++++ src/tools/rbd/OptionPrinter.cc | 105 ++++++++++++ src/tools/rbd/OptionPrinter.h | 40 +++++ src/tools/rbd/Shell.cc | 293 +++++++++++++++++++++++++++++++++ src/tools/rbd/Shell.h | 71 ++++++++ 8 files changed, 642 insertions(+), 2 deletions(-) create mode 100644 src/tools/rbd/IndentStream.cc create mode 100644 src/tools/rbd/IndentStream.h create mode 100644 src/tools/rbd/OptionPrinter.cc create mode 100644 src/tools/rbd/OptionPrinter.h create mode 100644 src/tools/rbd/Shell.cc create mode 100644 src/tools/rbd/Shell.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 424a2c76c6f09..dbba59626f11a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -876,6 +876,9 @@ if(${WITH_RBD}) install(TARGETS librados librbd DESTINATION lib) set(rbd_srcs tools/rbd/rbd.cc + tools/rbd/IndentStream.cc + tools/rbd/OptionPrinter.cc + tools/rbd/Shell.cc common/TextTable.cc) add_executable(rbd ${rbd_srcs} $ $ diff --git a/src/tools/Makefile-client.am b/src/tools/Makefile-client.am index aa635d4612a8c..6fd74da251338 100644 --- a/src/tools/Makefile-client.am +++ b/src/tools/Makefile-client.am @@ -24,8 +24,17 @@ bin_PROGRAMS += rados if WITH_RBD rbd_SOURCES = \ - tools/rbd/rbd.cc -rbd_LDADD = $(LIBKRBD) $(LIBRBD) $(LIBRADOS) $(CEPH_GLOBAL) + tools/rbd/rbd.cc \ + tools/rbd/IndentStream.cc \ + tools/rbd/OptionPrinter.cc \ + tools/rbd/Shell.cc +noinst_HEADERS += \ + tools/rbd/IndentStream.h \ + tools/rbd/OptionPrinter.h \ + tools/rbd/Shell.h +rbd_LDADD = \ + $(LIBKRBD) $(LIBRBD) $(LIBRADOS) $(CEPH_GLOBAL) \ + $(BOOST_PROGRAM_OPTIONS_LIBS) if LINUX bin_PROGRAMS += rbd endif # LINUX diff --git a/src/tools/rbd/IndentStream.cc b/src/tools/rbd/IndentStream.cc new file mode 100644 index 0000000000000..83591a8cb2ebd --- /dev/null +++ b/src/tools/rbd/IndentStream.cc @@ -0,0 +1,59 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "tools/rbd/IndentStream.h" + +namespace rbd { + +int IndentBuffer::overflow (int c) { + if (traits_type::eq_int_type(traits_type::eof(), c)) { + return traits_type::not_eof(c); + } + + int r; + switch (c) { + case '\n': + m_buffer += c; + flush_line(); + r = m_streambuf->sputn(m_buffer.c_str(), m_buffer.size()); + m_buffer.clear(); + return r; + case '\t': + // convert tab to single space and fall-through + c = ' '; + default: + if (m_indent + m_buffer.size() >= m_line_length) { + size_t word_offset = m_buffer.find_last_of(m_delim); + bool space_delim = (m_delim == " "); + if (word_offset == std::string::npos && !space_delim) { + word_offset = m_buffer.find_last_of(" "); + } + + if (word_offset != std::string::npos) { + flush_line(); + m_streambuf->sputn(m_buffer.c_str(), word_offset); + m_buffer = std::string(m_buffer, + word_offset + (space_delim ? 1 : 0)); + } else { + flush_line(); + m_streambuf->sputn(m_buffer.c_str(), m_buffer.size()); + m_buffer.clear(); + } + m_streambuf->sputc('\n'); + } + m_buffer += c; + return c; + } +} + +void IndentBuffer::flush_line() { + if (m_initial_offset >= m_indent) { + m_initial_offset = 0; + m_streambuf->sputc('\n'); + } + + m_streambuf->sputn(m_indent_prefix.c_str(), m_indent - m_initial_offset); + m_initial_offset = 0; +} + +} // namespace rbd diff --git a/src/tools/rbd/IndentStream.h b/src/tools/rbd/IndentStream.h new file mode 100644 index 0000000000000..ba7d90bc96b0c --- /dev/null +++ b/src/tools/rbd/IndentStream.h @@ -0,0 +1,60 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#ifndef CEPH_RBD_INDENT_STREAM_H +#define CEPH_RBD_INDENT_STREAM_H + +#include "include/int_types.h" +#include +#include +#include + +namespace rbd { + +class IndentBuffer : public std::streambuf { +public: + IndentBuffer(size_t indent, size_t initial_offset, size_t line_length, + std::streambuf *streambuf) + : m_indent(indent), m_initial_offset(initial_offset), + m_line_length(line_length), m_streambuf(streambuf), + m_delim(" "), m_indent_prefix(m_indent, ' ') { + } + + void set_delimiter(const std::string &delim) { + m_delim = delim; + } + +protected: + virtual int overflow (int c); + +private: + size_t m_indent; + size_t m_initial_offset; + size_t m_line_length; + std::streambuf *m_streambuf; + + std::string m_delim; + std::string m_indent_prefix; + std::string m_buffer; + + void flush_line(); +}; + +class IndentStream : public std::ostream { +public: + IndentStream(size_t indent, size_t initial_offset, size_t line_length, + std::ostream &os) + : std::ostream(&m_indent_buffer), + m_indent_buffer(indent, initial_offset, line_length, os.rdbuf()) { + } + + void set_delimiter(const std::string &delim) { + m_indent_buffer.set_delimiter(delim); + } +private: + IndentBuffer m_indent_buffer; +}; + +} // namespace rbd + +#endif // CEPH_RBD_INDENT_STREAM_ITERATOR_H diff --git a/src/tools/rbd/OptionPrinter.cc b/src/tools/rbd/OptionPrinter.cc new file mode 100644 index 0000000000000..a0c6049682b19 --- /dev/null +++ b/src/tools/rbd/OptionPrinter.cc @@ -0,0 +1,105 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "tools/rbd/OptionPrinter.h" +#include "tools/rbd/IndentStream.h" + +namespace rbd { + +namespace po = boost::program_options; + +const std::string OptionPrinter::POSITIONAL_ARGUMENTS("Positional arguments"); +const std::string OptionPrinter::OPTIONAL_ARGUMENTS("Optional arguments"); + +OptionPrinter::OptionPrinter(const OptionsDescription &positional, + const OptionsDescription &optional) + : m_positional(positional), m_optional(optional) { +} + +void OptionPrinter::print_short(std::ostream &os, size_t initial_offset) { + size_t name_width = std::min(initial_offset, MAX_DESCRIPTION_OFFSET) + 1; + + IndentStream indent_stream(name_width, initial_offset, LINE_WIDTH, os); + indent_stream.set_delimiter("["); + for (size_t i = 0; i < m_optional.options().size(); ++i) { + bool required = m_optional.options()[i]->semantic()->is_required(); + if (!required) { + indent_stream << "["; + } + indent_stream << "--" << m_optional.options()[i]->long_name(); + if (m_optional.options()[i]->semantic()->max_tokens() != 0) { + indent_stream << " <" << m_optional.options()[i]->long_name() << ">"; + } + if (!required) { + indent_stream << "]"; + } + indent_stream << " "; + } + indent_stream << std::endl; + + if (m_positional.options().size() > 0) { + indent_stream.set_delimiter(" "); + for (size_t i = 0; i < m_positional.options().size(); ++i) { + indent_stream << "<" << m_positional.options()[i]->long_name() << "> "; + if (m_positional.options()[i]->semantic()->max_tokens() > 1) { + indent_stream << "[<" << m_positional.options()[i]->long_name() + << "> ...]"; + break; + } + } + indent_stream << std::endl; + } +} + +void OptionPrinter::print_detailed(std::ostream &os) { + std::string indent_prefix(2, ' '); + size_t name_width = compute_name_width(indent_prefix.size()); + + if (m_positional.options().size() > 0) { + std::cout << POSITIONAL_ARGUMENTS << std::endl; + for (size_t i = 0; i < m_positional.options().size(); ++i) { + std::stringstream ss; + ss << indent_prefix << "<" << m_positional.options()[i]->long_name() + << ">"; + + std::cout << ss.str(); + IndentStream indent_stream(name_width, ss.str().size(), LINE_WIDTH, os); + indent_stream << m_positional.options()[i]->description() << std::endl; + } + std::cout << std::endl; + } + + if (m_optional.options().size() > 0) { + std::cout << OPTIONAL_ARGUMENTS << std::endl; + for (size_t i = 0; i < m_optional.options().size(); ++i) { + std::stringstream ss; + ss << indent_prefix + << m_optional.options()[i]->format_name() << " " + << m_optional.options()[i]->format_parameter(); + + std::cout << ss.str(); + IndentStream indent_stream(name_width, ss.str().size(), LINE_WIDTH, os); + indent_stream << m_optional.options()[i]->description() << std::endl; + } + std::cout << std::endl; + } +} + +size_t OptionPrinter::compute_name_width(size_t indent) { + size_t width = MIN_NAME_WIDTH; + std::vector descs = {m_positional, m_optional}; + for (size_t desc_idx = 0; desc_idx < descs.size(); ++desc_idx) { + const OptionsDescription &desc = descs[desc_idx]; + for (size_t opt_idx = 0; opt_idx < desc.options().size(); ++opt_idx) { + size_t name_width = desc.options()[opt_idx]->format_name().size() + + desc.options()[opt_idx]->format_parameter().size() + + 1; + width = std::max(width, name_width); + } + } + width += indent; + width = std::min(width, MAX_DESCRIPTION_OFFSET) + 1; + return width; +} + +} // namespace rbd diff --git a/src/tools/rbd/OptionPrinter.h b/src/tools/rbd/OptionPrinter.h new file mode 100644 index 0000000000000..e18a5f88ec6df --- /dev/null +++ b/src/tools/rbd/OptionPrinter.h @@ -0,0 +1,40 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#ifndef CEPH_RBD_OPTION_PRINTER_H +#define CEPH_RBD_OPTION_PRINTER_H + +#include "include/int_types.h" +#include +#include +#include + +namespace rbd { + +class OptionPrinter { +public: + typedef boost::program_options::options_description OptionsDescription; + + static const std::string POSITIONAL_ARGUMENTS; + static const std::string OPTIONAL_ARGUMENTS; + + static const size_t LINE_WIDTH = 80; + static const size_t MIN_NAME_WIDTH = 20; + static const size_t MAX_DESCRIPTION_OFFSET = LINE_WIDTH / 2; + + OptionPrinter(const OptionsDescription &positional, + const OptionsDescription &optional); + + void print_short(std::ostream &os, size_t initial_offset); + void print_detailed(std::ostream &os); + +private: + const OptionsDescription &m_positional; + const OptionsDescription &m_optional; + + size_t compute_name_width(size_t indent); +}; + +} // namespace rbd + +#endif // CEPH_RBD_OPTION_PRINTER_H diff --git a/src/tools/rbd/Shell.cc b/src/tools/rbd/Shell.cc new file mode 100644 index 0000000000000..7bb0a89d677e0 --- /dev/null +++ b/src/tools/rbd/Shell.cc @@ -0,0 +1,293 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include "tools/rbd/Shell.h" +#include "tools/rbd/IndentStream.h" +#include "tools/rbd/OptionPrinter.h" +#include "common/config.h" +#include "global/global_context.h" +#include "include/stringify.h" +#include +#include +#include + +namespace rbd { + +namespace po = boost::program_options; + +namespace { + +struct Secret {}; + +void validate(boost::any& v, const std::vector& values, + Secret *target_type, int) { + std::cerr << "rbd: --secret is deprecated, use --keyfile" << std::endl; + + po::validators::check_first_occurrence(v); + const std::string &s = po::validators::get_single_string(values); + int r = g_conf->set_val("keyfile", s.c_str()); + assert(r == 0); + v = boost::any(s); +} + +std::string base_name(const std::string &path, + const std::string &delims = "/\\") { + return path.substr(path.find_last_of(delims) + 1); +} + +std::string format_command_spec(const Shell::CommandSpec &spec) { + return joinify(spec.begin(), spec.end(), " "); +} + +std::string format_command_name(const Shell::CommandSpec &spec, + const Shell::CommandSpec &alias_spec) { + std::string name = format_command_spec(spec); + if (!alias_spec.empty()) { + name += " (" + format_command_spec(alias_spec) + ")"; + } + return name; +} + +} // anonymous namespace + +std::vector Shell::s_actions; +std::set Shell::s_switch_arguments; + +int Shell::execute(int arg_count, const char **arg_values) { + std::string app_name(base_name(arg_values[0])); + std::vector command_spec; + get_command_spec(arg_count, arg_values, &command_spec); + + if (command_spec.empty() || command_spec == CommandSpec({"help"})) { + // list all available actions + print_help(app_name); + return 0; + } else if (command_spec[0] == "help") { + // list help for specific action + command_spec.erase(command_spec.begin()); + Action *action = find_action(command_spec, NULL); + if (action == NULL) { + print_unknown_action(app_name, command_spec); + return EXIT_FAILURE; + } else { + print_action_help(app_name, action); + return 0; + } + } + + CommandSpec *matching_spec; + Action *action = find_action(command_spec, &matching_spec); + if (action == NULL) { + print_unknown_action(app_name, command_spec); + return EXIT_FAILURE; + } + + po::variables_map vm; + try { + po::options_description positional; + po::options_description options; + (*action->get_arguments)(&positional, &options); + + // dynamically allocate options for our command (e.g. snap list) and + // its associated positional arguments + po::options_description arguments; + arguments.add_options() + ("positional-command-spec", + po::value >()->required(), "") + ("positional-arguments", + po::value >(), ""); + + po::positional_options_description positional_options; + positional_options.add("positional-command-spec", + matching_spec->size()); + if (command_spec.size() > matching_spec->size()) { + positional_options.add("positional-arguments", -1); + } + + po::options_description global; + get_global_options(&global); + + po::options_description group; + group.add(options).add(arguments).add(global); + + po::store(po::command_line_parser(arg_count, arg_values) + .style(po::command_line_style::default_style & + ~po::command_line_style::allow_guessing) + .options(group) + .positional(positional_options) + .allow_unregistered() + .run(), vm); + + if (vm["positional-command-spec"].as >() != + *matching_spec) { + std::cerr << "rbd: failed to parse command" << std::endl; + return EXIT_FAILURE; + } + + po::notify(vm); + + int r = (*action->execute)(vm); + if (r != 0) { + return std::abs(r); + } + } catch (po::required_option& e) { + std::cerr << "rbd: " << e.what() << std::endl << std::endl; + return EXIT_FAILURE; + } catch (po::too_many_positional_options_error& e) { + std::cerr << "rbd: too many positional arguments or unrecognized optional " + << "argument" << std::endl; + } catch (po::error& e) { + std::cerr << "rbd: " << e.what() << std::endl << std::endl; + return EXIT_FAILURE; + } + + return 0; +} + +void Shell::get_command_spec(int arg_count, const char **arg_values, + std::vector *command_spec) { + for (int i = 1; i < arg_count; ++i) { + std::string arg(arg_values[i]); + if (arg == "-h" || arg == "--help") { + *command_spec = {"help"}; + return; + } else if (arg[0] == '-') { + // if the option is not a switch, skip its value + if (arg.size() >= 2 && + (arg[1] == '-' || s_switch_arguments.count(arg.substr(1, 1)) == 0) && + (arg[1] != '-' || + s_switch_arguments.count(arg.substr(2, std::string::npos)) == 0) && + arg.find('=') == std::string::npos) { + ++i; + } + } else { + command_spec->push_back(arg); + } + } +} + +Shell::Action *Shell::find_action(const CommandSpec &command_spec, + CommandSpec **matching_spec) { + for (size_t i = 0; i < s_actions.size(); ++i) { + Action *action = s_actions[i]; + if (action->command_spec.size() <= command_spec.size()) { + if (std::includes(action->command_spec.begin(), + action->command_spec.end(), + command_spec.begin(), + command_spec.begin() + action->command_spec.size())) { + if (matching_spec != NULL) { + *matching_spec = &action->command_spec; + } + return action; + } + } + if (!action->alias_command_spec.empty() && + action->alias_command_spec.size() <= command_spec.size()) { + if (std::includes(action->alias_command_spec.begin(), + action->alias_command_spec.end(), + command_spec.begin(), + command_spec.begin() + + action->alias_command_spec.size())) { + if (matching_spec != NULL) { + *matching_spec = &action->alias_command_spec; + } + return action; + } + } + } + return NULL; +} + +void Shell::get_global_options(po::options_description *opts) { + opts->add_options() + ("conf,c", "path to cluster configuration") + ("cluster", "cluster name") + ("id,i", "client id (without 'client.' prefix)") + ("name,n", "client name") + ("secret", po::value(), "path to secret key (deprecated)") + ("keyfile", "path to secret key") + ("keyring", "path to keyring"); +} + +void Shell::print_help(const std::string &app_name) { + std::cout << "usage: " << app_name << " ..." + << std::endl << std::endl + << "Command-line interface for managing Ceph RBD images." + << std::endl << std::endl; + + std::vector actions(s_actions); + std::sort(actions.begin(), actions.end(), + [](Action *lhs, Action *rhs) { return lhs->command_spec < + rhs->command_spec; }); + + std::cout << OptionPrinter::POSITIONAL_ARGUMENTS << ":" << std::endl + << " " << std::endl; + + // since the commands have spaces, we have to build our own formatter + std::string indent(4, ' '); + size_t name_width = OptionPrinter::MIN_NAME_WIDTH; + for (size_t i = 0; i < actions.size(); ++i) { + Action *action = actions[i]; + std::string name = format_command_name(action->command_spec, + action->alias_command_spec); + name_width = std::max(name_width, name.size()); + } + name_width += indent.size(); + name_width = std::min(name_width, OptionPrinter::MAX_DESCRIPTION_OFFSET) + 1; + + for (size_t i = 0; i < actions.size(); ++i) { + Action *action = actions[i]; + std::stringstream ss; + ss << indent + << format_command_name(action->command_spec, action->alias_command_spec); + + std::cout << ss.str(); + if (!action->description.empty()) { + IndentStream indent_stream(name_width, ss.str().size(), + OptionPrinter::LINE_WIDTH, + std::cout); + indent_stream << action->description << std::endl; + } else { + std::cout << std::endl; + } + } + + po::options_description global_opts(OptionPrinter::OPTIONAL_ARGUMENTS); + get_global_options(&global_opts); + std::cout << std::endl << global_opts << std::endl + << "See '" << app_name << " help ' for help on a specific " + << "command." << std::endl; +} + +void Shell::print_action_help(const std::string &app_name, Action *action) { + + std::stringstream ss; + ss << "usage: " << app_name << " " + << format_command_spec(action->command_spec); + std::cout << ss.str(); + + po::options_description positional; + po::options_description options; + (*action->get_arguments)(&positional, &options); + + OptionPrinter option_printer(positional, options); + option_printer.print_short(std::cout, ss.str().size()); + + if (!action->description.empty()) { + std::cout << std::endl << action->description << std::endl; + } + + std::cout << std::endl; + option_printer.print_detailed(std::cout); +} + +void Shell::print_unknown_action(const std::string &app_name, + const std::vector &command_spec) { + std::cerr << "error: unknown option '" + << joinify(command_spec.begin(), + command_spec.end(), " ") << "'" + << std::endl << std::endl; + print_help(app_name); +} + +} // namespace rbd diff --git a/src/tools/rbd/Shell.h b/src/tools/rbd/Shell.h new file mode 100644 index 0000000000000..a4502af589c37 --- /dev/null +++ b/src/tools/rbd/Shell.h @@ -0,0 +1,71 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#ifndef CEPH_RBD_SHELL_H +#define CEPH_RBD_SHELL_H + +#include "include/int_types.h" +#include +#include +#include +#include + +namespace rbd { + +class Shell { +public: + typedef std::vector CommandSpec; + + struct Action { + typedef void (*GetArguments)(boost::program_options::options_description *, + boost::program_options::options_description *); + typedef int (*Execute)(const boost::program_options::variables_map &); + + CommandSpec command_spec; + CommandSpec alias_command_spec; + const std::string description; + const std::string help; + GetArguments get_arguments; + Execute execute; + + template + Action(const std::initializer_list &command_spec, + const std::initializer_list &alias_command_spec, + const std::string &description, const std::string &help, + Args args, Execute execute) + : command_spec(command_spec), alias_command_spec(alias_command_spec), + description(description), help(help), get_arguments(args), + execute(execute) { + Shell::s_actions.push_back(this); + } + + }; + + struct SwitchArguments { + SwitchArguments(const std::initializer_list &arguments) { + Shell::s_switch_arguments.insert(arguments.begin(), arguments.end()); + } + }; + + int execute(int arg_count, const char **arg_values); + +private: + static std::vector s_actions; + static std::set s_switch_arguments; + + void get_command_spec(int arg_count, const char **arg_values, + std::vector *command_spec); + Action *find_action(const CommandSpec &command_spec, + CommandSpec **matching_spec); + + void get_global_options(boost::program_options::options_description *opts); + + void print_help(const std::string &app_name); + void print_action_help(const std::string &app_name, Action *action); + void print_unknown_action(const std::string &app_name, + const std::vector &command_spec); +}; + +} // namespace rbd + +#endif // CEPH_RBD_SHELL_H -- 2.39.5