]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr: load command definitions earlier
authorJohn Spray <john.spray@redhat.com>
Thu, 23 Nov 2017 13:01:33 +0000 (08:01 -0500)
committerJohn Spray <john.spray@redhat.com>
Wed, 24 Jan 2018 18:08:20 +0000 (13:08 -0500)
...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>
12 files changed:
src/mgr/ActivePyModule.cc
src/mgr/ActivePyModule.h
src/mgr/ActivePyModules.cc
src/mgr/ActivePyModules.h
src/mgr/DaemonServer.cc
src/mgr/Mgr.cc
src/mgr/Mgr.h
src/mgr/MgrStandby.cc
src/mgr/PyModule.cc
src/mgr/PyModule.h
src/mgr/PyModuleRegistry.cc
src/mgr/PyModuleRegistry.h

index 311144086abb612c1daac36d319589e84199787a..185ef445428849e40bce90a7be4462ff1562be95 100644 (file)
@@ -46,7 +46,7 @@ int ActivePyModule::load(ActivePyModules *py_modules)
     dout(1) << "Constructed class from module: " << get_name() << dendl;
   }
 
-  return load_commands();
+  return 0;
 }
 
 void ActivePyModule::notify(const std::string &notify_type, const std::string &notify_id)
@@ -100,55 +100,7 @@ void ActivePyModule::notify_clog(const LogEntry &log_entry)
   }
 }
 
-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,
index b475610fbb1de01d72f9c692770583a2c9619bff..9279f1c8f5d8a7d548038e5c14a8211af4604645 100644 (file)
 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;
 
@@ -67,11 +52,6 @@ public:
   void notify(const std::string &notify_type, const std::string &notify_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,
index 76b54203ded6e06245ca342f5167a041a474a0cc..7b370800926e35dc44f6fed9d2db98f0b9eb6e1d 100644 (file)
@@ -491,34 +491,6 @@ void ActivePyModules::set_config(const std::string &module_name,
   }
 }
 
-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;
@@ -713,6 +685,18 @@ void ActivePyModules::set_health_checks(const std::string& module_name,
   }
 }
 
+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);
index 6d0feedb4170d7f6444f9e5b749bf5db3df13c6c..60572c25bfcb0223b90b126b6dc0b370093a7d42 100644 (file)
@@ -90,11 +90,11 @@ public:
 
   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;
 
index 409aa18bee156539a3e26b2aefb513b8dd3c3d4d..5a18f27a947f3ef5aa5769b352288ecbcfdcb790 100644 (file)
@@ -1332,37 +1332,80 @@ bool DaemonServer::handle_command(MCommand *m)
     }
   }
 
-  // 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()
index eed2bae72007db9131a2a143f091e3efc016df0a..6c92395bcfa52dd12dcce9e5733be95a675e09ea 100644 (file)
@@ -22,9 +22,7 @@
 #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"
@@ -218,8 +216,8 @@ void Mgr::init()
 
   // 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;
@@ -624,16 +622,6 @@ void Mgr::tick()
   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);
index a4d8aad39ff703c47f1c9e4a429bc8385813a0fd..912114c380b9746bec9ea75ec149f2d4af7c2e8e 100644 (file)
@@ -101,7 +101,6 @@ public:
   void background_init(Context *completion);
   void shutdown();
 
-  std::vector<MonCommand> get_command_set() const;
   std::map<std::string, std::string> get_services() const;
 };
 
index 91f7737813b600b8d44d66b4852ec9b744cf26b7..cf491d8d72a99ad5d31c4cdeb901e361257df204 100644 (file)
@@ -22,6 +22,7 @@
 #include "global/signal_handler.h"
 
 #include "mgr/MgrContext.h"
+#include "mgr/mgr_commands.h"
 
 #include "messages/MMgrBeacon.h"
 #include "messages/MMgrMap.h"
@@ -189,7 +190,10 @@ void MgrStandby::send_beacon()
       // 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;
     }
index 3d4eb7e90fc4a147a235b271552c96ebb073488e..d2d825ca0d5a9dcbe15f4124b77afb96214b41ff 100644 (file)
@@ -210,6 +210,7 @@ int PyModule::load(PyThreadState *pMainThreadState)
   // Environment is all good, import the external module
   {
     Gil gil(pMyThreadState);
+
     int r;
     r = load_subclass_of("MgrModule", &pClass);
     if (r) {
@@ -217,6 +218,15 @@ int PyModule::load(PyThreadState *pMainThreadState)
       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...
@@ -268,6 +278,56 @@ int PyModule::load(PyThreadState *pMainThreadState)
   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
index d3c39f4abb33e2a7fff2663ea5aa42f1cf4c4a19..14f418e446eab46e3999521d82c22b384957f88e 100644 (file)
 
 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"};
@@ -48,6 +61,9 @@ private:
   // 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;
@@ -62,6 +78,18 @@ public:
 
   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.
@@ -74,6 +102,9 @@ public:
   }
 
   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;
   }
index dcda1e72f84de5c888e69c3f54277378db57979f..5c114b9e8b6febe7183634da533d1098f4f502c6 100644 (file)
@@ -251,3 +251,42 @@ void PyModuleRegistry::list_modules(std::set<std::string> *modules)
   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;
+}
+
index 3eaa9f640926eac893a345afbd79990fc3608bc8..397c46de720a22dd8b561baa64450cb4c702b13e 100644 (file)
@@ -117,6 +117,25 @@ public:
     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)
   // >>>
@@ -135,16 +154,6 @@ public:
     }
   }
 
-  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);