diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 18:24:20 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 18:24:20 +0000 |
commit | 483eb2f56657e8e7f419ab1a4fab8dce9ade8609 (patch) | |
tree | e5d88d25d870d5dedacb6bbdbe2a966086a0a5cf /src/mon/MonCap.cc | |
parent | Initial commit. (diff) | |
download | ceph-483eb2f56657e8e7f419ab1a4fab8dce9ade8609.tar.xz ceph-483eb2f56657e8e7f419ab1a4fab8dce9ade8609.zip |
Adding upstream version 14.2.21.upstream/14.2.21upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/mon/MonCap.cc')
-rw-r--r-- | src/mon/MonCap.cc | 630 |
1 files changed, 630 insertions, 0 deletions
diff --git a/src/mon/MonCap.cc b/src/mon/MonCap.cc new file mode 100644 index 00000000..87f11c3d --- /dev/null +++ b/src/mon/MonCap.cc @@ -0,0 +1,630 @@ +// -*- 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 + * + * This is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License version 2.1, as published by the Free Software + * Foundation. See file COPYING. + * + */ + +#include <boost/config/warning_disable.hpp> +#include <boost/spirit/include/qi_uint.hpp> +#include <boost/spirit/include/qi.hpp> +#include <boost/fusion/include/std_pair.hpp> +#include <boost/spirit/include/phoenix.hpp> +#include <boost/fusion/adapted/struct/adapt_struct.hpp> +#include <boost/fusion/include/adapt_struct.hpp> +#include <boost/algorithm/string/predicate.hpp> + +#include "MonCap.h" +#include "include/stringify.h" +#include "include/ipaddr.h" +#include "common/debug.h" +#include "common/Formatter.h" + +#include <algorithm> +#include <regex> + +#include "include/ceph_assert.h" + +static inline bool is_not_alnum_space(char c) +{ + return !(isalpha(c) || isdigit(c) || (c == '-') || (c == '_')); +} + +static string maybe_quote_string(const std::string& str) +{ + if (find_if(str.begin(), str.end(), is_not_alnum_space) == str.end()) + return str; + return string("\"") + str + string("\""); +} + +using std::ostream; +using std::vector; + +#define dout_subsys ceph_subsys_mon + +ostream& operator<<(ostream& out, const mon_rwxa_t& p) +{ + if (p == MON_CAP_ANY) + return out << "*"; + + if (p & MON_CAP_R) + out << "r"; + if (p & MON_CAP_W) + out << "w"; + if (p & MON_CAP_X) + out << "x"; + return out; +} + +ostream& operator<<(ostream& out, const StringConstraint& c) +{ + switch (c.match_type) { + case StringConstraint::MATCH_TYPE_EQUAL: + return out << "value " << c.value; + case StringConstraint::MATCH_TYPE_PREFIX: + return out << "prefix " << c.value; + case StringConstraint::MATCH_TYPE_REGEX: + return out << "regex " << c.value; + default: + break; + } + return out; +} + +ostream& operator<<(ostream& out, const MonCapGrant& m) +{ + out << "allow"; + if (m.service.length()) { + out << " service " << maybe_quote_string(m.service); + } + if (m.command.length()) { + out << " command " << maybe_quote_string(m.command); + if (!m.command_args.empty()) { + out << " with"; + for (map<string,StringConstraint>::const_iterator p = m.command_args.begin(); + p != m.command_args.end(); + ++p) { + switch (p->second.match_type) { + case StringConstraint::MATCH_TYPE_EQUAL: + out << " " << maybe_quote_string(p->first) << "=" + << maybe_quote_string(p->second.value); + break; + case StringConstraint::MATCH_TYPE_PREFIX: + out << " " << maybe_quote_string(p->first) << " prefix " + << maybe_quote_string(p->second.value); + break; + case StringConstraint::MATCH_TYPE_REGEX: + out << " " << maybe_quote_string(p->first) << " regex " + << maybe_quote_string(p->second.value); + break; + default: + break; + } + } + } + } + if (m.profile.length()) { + out << " profile " << maybe_quote_string(m.profile); + } + if (m.allow != 0) + out << " " << m.allow; + if (m.network.size()) + out << " network " << m.network; + return out; +} + + +// <magic> +// fusion lets us easily populate structs via the qi parser. + +typedef map<string,StringConstraint> kvmap; + +BOOST_FUSION_ADAPT_STRUCT(MonCapGrant, + (std::string, service) + (std::string, profile) + (std::string, command) + (kvmap, command_args) + (mon_rwxa_t, allow) + (std::string, network)) + +BOOST_FUSION_ADAPT_STRUCT(StringConstraint, + (StringConstraint::MatchType, match_type) + (std::string, value)) + +// </magic> + +void MonCapGrant::parse_network() +{ + network_valid = ::parse_network(network.c_str(), &network_parsed, + &network_prefix); +} + +void MonCapGrant::expand_profile(const EntityName& name) const +{ + // only generate this list once + if (!profile_grants.empty()) + return; + + if (profile == "read-only") { + // grants READ-ONLY caps monitor-wide + // 'auth' requires MON_CAP_X even for RO, which we do not grant here. + profile_grants.push_back(mon_rwxa_t(MON_CAP_R)); + return; + } + + if (profile == "read-write") { + // grants READ-WRITE caps monitor-wide + // 'auth' requires MON_CAP_X for all operations, which we do not grant. + profile_grants.push_back(mon_rwxa_t(MON_CAP_R | MON_CAP_W)); + return; + } + + if (profile == "mon") { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_ALL)); + profile_grants.push_back(MonCapGrant("log", MON_CAP_ALL)); + } + if (profile == "osd") { + profile_grants.push_back(MonCapGrant("osd", MON_CAP_ALL)); + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("pg", MON_CAP_R | MON_CAP_W)); + profile_grants.push_back(MonCapGrant("log", MON_CAP_W)); + } + if (profile == "mds") { + profile_grants.push_back(MonCapGrant("mds", MON_CAP_ALL)); + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R)); + // This command grant is checked explicitly in MRemoveSnaps handling + profile_grants.push_back(MonCapGrant("osd pool rmsnap")); + profile_grants.push_back(MonCapGrant("osd blacklist")); + profile_grants.push_back(MonCapGrant("log", MON_CAP_W)); + } + if (profile == "mgr") { + profile_grants.push_back(MonCapGrant("mgr", MON_CAP_ALL)); + profile_grants.push_back(MonCapGrant("log", MON_CAP_R | MON_CAP_W)); + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R | MON_CAP_W)); + profile_grants.push_back(MonCapGrant("mds", MON_CAP_R | MON_CAP_W)); + profile_grants.push_back(MonCapGrant("fs", MON_CAP_R | MON_CAP_W)); + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R | MON_CAP_W)); + profile_grants.push_back(MonCapGrant("auth", MON_CAP_R | MON_CAP_W | MON_CAP_X)); + profile_grants.push_back(MonCapGrant("config-key", MON_CAP_R | MON_CAP_W)); + profile_grants.push_back(MonCapGrant("config", MON_CAP_R | MON_CAP_W)); + } + if (profile == "osd" || profile == "mds" || profile == "mon" || + profile == "mgr") { + StringConstraint constraint(StringConstraint::MATCH_TYPE_PREFIX, + string("daemon-private/") + stringify(name) + + string("/")); + string prefix = string("daemon-private/") + stringify(name) + string("/"); + profile_grants.push_back(MonCapGrant("config-key get", "key", constraint)); + profile_grants.push_back(MonCapGrant("config-key put", "key", constraint)); + profile_grants.push_back(MonCapGrant("config-key set", "key", constraint)); + profile_grants.push_back(MonCapGrant("config-key exists", "key", constraint)); + profile_grants.push_back(MonCapGrant("config-key delete", "key", constraint)); + } + if (profile == "bootstrap-osd") { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); // read monmap + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R)); // read osdmap + profile_grants.push_back(MonCapGrant("mon getmap")); + profile_grants.push_back(MonCapGrant("osd new")); + profile_grants.push_back(MonCapGrant("osd purge-new")); + } + if (profile == "bootstrap-mds") { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); // read monmap + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R)); // read osdmap + profile_grants.push_back(MonCapGrant("mon getmap")); + profile_grants.push_back(MonCapGrant("auth get-or-create")); // FIXME: this can expose other mds keys + profile_grants.back().command_args["entity"] = StringConstraint( + StringConstraint::MATCH_TYPE_PREFIX, "mds."); + profile_grants.back().command_args["caps_mon"] = StringConstraint( + StringConstraint::MATCH_TYPE_EQUAL, "allow profile mds"); + profile_grants.back().command_args["caps_osd"] = StringConstraint( + StringConstraint::MATCH_TYPE_EQUAL, "allow rwx"); + profile_grants.back().command_args["caps_mds"] = StringConstraint( + StringConstraint::MATCH_TYPE_EQUAL, "allow"); + } + if (profile == "bootstrap-mgr") { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); // read monmap + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R)); // read osdmap + profile_grants.push_back(MonCapGrant("mon getmap")); + profile_grants.push_back(MonCapGrant("auth get-or-create")); // FIXME: this can expose other mgr keys + profile_grants.back().command_args["entity"] = StringConstraint( + StringConstraint::MATCH_TYPE_PREFIX, "mgr."); + profile_grants.back().command_args["caps_mon"] = StringConstraint( + StringConstraint::MATCH_TYPE_EQUAL, "allow profile mgr"); + } + if (profile == "bootstrap-rgw") { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); // read monmap + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R)); // read osdmap + profile_grants.push_back(MonCapGrant("mon getmap")); + profile_grants.push_back(MonCapGrant("auth get-or-create")); // FIXME: this can expose other mds keys + profile_grants.back().command_args["entity"] = StringConstraint( + StringConstraint::MATCH_TYPE_PREFIX, "client.rgw."); + profile_grants.back().command_args["caps_mon"] = StringConstraint( + StringConstraint::MATCH_TYPE_EQUAL, "allow rw"); + profile_grants.back().command_args["caps_osd"] = StringConstraint( + StringConstraint::MATCH_TYPE_EQUAL, "allow rwx"); + } + if (profile == "bootstrap-rbd" || profile == "bootstrap-rbd-mirror") { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); // read monmap + profile_grants.push_back(MonCapGrant("auth get-or-create")); // FIXME: this can expose other rbd keys + profile_grants.back().command_args["entity"] = StringConstraint( + StringConstraint::MATCH_TYPE_PREFIX, "client."); + profile_grants.back().command_args["caps_mon"] = StringConstraint( + StringConstraint::MATCH_TYPE_EQUAL, + (profile == "bootstrap-rbd-mirror" ? "profile rbd-mirror" : + "profile rbd")); + profile_grants.back().command_args["caps_osd"] = StringConstraint( + StringConstraint::MATCH_TYPE_REGEX, + "^([ ,]*profile(=|[ ]+)['\"]?rbd[^ ,'\"]*['\"]?([ ]+pool(=|[ ]+)['\"]?[^,'\"]+['\"]?)?)+$"); + } + if (profile == "fs-client") { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("mds", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("pg", MON_CAP_R)); + } + if (profile == "simple-rados-client") { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("pg", MON_CAP_R)); + } + if (boost::starts_with(profile, "rbd")) { + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("osd", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("pg", MON_CAP_R)); + + // exclusive lock dead-client blacklisting (IP+nonce required) + profile_grants.push_back(MonCapGrant("osd blacklist")); + profile_grants.back().command_args["blacklistop"] = StringConstraint( + StringConstraint::MATCH_TYPE_EQUAL, "add"); + profile_grants.back().command_args["addr"] = StringConstraint( + StringConstraint::MATCH_TYPE_REGEX, "^[^/]+/[0-9]+$"); + + } + if (profile == "rbd-mirror") { + StringConstraint constraint(StringConstraint::MATCH_TYPE_PREFIX, + "rbd/mirror/"); + profile_grants.push_back(MonCapGrant("config-key get", "key", constraint)); + } else if (profile == "rbd-mirror-peer") { + StringConstraint constraint(StringConstraint::MATCH_TYPE_REGEX, + "rbd/mirror/[^/]+"); + profile_grants.push_back(MonCapGrant("config-key get", "key", constraint)); + + constraint = StringConstraint(StringConstraint::MATCH_TYPE_PREFIX, + "rbd/mirror/peer/"); + profile_grants.push_back(MonCapGrant("config-key set", "key", constraint)); + } + else if (profile == "crash") { + // TODO: we could limit this to getting the monmap and mgrmap... + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); + } + if (profile == "role-definer") { + // grants ALL caps to the auth subsystem, read-only on the + // monitor subsystem and nothing else. + profile_grants.push_back(MonCapGrant("mon", MON_CAP_R)); + profile_grants.push_back(MonCapGrant("auth", MON_CAP_ALL)); + } +} + +mon_rwxa_t MonCapGrant::get_allowed(CephContext *cct, + EntityName name, + const std::string& s, const std::string& c, + const map<string,string>& c_args) const +{ + if (profile.length()) { + expand_profile(name); + mon_rwxa_t a; + for (list<MonCapGrant>::const_iterator p = profile_grants.begin(); + p != profile_grants.end(); ++p) + a = a | p->get_allowed(cct, name, s, c, c_args); + return a; + } + if (service.length()) { + if (service != s) + return 0; + return allow; + } + if (command.length()) { + if (command != c) + return 0; + for (map<string,StringConstraint>::const_iterator p = command_args.begin(); p != command_args.end(); ++p) { + map<string,string>::const_iterator q = c_args.find(p->first); + // argument must be present if a constraint exists + if (q == c_args.end()) + return 0; + switch (p->second.match_type) { + case StringConstraint::MATCH_TYPE_EQUAL: + if (p->second.value != q->second) + return 0; + break; + case StringConstraint::MATCH_TYPE_PREFIX: + if (q->second.find(p->second.value) != 0) + return 0; + break; + case StringConstraint::MATCH_TYPE_REGEX: + try { + std::regex pattern( + p->second.value, std::regex::extended); + if (!std::regex_match(q->second, pattern)) + return 0; + } catch(const std::regex_error&) { + return 0; + } + break; + default: + break; + } + } + return MON_CAP_ALL; + } + // we don't allow config-key service to be accessed with blanket caps other + // than '*' (i.e., 'any'), and that should have been checked by the caller + // via 'is_allow_all()'. + if (s == "config-key") { + return 0; + } + return allow; +} + +ostream& operator<<(ostream&out, const MonCap& m) +{ + for (vector<MonCapGrant>::const_iterator p = m.grants.begin(); p != m.grants.end(); ++p) { + if (p != m.grants.begin()) + out << ", "; + out << *p; + } + return out; +} + +bool MonCap::is_allow_all() const +{ + for (vector<MonCapGrant>::const_iterator p = grants.begin(); p != grants.end(); ++p) + if (p->is_allow_all()) + return true; + return false; +} + +void MonCap::set_allow_all() +{ + grants.clear(); + grants.push_back(MonCapGrant(MON_CAP_ANY)); + text = "allow *"; +} + +bool MonCap::is_capable( + CephContext *cct, + EntityName name, + const string& service, + const string& command, const map<string,string>& command_args, + bool op_may_read, bool op_may_write, bool op_may_exec, + const entity_addr_t& addr) const +{ + if (cct) + ldout(cct, 20) << "is_capable service=" << service << " command=" << command + << (op_may_read ? " read":"") + << (op_may_write ? " write":"") + << (op_may_exec ? " exec":"") + << " addr " << addr + << " on cap " << *this + << dendl; + + mon_rwxa_t allow = 0; + for (vector<MonCapGrant>::const_iterator p = grants.begin(); + p != grants.end(); ++p) { + if (cct) + ldout(cct, 20) << " allow so far " << allow << ", doing grant " << *p + << dendl; + + if (p->network.size() && + (!p->network_valid || + !network_contains(p->network_parsed, + p->network_prefix, + addr))) { + continue; + } + + if (p->is_allow_all()) { + if (cct) + ldout(cct, 20) << " allow all" << dendl; + return true; + } + + // check enumerated caps + allow = allow | p->get_allowed(cct, name, service, command, command_args); + if ((!op_may_read || (allow & MON_CAP_R)) && + (!op_may_write || (allow & MON_CAP_W)) && + (!op_may_exec || (allow & MON_CAP_X))) { + if (cct) + ldout(cct, 20) << " match" << dendl; + return true; + } + } + return false; +} + +void MonCap::encode(bufferlist& bl) const +{ + ENCODE_START(4, 4, bl); // legacy MonCaps was 3, 3 + encode(text, bl); + ENCODE_FINISH(bl); +} + +void MonCap::decode(bufferlist::const_iterator& bl) +{ + string s; + DECODE_START(4, bl); + decode(s, bl); + DECODE_FINISH(bl); + parse(s, NULL); +} + +void MonCap::dump(Formatter *f) const +{ + f->dump_string("text", text); +} + +void MonCap::generate_test_instances(list<MonCap*>& ls) +{ + ls.push_back(new MonCap); + ls.push_back(new MonCap); + ls.back()->parse("allow *"); + ls.push_back(new MonCap); + ls.back()->parse("allow rwx"); + ls.push_back(new MonCap); + ls.back()->parse("allow service foo x"); + ls.push_back(new MonCap); + ls.back()->parse("allow command bar x"); + ls.push_back(new MonCap); + ls.back()->parse("allow service foo r, allow command bar x"); + ls.push_back(new MonCap); + ls.back()->parse("allow command bar with k1=v1 x"); + ls.push_back(new MonCap); + ls.back()->parse("allow command bar with k1=v1 k2=v2 x"); +} + +// grammar +namespace qi = boost::spirit::qi; +namespace ascii = boost::spirit::ascii; +namespace phoenix = boost::phoenix; + + +template <typename Iterator> +struct MonCapParser : qi::grammar<Iterator, MonCap()> +{ + MonCapParser() : MonCapParser::base_type(moncap) + { + using qi::char_; + using qi::int_; + using qi::ulong_long; + using qi::lexeme; + using qi::alnum; + using qi::_val; + using qi::_1; + using qi::_2; + using qi::_3; + using qi::eps; + using qi::lit; + + quoted_string %= + lexeme['"' >> +(char_ - '"') >> '"'] | + lexeme['\'' >> +(char_ - '\'') >> '\'']; + unquoted_word %= +char_("a-zA-Z0-9_./-"); + str %= quoted_string | unquoted_word; + network_str %= +char_("/.:a-fA-F0-9]["); + + spaces = +(lit(' ') | lit('\n') | lit('\t')); + + // command := command[=]cmd [k1=v1 k2=v2 ...] + str_match = '=' >> qi::attr(StringConstraint::MATCH_TYPE_EQUAL) >> str; + str_prefix = spaces >> lit("prefix") >> spaces >> + qi::attr(StringConstraint::MATCH_TYPE_PREFIX) >> str; + str_regex = spaces >> lit("regex") >> spaces >> + qi::attr(StringConstraint::MATCH_TYPE_REGEX) >> str; + kv_pair = str >> (str_match | str_prefix | str_regex); + kv_map %= kv_pair >> *(spaces >> kv_pair); + command_match = -spaces >> lit("allow") >> spaces >> lit("command") >> (lit('=') | spaces) + >> qi::attr(string()) >> qi::attr(string()) + >> str + >> -(spaces >> lit("with") >> spaces >> kv_map) + >> qi::attr(0) + >> -(spaces >> lit("network") >> spaces >> network_str); + + // service foo rwxa + service_match %= -spaces >> lit("allow") >> spaces >> lit("service") >> (lit('=') | spaces) + >> str >> qi::attr(string()) >> qi::attr(string()) + >> qi::attr(map<string,StringConstraint>()) + >> spaces >> rwxa + >> -(spaces >> lit("network") >> spaces >> network_str); + + // profile foo + profile_match %= -spaces >> -(lit("allow") >> spaces) + >> lit("profile") >> (lit('=') | spaces) + >> qi::attr(string()) + >> str + >> qi::attr(string()) + >> qi::attr(map<string,StringConstraint>()) + >> qi::attr(0) + >> -(spaces >> lit("network") >> spaces >> network_str); + + // rwxa + rwxa_match %= -spaces >> lit("allow") >> spaces + >> qi::attr(string()) >> qi::attr(string()) >> qi::attr(string()) + >> qi::attr(map<string,StringConstraint>()) + >> rwxa + >> -(spaces >> lit("network") >> spaces >> network_str); + + // rwxa := * | [r][w][x] + rwxa = + (lit("*")[_val = MON_CAP_ANY]) | + (lit("all")[_val = MON_CAP_ANY]) | + ( eps[_val = 0] >> + ( lit('r')[_val |= MON_CAP_R] || + lit('w')[_val |= MON_CAP_W] || + lit('x')[_val |= MON_CAP_X] + ) + ); + + // grant := allow ... + grant = -spaces >> (rwxa_match | profile_match | service_match | command_match) >> -spaces; + + // moncap := grant [grant ...] + grants %= (grant % (*lit(' ') >> (lit(';') | lit(',')) >> *lit(' '))); + moncap = grants [_val = phoenix::construct<MonCap>(_1)]; + + } + qi::rule<Iterator> spaces; + qi::rule<Iterator, unsigned()> rwxa; + qi::rule<Iterator, string()> quoted_string; + qi::rule<Iterator, string()> unquoted_word; + qi::rule<Iterator, string()> str, network_str; + + qi::rule<Iterator, StringConstraint()> str_match, str_prefix, str_regex; + qi::rule<Iterator, pair<string, StringConstraint>()> kv_pair; + qi::rule<Iterator, map<string, StringConstraint>()> kv_map; + + qi::rule<Iterator, MonCapGrant()> rwxa_match; + qi::rule<Iterator, MonCapGrant()> command_match; + qi::rule<Iterator, MonCapGrant()> service_match; + qi::rule<Iterator, MonCapGrant()> profile_match; + qi::rule<Iterator, MonCapGrant()> grant; + qi::rule<Iterator, std::vector<MonCapGrant>()> grants; + qi::rule<Iterator, MonCap()> moncap; +}; + +bool MonCap::parse(const string& str, ostream *err) +{ + auto iter = str.begin(); + auto end = str.end(); + + MonCapParser<string::const_iterator> exp; + bool r = qi::parse(iter, end, exp, *this); + if (r && iter == end) { + text = str; + for (auto& g : grants) { + g.parse_network(); + } + return true; + } + + // Make sure no grants are kept after parsing failed! + grants.clear(); + + if (err) { + if (iter != end) + *err << "mon capability parse failed, stopped at '" + << std::string(iter, end) + << "' of '" << str << "'"; + else + *err << "mon capability parse failed, stopped at end of '" << str << "'"; + } + + return false; +} + |