...and for all modules, not just the active ones.
This enables us to give better feedback to the user
when they try and use a command from a disabled module,
and also fixes the race between enabling a module and
trying to use its commands.
Fixes: http://tracker.ceph.com/issues/21683
Signed-off-by: John Spray <john.spray@redhat.com>
dout(1) << "Constructed class from module: " << get_name() << dendl;
}
- return load_commands();
+ return 0;
}
void ActivePyModule::notify(const std::string ¬ify_type, const std::string ¬ify_id)
}
}
-int ActivePyModule::load_commands()
-{
- // Don't need a Gil here -- this is called from ActivePyModule::load(),
- // which already has one.
- PyObject *command_list = PyObject_GetAttrString(pClassInstance, "COMMANDS");
- if (command_list == nullptr) {
- // Even modules that don't define command should still have the COMMANDS
- // from the MgrModule definition. Something is wrong!
- derr << "Module " << get_name() << " has missing COMMANDS member" << dendl;
- return -EINVAL;
- }
- if (!PyObject_TypeCheck(command_list, &PyList_Type)) {
- // Relatively easy mistake for human to make, e.g. defining COMMANDS
- // as a {} instead of a []
- derr << "Module " << get_name() << " has COMMANDS member of wrong type ("
- "should be a list)" << dendl;
- return -EINVAL;
- }
- const size_t list_size = PyList_Size(command_list);
- for (size_t i = 0; i < list_size; ++i) {
- PyObject *command = PyList_GetItem(command_list, i);
- assert(command != nullptr);
-
- ModuleCommand item;
-
- PyObject *pCmd = PyDict_GetItemString(command, "cmd");
- assert(pCmd != nullptr);
- item.cmdstring = PyString_AsString(pCmd);
-
- dout(20) << "loaded command " << item.cmdstring << dendl;
- PyObject *pDesc = PyDict_GetItemString(command, "desc");
- assert(pDesc != nullptr);
- item.helpstring = PyString_AsString(pDesc);
-
- PyObject *pPerm = PyDict_GetItemString(command, "perm");
- assert(pPerm != nullptr);
- item.perm = PyString_AsString(pPerm);
-
- item.handler = this;
-
- commands.push_back(item);
- }
- Py_DECREF(command_list);
-
- dout(10) << "loaded " << commands.size() << " commands" << dendl;
-
- return 0;
-}
int ActivePyModule::handle_command(
const cmdmap_t &cmdmap,
class ActivePyModule;
class ActivePyModules;
-/**
- * A Ceph CLI command description provided from a Python module
- */
-class ModuleCommand {
-public:
- std::string cmdstring;
- std::string helpstring;
- std::string perm;
- ActivePyModule *handler;
-};
-
class ActivePyModule : public PyModuleRunner
{
private:
health_check_map_t health_checks;
- std::vector<ModuleCommand> commands;
-
- int load_commands();
-
// Optional, URI exposed by plugins that implement serve()
std::string uri;
void notify(const std::string ¬ify_type, const std::string ¬ify_id);
void notify_clog(const LogEntry &le);
- const std::vector<ModuleCommand> &get_commands() const
- {
- return commands;
- }
-
int handle_command(
const cmdmap_t &cmdmap,
std::stringstream *ds,
}
}
-std::vector<ModuleCommand> ActivePyModules::get_py_commands() const
-{
- Mutex::Locker l(lock);
-
- std::vector<ModuleCommand> result;
- for (const auto& i : modules) {
- auto module = i.second.get();
- auto mod_commands = module->get_commands();
- for (auto j : mod_commands) {
- result.push_back(j);
- }
- }
-
- return result;
-}
-
-std::vector<MonCommand> ActivePyModules::get_commands() const
-{
- std::vector<ModuleCommand> commands = get_py_commands();
- std::vector<MonCommand> result;
- for (auto &pyc: commands) {
- result.push_back({pyc.cmdstring, pyc.helpstring, "mgr",
- pyc.perm, "cli", MonCommand::FLAG_MGR});
- }
- return result;
-}
-
-
std::map<std::string, std::string> ActivePyModules::get_services() const
{
std::map<std::string, std::string> result;
}
}
+int ActivePyModules::handle_command(
+ std::string const &module_name,
+ const cmdmap_t &cmdmap,
+ std::stringstream *ds,
+ std::stringstream *ss)
+{
+ lock.Lock();
+ auto mod = modules.at(module_name).get();
+ lock.Unlock();
+ return mod->handle_command(cmdmap, ds, ss);
+}
+
void ActivePyModules::get_health_checks(health_check_map_t *checks)
{
Mutex::Locker l(lock);
void set_uri(const std::string& module_name, const std::string &uri);
- // Python command definitions, including callback
- std::vector<ModuleCommand> get_py_commands() const;
-
- // Monitor command definitions, suitable for CLI
- std::vector<MonCommand> get_commands() const;
+ int handle_command(
+ const std::string &module_name,
+ const cmdmap_t &cmdmap,
+ std::stringstream *ds,
+ std::stringstream *ss);
std::map<std::string, std::string> get_services() const;
}
}
- // None of the special native commands,
- ActivePyModule *handler = nullptr;
+ // Resolve the command to the name of the module that will
+ // handle it (if the command exists)
+ std::string handler_name;
auto py_commands = py_modules.get_py_commands();
for (const auto &pyc : py_commands) {
auto pyc_prefix = cmddesc_get_prefix(pyc.cmdstring);
dout(1) << "pyc_prefix: '" << pyc_prefix << "'" << dendl;
if (pyc_prefix == prefix) {
- handler = pyc.handler;
+ handler_name = pyc.module_name;
break;
}
}
- if (handler == nullptr) {
+ // Was the command unfound?
+ if (handler_name.empty()) {
ss << "No handler found for '" << prefix << "'";
dout(4) << "No handler found for '" << prefix << "'" << dendl;
cmdctx->reply(-EINVAL, ss);
return true;
- } else {
- // Okay, now we have a handler to call, but we must not call it
- // in this thread, because the python handlers can do anything,
- // including blocking, and including calling back into mgr.
- dout(4) << "passing through " << cmdctx->cmdmap.size() << dendl;
- finisher.queue(new FunctionContext([cmdctx, handler](int r_) {
- std::stringstream ds;
- std::stringstream ss;
- int r = handler->handle_command(cmdctx->cmdmap, &ds, &ss);
- cmdctx->odata.append(ds);
- cmdctx->reply(r, ss);
- }));
- return true;
}
+
+ dout(4) << "passing through " << cmdctx->cmdmap.size() << dendl;
+ finisher.queue(new FunctionContext([this, cmdctx, handler_name, prefix](int r_) {
+ std::stringstream ss;
+
+ // Validate that the module is enabled
+ PyModuleRef module = py_modules.get_module(handler_name);
+ if (!module->is_enabled()) {
+ ss << "Module '" << handler_name << "' is not enabled (required by "
+ "command '" << prefix << "'): use `ceph mgr module enable "
+ << handler_name << "` to enable it";
+ dout(4) << ss.str() << dendl;
+ cmdctx->reply(-EOPNOTSUPP, ss);
+ return;
+ }
+
+ // Hack: allow the self-test method to run on unhealthy modules.
+ // Fix this in future by creating a special path for self test rather
+ // than having the hook be a normal module command.
+ std::string self_test_prefix = handler_name + " " + "self-test";
+
+ // Validate that the module is healthy
+ bool accept_command;
+ if (module->is_loaded()) {
+ if (module->get_can_run() && !module->is_failed()) {
+ // Healthy module
+ accept_command = true;
+ } else if (self_test_prefix == prefix) {
+ // Unhealthy, but allow because it's a self test command
+ accept_command = true;
+ } else {
+ accept_command = false;
+ ss << "Module '" << handler_name << "' has experienced an error and "
+ "cannot handle commands: " << module->get_error_string();
+ }
+ } else {
+ // Module not loaded
+ accept_command = false;
+ ss << "Module '" << handler_name << "' failed to load and "
+ "cannot handle commands: " << module->get_error_string();
+ }
+
+ if (!accept_command) {
+ dout(4) << ss.str() << dendl;
+ cmdctx->reply(-EIO, ss);
+ return;
+ }
+
+ std::stringstream ds;
+ int r = py_modules.handle_command(handler_name, cmdctx->cmdmap, &ds, &ss);
+ cmdctx->odata.append(ds);
+ cmdctx->reply(r, ss);
+ }));
+ return true;
}
void DaemonServer::_prune_pending_service_map()
#include "global/signal_handler.h"
#include "mgr/MgrContext.h"
-#include "mgr/mgr_commands.h"
-//#include "MgrPyModule.h"
#include "DaemonServer.h"
#include "messages/MMgrBeacon.h"
#include "messages/MMgrDigest.h"
// assume finisher already initialized in background_init
dout(4) << "starting python modules..." << dendl;
- py_module_registry->active_start(loaded_config, daemon_state, cluster_state, *monc,
- clog, *objecter, *client, finisher);
+ py_module_registry->active_start(loaded_config, daemon_state, cluster_state,
+ *monc, clog, *objecter, *client, finisher);
dout(4) << "Complete." << dendl;
initializing = false;
server.send_report();
}
-std::vector<MonCommand> Mgr::get_command_set() const
-{
- Mutex::Locker l(lock);
-
- std::vector<MonCommand> commands = mgr_commands;
- std::vector<MonCommand> py_commands = py_module_registry->get_commands();
- commands.insert(commands.end(), py_commands.begin(), py_commands.end());
- return commands;
-}
-
std::map<std::string, std::string> Mgr::get_services() const
{
Mutex::Locker l(lock);
void background_init(Context *completion);
void shutdown();
- std::vector<MonCommand> get_command_set() const;
std::map<std::string, std::string> get_services() const;
};
#include "global/signal_handler.h"
#include "mgr/MgrContext.h"
+#include "mgr/mgr_commands.h"
#include "messages/MMgrBeacon.h"
#include "messages/MMgrMap.h"
// We are informing the mon that we are done initializing: inform
// it of our command set. This has to happen after init() because
// it needs the python modules to have loaded.
- m->set_command_descs(active_mgr->get_command_set());
+ std::vector<MonCommand> commands = mgr_commands;
+ std::vector<MonCommand> py_commands = py_module_registry.get_commands();
+ commands.insert(commands.end(), py_commands.begin(), py_commands.end());
+ m->set_command_descs(commands);
dout(4) << "going active, including " << m->get_command_descs().size()
<< " commands in beacon" << dendl;
}
// Environment is all good, import the external module
{
Gil gil(pMyThreadState);
+
int r;
r = load_subclass_of("MgrModule", &pClass);
if (r) {
return r;
}
+ r = load_commands();
+ if (r != 0) {
+ std::ostringstream oss;
+ oss << "Missing COMMAND attribute in module '" << module_name << "'";
+ error_string = oss.str();
+ derr << oss.str() << dendl;
+ return r;
+ }
+
// We've imported the module and found a MgrModule subclass, at this
// point the module is considered loaded. It might still not be
// runnable though, can_run populated later...
return 0;
}
+int PyModule::load_commands()
+{
+ // Don't need a Gil here -- this is called from load(),
+ // which already has one.
+ PyObject *command_list = PyObject_GetAttrString(pClass, "COMMANDS");
+ if (command_list == nullptr) {
+ // Even modules that don't define command should still have the COMMANDS
+ // from the MgrModule definition. Something is wrong!
+ derr << "Module " << get_name() << " has missing COMMANDS member" << dendl;
+ return -EINVAL;
+ }
+ if (!PyObject_TypeCheck(command_list, &PyList_Type)) {
+ // Relatively easy mistake for human to make, e.g. defining COMMANDS
+ // as a {} instead of a []
+ derr << "Module " << get_name() << " has COMMANDS member of wrong type ("
+ "should be a list)" << dendl;
+ return -EINVAL;
+ }
+ const size_t list_size = PyList_Size(command_list);
+ for (size_t i = 0; i < list_size; ++i) {
+ PyObject *command = PyList_GetItem(command_list, i);
+ assert(command != nullptr);
+
+ ModuleCommand item;
+
+ PyObject *pCmd = PyDict_GetItemString(command, "cmd");
+ assert(pCmd != nullptr);
+ item.cmdstring = PyString_AsString(pCmd);
+
+ dout(20) << "loaded command " << item.cmdstring << dendl;
+
+ PyObject *pDesc = PyDict_GetItemString(command, "desc");
+ assert(pDesc != nullptr);
+ item.helpstring = PyString_AsString(pDesc);
+
+ PyObject *pPerm = PyDict_GetItemString(command, "perm");
+ assert(pPerm != nullptr);
+ item.perm = PyString_AsString(pPerm);
+
+ item.module_name = module_name;
+
+ commands.push_back(item);
+ }
+ Py_DECREF(command_list);
+
+ dout(10) << "loaded " << commands.size() << " commands" << dendl;
+
+ return 0;
+}
+
int PyModule::load_subclass_of(const char* base_class, PyObject** py_class)
{
// load the base class
std::string handle_pyerror();
+/**
+ * A Ceph CLI command description provided from a Python module
+ */
+class ModuleCommand {
+public:
+ std::string cmdstring;
+ std::string helpstring;
+ std::string perm;
+
+ // Call the ActivePyModule of this name to handle the command
+ std::string module_name;
+};
+
class PyModule
{
mutable Mutex lock{"PyModule::lock"};
// Populated if loaded, can_run or failed indicates a problem
std::string error_string;
+ int load_commands();
+ std::vector<ModuleCommand> commands;
+
public:
SafeThreadState pMyThreadState;
PyObject *pClass = nullptr;
int load(PyThreadState *pMainThreadState);
+
+ /**
+ * Extend `out` with the contents of `this->commands`
+ */
+ void get_commands(std::vector<ModuleCommand> *out) const
+ {
+ Mutex::Locker l(lock);
+ assert(out != nullptr);
+ out->insert(out->end(), commands.begin(), commands.end());
+ }
+
+
/**
* Mark the module as failed, recording the reason in the error
* string.
}
bool is_enabled() const { Mutex::Locker l(lock) ; return enabled; }
+ bool is_failed() const { Mutex::Locker l(lock) ; return failed; }
+ bool is_loaded() const { Mutex::Locker l(lock) ; return loaded; }
+
const std::string &get_name() const {
Mutex::Locker l(lock) ; return module_name;
}
g_conf->with_val<std::string>("mgr_module_path",
&_list_modules, modules);
}
+
+int PyModuleRegistry::handle_command(
+ std::string const &module_name,
+ const cmdmap_t &cmdmap,
+ std::stringstream *ds,
+ std::stringstream *ss)
+{
+ if (active_modules) {
+ return active_modules->handle_command(module_name, cmdmap, ds, ss);
+ } else {
+ // We do not expect to be called before active modules is up, but
+ // it's straightfoward to handle this case so let's do it.
+ return -EAGAIN;
+ }
+}
+
+std::vector<ModuleCommand> PyModuleRegistry::get_py_commands() const
+{
+ Mutex::Locker l(lock);
+
+ std::vector<ModuleCommand> result;
+ for (const auto& i : modules) {
+ i.second->get_commands(&result);
+ }
+
+ return result;
+}
+
+std::vector<MonCommand> PyModuleRegistry::get_commands() const
+{
+ std::vector<ModuleCommand> commands = get_py_commands();
+ std::vector<MonCommand> result;
+ for (auto &pyc: commands) {
+ result.push_back({pyc.cmdstring, pyc.helpstring, "mgr",
+ pyc.perm, "cli", MonCommand::FLAG_MGR});
+ }
+ return result;
+}
+
std::forward<Callback>(cb)(*active_modules, std::forward<Args>(args)...);
}
+ std::vector<MonCommand> get_commands() const;
+ std::vector<ModuleCommand> get_py_commands() const;
+
+ /**
+ * module_name **must** exist, but does not have to be loaded
+ * or runnable.
+ */
+ PyModuleRef get_module(const std::string &module_name)
+ {
+ Mutex::Locker l(lock);
+ return modules.at(module_name);
+ }
+
+ int handle_command(
+ std::string const &module_name,
+ const cmdmap_t &cmdmap,
+ std::stringstream *ds,
+ std::stringstream *ss);
+
// FIXME: breaking interface so that I don't have to go rewrite all
// the places that call into these (for now)
// >>>
}
}
- std::vector<MonCommand> get_commands() const
- {
- assert(active_modules);
- return active_modules->get_commands();
- }
- std::vector<ModuleCommand> get_py_commands() const
- {
- assert(active_modules);
- return active_modules->get_py_commands();
- }
void get_health_checks(health_check_map_t *checks)
{
assert(active_modules);