Add a new client called 'gnt-storage'.
The client interacts with the ExtStorage interface, similarly to
the way gnt-os interacts with the OS interface.

For now, only two commands are supported: 'info' and 'diagnose'.

'diagnose' calculates the node status of each provider on each node,
similarly to gnt-os diagnose. Furthermore, for every provider, it
calculates it's nodegroup validity for each nodegroup. This is done
inside the LU and not the client (marked as 'TODO' for the  global
validity of gnt-os diagnose).

In the future, gnt-storage can be used to manage storage pools,
or even be extended to diagnose other storage types supported by
Ganeti, such as lvm, drbd (INT_MIRROR) or rbd (EXT_MIRROR).

Signed-off-by: Constantinos Venetsanopoulos <c...@grnet.gr>
---
 Makefile.am                     |    6 +-
 autotools/build-bash-completion |    4 +
 lib/backend.py                  |   45 +++++++++
 lib/cli.py                      |   12 ++-
 lib/client/gnt_storage.py       |  197 +++++++++++++++++++++++++++++++++++++++
 lib/cmdlib.py                   |  154 ++++++++++++++++++++++++++++++
 lib/constants.py                |    3 +-
 lib/opcodes.py                  |   10 ++
 lib/query.py                    |   34 +++++++
 lib/rpc_defs.py                 |    7 +-
 lib/server/noded.py             |    9 ++
 11 files changed, 476 insertions(+), 5 deletions(-)
 create mode 100644 lib/client/gnt_storage.py

diff --git a/Makefile.am b/Makefile.am
index 6794e38..fd408fc 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -244,7 +244,8 @@ client_PYTHON = \
        lib/client/gnt_instance.py \
        lib/client/gnt_job.py \
        lib/client/gnt_node.py \
-       lib/client/gnt_os.py
+       lib/client/gnt_os.py \
+       lib/client/gnt_storage.py
 
 hypervisor_PYTHON = \
        lib/hypervisor/__init__.py \
@@ -480,7 +481,8 @@ gnt_scripts = \
        scripts/gnt-instance \
        scripts/gnt-job \
        scripts/gnt-node \
-       scripts/gnt-os
+       scripts/gnt-os \
+       scripts/gnt-storage
 
 PYTHON_BOOTSTRAP_SBIN = \
        daemons/ganeti-masterd \
diff --git a/autotools/build-bash-completion b/autotools/build-bash-completion
index 6eb0dbe..5cb386c 100755
--- a/autotools/build-bash-completion
+++ b/autotools/build-bash-completion
@@ -341,6 +341,8 @@ class CompletionWriter:
           WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_OS:
           WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_EXTSTORAGE:
+          WriteCompReply(sw, "-W \"$(_ganeti_extstorage)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
           WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
@@ -446,6 +448,8 @@ class CompletionWriter:
           choices = "$(_ganeti_jobs)"
         elif isinstance(arg, cli.ArgOs):
           choices = "$(_ganeti_os)"
+        elif isinstance(arg, cli.ArgExtStorage):
+          choices = "$(_ganeti_extstorage)"
         elif isinstance(arg, cli.ArgFile):
           choices = ""
           compgenargs.append("-f")
diff --git a/lib/backend.py b/lib/backend.py
index 338a30d..e563a76 100644
--- a/lib/backend.py
+++ b/lib/backend.py
@@ -2438,6 +2438,51 @@ def OSEnvironment(instance, inst_os, debug=0):
   return result
 
 
+def DiagnoseExtStorage(top_dirs=None):
+  """Compute the validity for all ExtStorage Providers.
+
+  @type top_dirs: list
+  @param top_dirs: the list of directories in which to
+      search (if not given defaults to
+      L{constants.ES_SEARCH_PATH})
+  @rtype: list of L{objects.ExtStorage}
+  @return: a list of tuples (name, path, status, diagnose, parameters)
+      for all (potential) ExtStorage Providers under all
+      search paths, where:
+          - name is the (potential) ExtStorage Provider
+          - path is the full path to the ExtStorage Provider
+          - status True/False is the validity of the ExtStorage Provider
+          - diagnose is the error message for an invalid ExtStorage Provider,
+            otherwise empty
+          - parameters is a list of (name, help) parameters, if any
+
+  """
+  if top_dirs is None:
+    top_dirs = constants.ES_SEARCH_PATH
+
+  result = []
+  for dir_name in top_dirs:
+    if os.path.isdir(dir_name):
+      try:
+        f_names = utils.ListVisibleFiles(dir_name)
+      except EnvironmentError, err:
+        logging.exception("Can't list the ExtStorage directory %s: %s",
+                          dir_name, err)
+        break
+      for name in f_names:
+        es_path = utils.PathJoin(dir_name, name)
+        status, es_inst = bdev.ExtStorageFromDisk(name, base_dir=dir_name)
+        if status:
+          diagnose = ""
+          parameters = es_inst.supported_parameters
+        else:
+          diagnose = es_inst
+          parameters = []
+        result.append((name, es_path, status, diagnose, parameters))
+
+  return result
+
+
 def BlockdevGrow(disk, amount, dryrun):
   """Grow a stack of block devices.
 
diff --git a/lib/cli.py b/lib/cli.py
index 87f0493..f769869 100644
--- a/lib/cli.py
+++ b/lib/cli.py
@@ -244,6 +244,7 @@ __all__ = [
   "ArgJobId",
   "ArgNode",
   "ArgOs",
+  "ArgExtStorage",
   "ArgSuggest",
   "ArgUnknown",
   "OPT_COMPL_INST_ADD_NODES",
@@ -253,6 +254,7 @@ __all__ = [
   "OPT_COMPL_ONE_NODE",
   "OPT_COMPL_ONE_NODEGROUP",
   "OPT_COMPL_ONE_OS",
+  "OPT_COMPL_ONE_EXTSTORAGE",
   "cli_option",
   "SplitNodeOption",
   "CalculateOSNames",
@@ -373,6 +375,12 @@ class ArgOs(_Argument):
   """
 
 
+class ArgExtStorage(_Argument):
+  """ExtStorage argument.
+
+  """
+
+
 ARGS_NONE = []
 ARGS_MANY_INSTANCES = [ArgInstance()]
 ARGS_MANY_NODES = [ArgNode()]
@@ -607,15 +615,17 @@ def check_list(option, opt, value): # pylint: 
disable=W0613
  OPT_COMPL_ONE_NODE,
  OPT_COMPL_ONE_INSTANCE,
  OPT_COMPL_ONE_OS,
+ OPT_COMPL_ONE_EXTSTORAGE,
  OPT_COMPL_ONE_IALLOCATOR,
  OPT_COMPL_INST_ADD_NODES,
- OPT_COMPL_ONE_NODEGROUP) = range(100, 107)
+ OPT_COMPL_ONE_NODEGROUP) = range(100, 108)
 
 OPT_COMPL_ALL = frozenset([
   OPT_COMPL_MANY_NODES,
   OPT_COMPL_ONE_NODE,
   OPT_COMPL_ONE_INSTANCE,
   OPT_COMPL_ONE_OS,
+  OPT_COMPL_ONE_EXTSTORAGE,
   OPT_COMPL_ONE_IALLOCATOR,
   OPT_COMPL_INST_ADD_NODES,
   OPT_COMPL_ONE_NODEGROUP,
diff --git a/lib/client/gnt_storage.py b/lib/client/gnt_storage.py
new file mode 100644
index 0000000..2ada46b
--- /dev/null
+++ b/lib/client/gnt_storage.py
@@ -0,0 +1,197 @@
+#
+#
+
+# Copyright (C) 2012 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+"""External Storage related commands"""
+
+# pylint: disable=W0401,W0613,W0614,C0103
+# W0401: Wildcard import ganeti.cli
+# W0613: Unused argument, since all functions follow the same API
+# W0614: Unused import %s from wildcard import (since we need cli)
+# C0103: Invalid name gnt-storage
+
+from ganeti.cli import *
+from ganeti import constants
+from ganeti import opcodes
+from ganeti import utils
+
+
+def ShowExtStorageInfo(opts, args):
+  """List detailed information about ExtStorage providers.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: empty list or list of ExtStorage providers' names
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  op = opcodes.OpExtStorageDiagnose(output_fields=["name", "nodegroup_status",
+                                                   "parameters"],
+                                    names=[])
+
+  result = SubmitOpCode(op, opts=opts)
+
+  if not result:
+    ToStderr("Can't get the ExtStorage providers list")
+    return 1
+
+  do_filter = bool(args)
+
+  for (name, nodegroup_data, parameters) in result:
+    if do_filter:
+      if name not in args:
+        continue
+      else:
+        args.remove(name)
+
+    nodegroups_valid = []
+    for nodegroup_name, nodegroup_status in nodegroup_data.iteritems():
+      if nodegroup_status:
+        nodegroups_valid.append(nodegroup_name)
+
+    ToStdout("%s:", name)
+
+    if nodegroups_valid != []:
+      ToStdout("  - Valid for nodegroups:")
+      for ndgrp in utils.NiceSort(nodegroups_valid):
+        ToStdout("      %s", ndgrp)
+      ToStdout("  - Supported parameters:")
+      for pname, pdesc in parameters:
+        ToStdout("      %s: %s", pname, pdesc)
+    else:
+      ToStdout("  - Invalid for all nodegroups")
+
+    ToStdout("")
+
+  if args:
+    for name in args:
+      ToStdout("%s: Not Found", name)
+      ToStdout("")
+
+  return 0
+
+
+def _ExtStorageStatus(status, diagnose):
+  """Beautifier function for ExtStorage status.
+
+  @type status: boolean
+  @param status: is the ExtStorage provider valid
+  @type diagnose: string
+  @param diagnose: the error message for invalid ExtStorages
+  @rtype: string
+  @return: a formatted status
+
+  """
+  if status:
+    return "valid"
+  else:
+    return "invalid - %s" % diagnose
+
+
+def DiagnoseExtStorage(opts, args):
+  """Analyse all ExtStorage providers.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: should be an empty list
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  op = opcodes.OpExtStorageDiagnose(output_fields=["name", "node_status",
+                                                   "nodegroup_status"],
+                                    names=[])
+
+  result = SubmitOpCode(op, opts=opts)
+
+  if not result:
+    ToStderr("Can't get the list of ExtStorage providers")
+    return 1
+
+  for provider_name, node_data, nodegroup_data in result:
+
+    nodes_valid = {}
+    nodes_bad = {}
+    nodegroups_valid = {}
+    nodegroups_bad = {}
+
+    # Per node diagnose
+    for node_name, node_info in node_data.iteritems():
+      if node_info: # at least one entry in the per-node list
+        (fo_path, fo_status, fo_msg, fo_params) = node_info.pop(0)
+        fo_msg = "%s (path: %s)" % (_ExtStorageStatus(fo_status, fo_msg),
+                                    fo_path)
+        if fo_params:
+          fo_msg += (" [parameters: %s]" %
+                     utils.CommaJoin([v[0] for v in fo_params]))
+        else:
+          fo_msg += " [no parameters]"
+        if fo_status:
+          nodes_valid[node_name] = fo_msg
+        else:
+          nodes_bad[node_name] = fo_msg
+      else:
+        nodes_bad[node_name] = "ExtStorage provider not found"
+
+    # Per nodegroup diagnose
+    for nodegroup_name, nodegroup_status in nodegroup_data.iteritems():
+      status = nodegroup_status
+      if status:
+        nodegroups_valid[nodegroup_name] = "valid"
+      else:
+        nodegroups_bad[nodegroup_name] = "invalid"
+
+    def _OutputPerNodegroupStatus(msg_map):
+      map_k = utils.NiceSort(msg_map.keys())
+      for nodegroup in map_k:
+        ToStdout("  For nodegroup: %s --> %s", nodegroup,
+                 msg_map[nodegroup])
+
+    def _OutputPerNodeStatus(msg_map):
+      map_k = utils.NiceSort(msg_map.keys())
+      for node_name in map_k:
+        ToStdout("  Node: %s, status: %s", node_name, msg_map[node_name])
+
+    # Print the output
+    st_msg = "Provider: %s"  % provider_name
+    ToStdout(st_msg)
+    ToStdout("---")
+    _OutputPerNodeStatus(nodes_valid)
+    _OutputPerNodeStatus(nodes_bad)
+    ToStdout("  --")
+    _OutputPerNodegroupStatus(nodegroups_valid)
+    _OutputPerNodegroupStatus(nodegroups_bad)
+    ToStdout("")
+
+  return 0
+
+
+commands = {
+  "diagnose": (
+    DiagnoseExtStorage, ARGS_NONE, [PRIORITY_OPT],
+    "", "Diagnose all ExtStorage providers"),
+  "info": (
+    ShowExtStorageInfo, [ArgOs()], [PRIORITY_OPT],
+    "", "Show info about ExtStorage providers"),
+  }
+
+
+def Main():
+  return GenericMain(commands)
diff --git a/lib/cmdlib.py b/lib/cmdlib.py
index 2e4beec..92b9c29 100644
--- a/lib/cmdlib.py
+++ b/lib/cmdlib.py
@@ -4849,6 +4849,159 @@ class LUOsDiagnose(NoHooksLU):
     return self.oq.OldStyleQuery(self)
 
 
+class _ExtStorageQuery(_QueryBase):
+  FIELDS = query.EXTSTORAGE_FIELDS
+
+  def ExpandNames(self, lu):
+    # Lock all nodes in shared mode
+    # Temporary removal of locks, should be reverted later
+    # TODO: reintroduce locks when they are lighter-weight
+    lu.needed_locks = {}
+    #self.share_locks[locking.LEVEL_NODE] = 1
+    #self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET
+
+    # The following variables interact with _QueryBase._GetNames
+    if self.names:
+      self.wanted = self.names
+    else:
+      self.wanted = locking.ALL_SET
+
+    self.do_locking = self.use_locking
+
+  def DeclareLocks(self, lu, level):
+    pass
+
+  @staticmethod
+  def _DiagnoseByProvider(rlist):
+    """Remaps a per-node return list into an a per-provider per-node dictionary
+
+    @param rlist: a map with node names as keys and ExtStorage objects as 
values
+
+    @rtype: dict
+    @return: a dictionary with extstorage providers as keys and as
+        value another map, with nodes as keys and tuples of
+        (path, status, diagnose, parameters) as values, eg::
+
+          {"provider1": {"node1": [(/usr/lib/..., True, "", [])]
+                         "node2": [(/srv/..., False, "missing file")]
+                         "node3": [(/srv/..., True, "", [])]
+          }
+
+    """
+    all_es = {}
+    # we build here the list of nodes that didn't fail the RPC (at RPC
+    # level), so that nodes with a non-responding node daemon don't
+    # make all OSes invalid
+    good_nodes = [node_name for node_name in rlist
+                  if not rlist[node_name].fail_msg]
+    for node_name, nr in rlist.items():
+      if nr.fail_msg or not nr.payload:
+        continue
+      for (name, path, status, diagnose, params) in nr.payload:
+        if name not in all_es:
+          # build a list of nodes for this os containing empty lists
+          # for each node in node_list
+          all_es[name] = {}
+          for nname in good_nodes:
+            all_es[name][nname] = []
+        # convert params from [name, help] to (name, help)
+        params = [tuple(v) for v in params]
+        all_es[name][node_name].append((path, status, diagnose, params))
+    return all_es
+
+  def _GetQueryData(self, lu):
+    """Computes the list of nodes and their attributes.
+
+    """
+    # Locking is not used
+    assert not (compat.any(lu.glm.is_owned(level)
+                           for level in locking.LEVELS
+                           if level != locking.LEVEL_CLUSTER) or
+                self.do_locking or self.use_locking)
+
+    valid_nodes = [node.name
+                   for node in lu.cfg.GetAllNodesInfo().values()
+                   if not node.offline and node.vm_capable]
+    pol = 
self._DiagnoseByProvider(lu.rpc.call_extstorage_diagnose(valid_nodes))
+
+    data = {}
+
+    nodegroup_list = lu.cfg.GetNodeGroupList()
+
+    for (es_name, es_data) in pol.items():
+      # For every provider compute the nodegroup validity.
+      # To do this we need to check the validity of each node in es_data
+      # and then construct the corresponding nodegroup dict:
+      #      { nodegroup1: status
+      #        nodegroup2: status
+      #      }
+      ndgrp_data = {}
+      for nodegroup in nodegroup_list:
+        ndgrp = lu.cfg.GetNodeGroup(nodegroup)
+
+        nodegroup_nodes = ndgrp.members
+        nodegroup_name = ndgrp.name
+        node_statuses = []
+
+        for node in nodegroup_nodes:
+          if node in valid_nodes:
+            if es_data[node] != []:
+              node_status = es_data[node][0][1]
+              node_statuses.append(node_status)
+            else:
+              node_statuses.append(False)
+
+        if False in node_statuses:
+          ndgrp_data[nodegroup_name] = False
+        else:
+          ndgrp_data[nodegroup_name] = True
+
+      # Compute the provider's parameters
+      parameters = set()
+      for idx, esl in enumerate(es_data.values()):
+        valid = bool(esl and esl[0][1])
+        if not valid:
+          break
+
+        node_params = esl[0][3]
+        if idx == 0:
+          # First entry
+          parameters.update(node_params)
+        else:
+          # Filter out inconsistent values
+          parameters.intersection_update(node_params)
+
+      params = list(parameters)
+
+      # Now fill all the info for this provider
+      info = query.ExtStorageInfo(name=es_name, node_status=es_data,
+                                  nodegroup_status=ndgrp_data,
+                                  parameters=params)
+
+      data[es_name] = info
+
+    # Prepare data in requested order
+    return [data[name] for name in self._GetNames(lu, pol.keys(), None)
+            if name in data]
+
+
+class LUExtStorageDiagnose(NoHooksLU):
+  """Logical unit for ExtStorage diagnose/query.
+
+  """
+  REQ_BGL = False
+
+  def CheckArguments(self):
+    self.eq = _ExtStorageQuery(qlang.MakeSimpleFilter("name", self.op.names),
+                               self.op.output_fields, False)
+
+  def ExpandNames(self):
+    self.eq.ExpandNames(self)
+
+  def Exec(self, feedback_fn):
+    return self.eq.OldStyleQuery(self)
+
+
 class LUNodeRemove(LogicalUnit):
   """Logical unit for removing a node.
 
@@ -15164,6 +15317,7 @@ _QUERY_IMPL = {
   constants.QR_NODE: _NodeQuery,
   constants.QR_GROUP: _GroupQuery,
   constants.QR_OS: _OsQuery,
+  constants.QR_EXTSTORAGE: _ExtStorageQuery,
   }
 
 assert set(_QUERY_IMPL.keys()) == constants.QR_VIA_OP
diff --git a/lib/constants.py b/lib/constants.py
index 4752cf8..23959d1 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -1663,9 +1663,10 @@ QR_NODE = "node"
 QR_LOCK = "lock"
 QR_GROUP = "group"
 QR_OS = "os"
+QR_EXTSTORAGE = "extstorage"
 
 #: List of resources which can be queried using L{opcodes.OpQuery}
-QR_VIA_OP = frozenset([QR_INSTANCE, QR_NODE, QR_GROUP, QR_OS])
+QR_VIA_OP = frozenset([QR_INSTANCE, QR_NODE, QR_GROUP, QR_OS, QR_EXTSTORAGE])
 
 #: List of resources which can be queried using Local UniX Interface
 QR_VIA_LUXI = QR_VIA_OP.union([
diff --git a/lib/opcodes.py b/lib/opcodes.py
index 254c96f..719ede0 100644
--- a/lib/opcodes.py
+++ b/lib/opcodes.py
@@ -1706,6 +1706,16 @@ class OpOsDiagnose(OpCode):
   OP_RESULT = _TOldQueryResult
 
 
+# ExtStorage opcodes
+class OpExtStorageDiagnose(OpCode):
+  """Compute the list of external storage providers."""
+  OP_PARAMS = [
+    _POutputFields,
+    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
+     "Which ExtStorage Provider to diagnose"),
+    ]
+
+
 # Exports opcodes
 class OpBackupQuery(OpCode):
   """Compute the list of exported images."""
diff --git a/lib/query.py b/lib/query.py
index 89681b8..51d2684 100644
--- a/lib/query.py
+++ b/lib/query.py
@@ -2133,6 +2133,36 @@ def _BuildOsFields():
   return _PrepareFieldList(fields, [])
 
 
+class ExtStorageInfo(objects.ConfigObject):
+  __slots__ = [
+    "name",
+    "node_status",
+    "nodegroup_status",
+    "parameters",
+    ]
+
+
+def _BuildExtStorageFields():
+  """Builds list of fields for extstorage provider queries.
+
+  """
+  fields = [
+    (_MakeField("name", "Name", QFT_TEXT, "ExtStorage provider name"),
+     None, 0, _GetItemAttr("name")),
+    (_MakeField("node_status", "NodeStatus", QFT_OTHER,
+                "Status from node"),
+     None, 0, _GetItemAttr("node_status")),
+    (_MakeField("nodegroup_status", "NodegroupStatus", QFT_OTHER,
+                "Overall Nodegroup status"),
+     None, 0, _GetItemAttr("nodegroup_status")),
+    (_MakeField("parameters", "Parameters", QFT_OTHER,
+                "ExtStorage provider parameters"),
+     None, 0, _GetItemAttr("parameters")),
+  ]
+
+  return _PrepareFieldList(fields, [])
+
+
 #: Fields available for node queries
 NODE_FIELDS = _BuildNodeFields()
 
@@ -2148,6 +2178,9 @@ GROUP_FIELDS = _BuildGroupFields()
 #: Fields available for operating system queries
 OS_FIELDS = _BuildOsFields()
 
+#: Fields available for extstorage provider queries
+EXTSTORAGE_FIELDS = _BuildExtStorageFields()
+
 #: All available resources
 ALL_FIELDS = {
   constants.QR_INSTANCE: INSTANCE_FIELDS,
@@ -2155,6 +2188,7 @@ ALL_FIELDS = {
   constants.QR_LOCK: LOCK_FIELDS,
   constants.QR_GROUP: GROUP_FIELDS,
   constants.QR_OS: OS_FIELDS,
+  constants.QR_EXTSTORAGE: EXTSTORAGE_FIELDS,
   }
 
 #: All available field lists
diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py
index 44bdc4c..685e149 100644
--- a/lib/rpc_defs.py
+++ b/lib/rpc_defs.py
@@ -433,6 +433,11 @@ _OS_CALLS = [
     ], None, _OsGetPostProc, "Returns an OS definition"),
   ]
 
+_EXTSTORAGE_CALLS = [
+  ("extstorage_diagnose", MULTI, None, TMO_FAST, [], None, None,
+   "Request a diagnose of ExtStorage Providers"),
+  ]
+
 _NODE_CALLS = [
   ("node_has_ip_address", SINGLE, None, TMO_FAST, [
     ("address", None, "IP address"),
@@ -501,7 +506,7 @@ CALLS = {
   "RpcClientDefault": \
     _Prepare(_IMPEXP_CALLS + _X509_CALLS + _OS_CALLS + _NODE_CALLS +
              _FILE_STORAGE_CALLS + _MISC_CALLS + _INSTANCE_CALLS +
-             _BLOCKDEV_CALLS + _STORAGE_CALLS),
+             _BLOCKDEV_CALLS + _STORAGE_CALLS + _EXTSTORAGE_CALLS),
   "RpcClientJobQueue": _Prepare([
     ("jobqueue_update", MULTI, None, TMO_URGENT, [
       ("file_name", None, None),
diff --git a/lib/server/noded.py b/lib/server/noded.py
index d95680a..efa4717 100644
--- a/lib/server/noded.py
+++ b/lib/server/noded.py
@@ -830,6 +830,15 @@ class NodeRequestHandler(http.server.HttpServerHandler):
     required, name, checks, params = params
     return backend.ValidateOS(required, name, checks, params)
 
+  # extstorage -----------------------
+
+  @staticmethod
+  def perspective_extstorage_diagnose(params):
+    """Query detailed information about existing extstorage providers.
+
+    """
+    return backend.DiagnoseExtStorage()
+
   # hooks -----------------------
 
   @staticmethod
-- 
1.7.2.5

Reply via email to