diff options
Diffstat (limited to 'src/common/cmdparse.cc')
-rw-r--r-- | src/common/cmdparse.cc | 738 |
1 files changed, 738 insertions, 0 deletions
diff --git a/src/common/cmdparse.cc b/src/common/cmdparse.cc new file mode 100644 index 000000000..009e6678c --- /dev/null +++ b/src/common/cmdparse.cc @@ -0,0 +1,738 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab +/* + * Ceph - scalable distributed file system + * + * Copyright (C) 2013 Inktank Storage, Inc. + * + * This is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License version 2, as published by the Free Software + * Foundation. See file COPYING. + * + */ + +#include "include/common_fwd.h" +#include "common/cmdparse.h" +#include "common/Formatter.h" +#include "common/debug.h" +#include "common/strtol.h" +#include "json_spirit/json_spirit.h" + +using std::is_same_v; +using std::ostringstream; +using std::string; +using std::stringstream; +using std::string_view; +using std::vector; + +using namespace std::literals; + +/** + * Given a cmddesc like "foo baz name=bar,type=CephString", + * return the prefix "foo baz". + */ +namespace ceph::common { +std::string cmddesc_get_prefix(const std::string_view &cmddesc) +{ + string tmp(cmddesc); // FIXME: stringstream ctor can't take string_view :( + stringstream ss(tmp); + std::string word; + std::ostringstream result; + bool first = true; + while (std::getline(ss, word, ' ')) { + if (word.find_first_of(",=") != string::npos) { + break; + } + + if (!first) { + result << " "; + } + result << word; + first = false; + } + + return result.str(); +} + +using arg_desc_t = std::map<std::string_view, std::string_view>; + +// Snarf up all the key=val,key=val pairs, put 'em in a dict. +arg_desc_t cmddesc_get_args(const string_view cmddesc) +{ + arg_desc_t arg_desc; + for_each_substr(cmddesc, ",", [&](auto kv) { + // key=value; key by itself implies value is bool true + // name="name" means arg dict will be titled 'name' + auto equal = kv.find('='); + if (equal == kv.npos) { + // it should be the command + return; + } + auto key = kv.substr(0, equal); + auto val = kv.substr(equal + 1); + arg_desc[key] = val; + }); + return arg_desc; +} + +std::string cmddesc_get_prenautilus_compat(const std::string &cmddesc) +{ + std::vector<std::string> out; + stringstream ss(cmddesc); + std::string word; + bool changed = false; + while (std::getline(ss, word, ' ')) { + // if no , or =, must be a plain word to put out + if (word.find_first_of(",=") == string::npos) { + out.push_back(word); + continue; + } + auto desckv = cmddesc_get_args(word); + auto j = desckv.find("type"); + if (j != desckv.end() && j->second == "CephBool") { + // Instruct legacy clients or mons to send --foo-bar string in place + // of a 'true'/'false' value + std::ostringstream oss; + oss << "--" << desckv["name"]; + std::string val = oss.str(); + std::replace(val.begin(), val.end(), '_', '-'); + desckv["type"] = "CephChoices"; + desckv["strings"] = val; + std::ostringstream fss; + for (auto k = desckv.begin(); k != desckv.end(); ++k) { + if (k != desckv.begin()) { + fss << ","; + } + fss << k->first << "=" << k->second; + } + out.push_back(fss.str()); + changed = true; + } else { + out.push_back(word); + } + } + if (!changed) { + return cmddesc; + } + std::string o; + for (auto i = out.begin(); i != out.end(); ++i) { + if (i != out.begin()) { + o += " "; + } + o += *i; + } + return o; +} + +/** + * Read a command description list out of cmd, and dump it to f. + * A signature description is a set of space-separated words; + * see MonCommands.h for more info. + */ + +void +dump_cmd_to_json(Formatter *f, uint64_t features, const string& cmd) +{ + // put whole command signature in an already-opened container + // elements are: "name", meaning "the typeless name that means a literal" + // an object {} with key:value pairs representing an argument + + stringstream ss(cmd); + std::string word; + bool positional = true; + + while (std::getline(ss, word, ' ')) { + if (word == "--") { + positional = false; + continue; + } + + // if no , or =, must be a plain word to put out + if (word.find_first_of(",=") == string::npos) { + f->dump_string("arg", word); + continue; + } + + // accumulate descriptor keywords in desckv + auto desckv = cmddesc_get_args(word); + // name the individual desc object based on the name key + f->open_object_section(desckv["name"]); + + // Compatibility for pre-nautilus clients that don't know about CephBool + std::string val; + if (!HAVE_FEATURE(features, SERVER_NAUTILUS)) { + auto i = desckv.find("type"); + if (i != desckv.end() && i->second == "CephBool") { + // Instruct legacy clients to send --foo-bar string in place + // of a 'true'/'false' value + std::ostringstream oss; + oss << "--" << desckv["name"]; + val = oss.str(); + std::replace(val.begin(), val.end(), '_', '-'); + + desckv["type"] = "CephChoices"; + desckv["strings"] = val; + } + } + + // dump all the keys including name into the array + if (!positional) { + desckv["positional"] = "false"; + } + for (auto [key, value] : desckv) { + if (key == "positional") { + if (!HAVE_FEATURE(features, SERVER_QUINCY)) { + continue; + } + f->dump_bool(key, value == "true" || value == "True"); + } else if (key == "req" && HAVE_FEATURE(features, SERVER_QUINCY)) { + f->dump_bool(key, value == "true" || value == "True"); + } else { + f->dump_string(key, value); + } + } + f->close_section(); // attribute object for individual desc + } +} + +void +dump_cmd_and_help_to_json(Formatter *jf, + uint64_t features, + const string& secname, + const string& cmdsig, + const string& helptext) +{ + jf->open_object_section(secname); + jf->open_array_section("sig"); + dump_cmd_to_json(jf, features, cmdsig); + jf->close_section(); // sig array + jf->dump_string("help", helptext); + jf->close_section(); // cmd +} + +void +dump_cmddesc_to_json(Formatter *jf, + uint64_t features, + const string& secname, + const string& cmdsig, + const string& helptext, + const string& module, + const string& perm, + uint64_t flags) +{ + jf->open_object_section(secname); + jf->open_array_section("sig"); + dump_cmd_to_json(jf, features, cmdsig); + jf->close_section(); // sig array + jf->dump_string("help", helptext); + jf->dump_string("module", module); + jf->dump_string("perm", perm); + jf->dump_int("flags", flags); + jf->close_section(); // cmd +} + +void cmdmap_dump(const cmdmap_t &cmdmap, Formatter *f) +{ + ceph_assert(f != nullptr); + + class dump_visitor : public boost::static_visitor<void> + { + Formatter *f; + std::string const &key; + public: + dump_visitor(Formatter *f_, std::string const &key_) + : f(f_), key(key_) + { + } + + void operator()(const std::string &operand) const + { + f->dump_string(key, operand); + } + + void operator()(const bool &operand) const + { + f->dump_bool(key, operand); + } + + void operator()(const int64_t &operand) const + { + f->dump_int(key, operand); + } + + void operator()(const double &operand) const + { + f->dump_float(key, operand); + } + + void operator()(const std::vector<std::string> &operand) const + { + f->open_array_section(key); + for (const auto& i : operand) { + f->dump_string("item", i); + } + f->close_section(); + } + + void operator()(const std::vector<int64_t> &operand) const + { + f->open_array_section(key); + for (const auto i : operand) { + f->dump_int("item", i); + } + f->close_section(); + } + + void operator()(const std::vector<double> &operand) const + { + f->open_array_section(key); + for (const auto i : operand) { + f->dump_float("item", i); + } + f->close_section(); + } + }; + + //f->open_object_section("cmdmap"); + for (const auto &i : cmdmap) { + boost::apply_visitor(dump_visitor(f, i.first), i.second); + } + //f->close_section(); +} + + +/** Parse JSON in vector cmd into a map from field to map of values + * (use mValue/mObject) + * 'cmd' should not disappear over lifetime of map + * 'mapp' points to the caller's map + * 'ss' captures any errors during JSON parsing; if function returns + * false, ss is valid */ + +bool +cmdmap_from_json(const vector<string>& cmd, cmdmap_t *mapp, std::ostream& ss) +{ + json_spirit::mValue v; + + string fullcmd; + // First, join all cmd strings + for (auto& c : cmd) + fullcmd += c; + + try { + if (!json_spirit::read(fullcmd, v)) + throw std::runtime_error("unparseable JSON " + fullcmd); + if (v.type() != json_spirit::obj_type) + throw std::runtime_error("not JSON object " + fullcmd); + + // allocate new mObject (map) to return + // make sure all contents are simple types (not arrays or objects) + json_spirit::mObject o = v.get_obj(); + for (auto it = o.begin(); it != o.end(); ++it) { + + // ok, marshal it into our string->cmd_vartype map, or throw an + // exception if it's not a simple datatype. This is kind of + // annoying, since json_spirit has a boost::variant inside it + // already, but it's not public. Oh well. + + switch (it->second.type()) { + + case json_spirit::obj_type: + default: + throw std::runtime_error("JSON array/object not allowed " + fullcmd); + break; + + case json_spirit::array_type: + { + // array is a vector of values. Unpack it to a vector + // of strings, doubles, or int64_t, the only types we handle. + const vector<json_spirit::mValue>& spvals = it->second.get_array(); + if (spvals.empty()) { + // if an empty array is acceptable, the caller should always check for + // vector<string> if the expected value of "vector<int64_t>" in the + // cmdmap is missing. + (*mapp)[it->first] = vector<string>(); + } else if (spvals.front().type() == json_spirit::str_type) { + vector<string> outv; + for (const auto& sv : spvals) { + if (sv.type() != json_spirit::str_type) { + throw std::runtime_error("Can't handle arrays of multiple types"); + } + outv.push_back(sv.get_str()); + } + (*mapp)[it->first] = std::move(outv); + } else if (spvals.front().type() == json_spirit::int_type) { + vector<int64_t> outv; + for (const auto& sv : spvals) { + if (spvals.front().type() != json_spirit::int_type) { + throw std::runtime_error("Can't handle arrays of multiple types"); + } + outv.push_back(sv.get_int64()); + } + (*mapp)[it->first] = std::move(outv); + } else if (spvals.front().type() == json_spirit::real_type) { + vector<double> outv; + for (const auto& sv : spvals) { + if (spvals.front().type() != json_spirit::real_type) { + throw std::runtime_error("Can't handle arrays of multiple types"); + } + outv.push_back(sv.get_real()); + } + (*mapp)[it->first] = std::move(outv); + } else { + throw std::runtime_error("Can't handle arrays of types other than " + "int, string, or double"); + } + } + break; + case json_spirit::str_type: + (*mapp)[it->first] = it->second.get_str(); + break; + + case json_spirit::bool_type: + (*mapp)[it->first] = it->second.get_bool(); + break; + + case json_spirit::int_type: + (*mapp)[it->first] = it->second.get_int64(); + break; + + case json_spirit::real_type: + (*mapp)[it->first] = it->second.get_real(); + break; + } + } + return true; + } catch (const std::runtime_error &e) { + ss << e.what(); + return false; + } +} + +class stringify_visitor : public boost::static_visitor<string> +{ + public: + template <typename T> + string operator()(T &operand) const + { + ostringstream oss; + oss << operand; + return oss.str(); + } +}; + +string +cmd_vartype_stringify(const cmd_vartype &v) +{ + return boost::apply_visitor(stringify_visitor(), v); +} + + +void +handle_bad_get(CephContext *cct, const string& k, const char *tname) +{ + ostringstream errstr; + int status; + const char *typestr = abi::__cxa_demangle(tname, 0, 0, &status); + if (status != 0) + typestr = tname; + errstr << "bad boost::get: key " << k << " is not type " << typestr; + lderr(cct) << errstr.str() << dendl; + + ostringstream oss; + oss << ClibBackTrace(1); + lderr(cct) << oss.str() << dendl; + + if (status == 0) + free((char *)typestr); +} + +long parse_pos_long(const char *s, std::ostream *pss) +{ + if (*s == '-' || *s == '+') { + if (pss) + *pss << "expected numerical value, got: " << s; + return -EINVAL; + } + + string err; + long r = strict_strtol(s, 10, &err); + if ((r == 0) && !err.empty()) { + if (pss) + *pss << err; + return -1; + } + if (r < 0) { + if (pss) + *pss << "unable to parse positive integer '" << s << "'"; + return -1; + } + return r; +} + +int parse_osd_id(const char *s, std::ostream *pss) +{ + // osd.NNN? + if (strncmp(s, "osd.", 4) == 0) { + s += 4; + } + + // NNN? + ostringstream ss; + long id = parse_pos_long(s, &ss); + if (id < 0) { + *pss << ss.str(); + return id; + } + if (id > 0xffff) { + *pss << "osd id " << id << " is too large"; + return -ERANGE; + } + return id; +} + +namespace { +template <typename Func> +bool find_first_in(std::string_view s, const char *delims, Func&& f) +{ + auto pos = s.find_first_not_of(delims); + while (pos != s.npos) { + s.remove_prefix(pos); + auto end = s.find_first_of(delims); + if (f(s.substr(0, end))) { + return true; + } + pos = s.find_first_not_of(delims, end); + } + return false; +} + +template<typename T> +T str_to_num(const std::string& s) +{ + if constexpr (is_same_v<T, int>) { + return std::stoi(s); + } else if constexpr (is_same_v<T, long>) { + return std::stol(s); + } else if constexpr (is_same_v<T, long long>) { + return std::stoll(s); + } else if constexpr (is_same_v<T, double>) { + return std::stod(s); + } +} + +template<typename T> +bool arg_in_range(T value, const arg_desc_t& desc, std::ostream& os) { + auto range = desc.find("range"); + if (range == desc.end()) { + return true; + } + auto min_max = get_str_list(string(range->second), "|"); + auto min = str_to_num<T>(min_max.front()); + auto max = std::numeric_limits<T>::max(); + if (min_max.size() > 1) { + max = str_to_num<T>(min_max.back()); + } + if (value < min || value > max) { + os << "'" << value << "' out of range: " << min_max; + return false; + } + return true; +} + +bool validate_str_arg(std::string_view value, + std::string_view type, + const arg_desc_t& desc, + std::ostream& os) +{ + if (type == "CephIPAddr") { + entity_addr_t addr; + if (addr.parse(value)) { + return true; + } else { + os << "failed to parse addr '" << value << "', should be ip:[port]"; + return false; + } + } else if (type == "CephChoices") { + auto choices = desc.find("strings"); + ceph_assert(choices != end(desc)); + auto strings = choices->second; + if (find_first_in(strings, "|", [=](auto choice) { + return (value == choice); + })) { + return true; + } else { + os << "'" << value << "' not belong to '" << strings << "'"; + return false; + } + } else { + // CephString or other types like CephPgid + return true; + } +} + +bool validate_bool(const cmdmap_t& cmdmap, + const arg_desc_t& desc, + const std::string_view name, + const std::string_view type, + std::ostream& os) +{ + bool v; + try { + if (!cmd_getval(cmdmap, name, v)) { + if (auto req = desc.find("req"); + req != end(desc) && req->second == "false") { + return true; + } else { + os << "missing required parameter: '" << name << "'"; + return false; + } + } + return true; + } catch (const bad_cmd_get& e) { + return false; + } +} + +template<bool is_vector, + typename T, + typename Value = std::conditional_t<is_vector, + vector<T>, + T>> +bool validate_arg(const cmdmap_t& cmdmap, + const arg_desc_t& desc, + const std::string_view name, + const std::string_view type, + std::ostream& os) +{ + Value v; + try { + if (!cmd_getval(cmdmap, name, v)) { + if constexpr (is_vector) { + // an empty list is acceptable. + return true; + } else { + if (auto req = desc.find("req"); + req != end(desc) && req->second == "false") { + return true; + } else { + os << "missing required parameter: '" << name << "'"; + return false; + } + } + } + } catch (const bad_cmd_get& e) { + return false; + } + auto validate = [&](const T& value) { + if constexpr (is_same_v<std::string, T>) { + return validate_str_arg(value, type, desc, os); + } else if constexpr (is_same_v<int64_t, T> || + is_same_v<double, T>) { + return arg_in_range(value, desc, os); + } + }; + if constexpr(is_vector) { + return find_if_not(begin(v), end(v), validate) == end(v); + } else { + return validate(v); + } +} +} // anonymous namespace + +bool validate_cmd(const std::string& desc, + const cmdmap_t& cmdmap, + std::ostream& os) +{ + return !find_first_in(desc, " ", [&](auto desc) { + auto arg_desc = cmddesc_get_args(desc); + if (arg_desc.empty()) { + return false; + } + ceph_assert(arg_desc.count("name")); + ceph_assert(arg_desc.count("type")); + auto name = arg_desc["name"]; + auto type = arg_desc["type"]; + if (arg_desc.count("n")) { + if (type == "CephInt") { + return !validate_arg<true, int64_t>(cmdmap, arg_desc, + name, type, os); + } else if (type == "CephFloat") { + return !validate_arg<true, double>(cmdmap, arg_desc, + name, type, os); + } else { + return !validate_arg<true, string>(cmdmap, arg_desc, + name, type, os); + } + } else { + if (type == "CephInt") { + return !validate_arg<false, int64_t>(cmdmap, arg_desc, + name, type, os); + } else if (type == "CephFloat") { + return !validate_arg<false, double>(cmdmap, arg_desc, + name, type, os); + } else if (type == "CephBool") { + return !validate_bool(cmdmap, arg_desc, + name, type, os); + } else { + return !validate_arg<false, string>(cmdmap, arg_desc, + name, type, os); + } + } + }); +} + +bool cmd_getval(const cmdmap_t& cmdmap, + std::string_view k, bool& val) +{ + /* + * Specialized getval for booleans. CephBool didn't exist before Nautilus, + * so earlier clients are sent a CephChoices argdesc instead, and will + * send us a "--foo-bar" value string for boolean arguments. + */ + auto found = cmdmap.find(k); + if (found == cmdmap.end()) { + return false; + } + try { + val = boost::get<bool>(found->second); + return true; + } catch (boost::bad_get&) { + try { + std::string expected{"--"}; + expected += k; + std::replace(expected.begin(), expected.end(), '_', '-'); + + std::string v_str = boost::get<std::string>(found->second); + if (v_str == expected) { + val = true; + return true; + } else { + throw bad_cmd_get(k, cmdmap); + } + } catch (boost::bad_get&) { + throw bad_cmd_get(k, cmdmap); + } + } +} + +bool cmd_getval_compat_cephbool( + const cmdmap_t& cmdmap, + const std::string& k, bool& val) +{ + try { + return cmd_getval(cmdmap, k, val); + } catch (bad_cmd_get& e) { + // try as legacy/compat CephChoices + std::string t; + if (!cmd_getval(cmdmap, k, t)) { + return false; + } + std::string expected = "--"s + k; + std::replace(expected.begin(), expected.end(), '_', '-'); + val = (t == expected); + return true; + } +} + +} |