// -*- 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; // 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 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 { 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 &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 &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 &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& 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& spvals = it->second.get_array(); if (spvals.empty()) { // if an empty array is acceptable, the caller should always check for // vector if the expected value of "vector" in the // cmdmap is missing. (*mapp)[it->first] = vector(); } else if (spvals.front().type() == json_spirit::str_type) { vector 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 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 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 { public: template 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 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 T str_to_num(const std::string& s) { if constexpr (is_same_v) { return std::stoi(s); } else if constexpr (is_same_v) { return std::stol(s); } else if constexpr (is_same_v) { return std::stoll(s); } else if constexpr (is_same_v) { return std::stod(s); } } template 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(min_max.front()); auto max = std::numeric_limits::max(); if (min_max.size() > 1) { max = str_to_num(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, 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) { return validate_str_arg(value, type, desc, os); } else if constexpr (is_same_v || is_same_v) { 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(cmdmap, arg_desc, name, type, os); } else if (type == "CephFloat") { return !validate_arg(cmdmap, arg_desc, name, type, os); } else { return !validate_arg(cmdmap, arg_desc, name, type, os); } } else { if (type == "CephInt") { return !validate_arg(cmdmap, arg_desc, name, type, os); } else if (type == "CephFloat") { return !validate_arg(cmdmap, arg_desc, name, type, os); } else if (type == "CephBool") { return !validate_bool(cmdmap, arg_desc, name, type, os); } else { return !validate_arg(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(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(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; } } }