Revision b954f097

b/Makefile.am
282 282
	lib/client/gnt_job.py \
283 283
	lib/client/gnt_node.py \
284 284
	lib/client/gnt_network.py \
285
	lib/client/gnt_os.py
285
	lib/client/gnt_os.py \
286
	lib/client/gnt_storage.py
286 287

  
287 288
hypervisor_PYTHON = \
288 289
	lib/hypervisor/__init__.py \
......
621 622
	scripts/gnt-job \
622 623
	scripts/gnt-network \
623 624
	scripts/gnt-node \
624
	scripts/gnt-os
625
	scripts/gnt-os \
626
	scripts/gnt-storage
625 627

  
626 628
PYTHON_BOOTSTRAP_SBIN = \
627 629
	daemons/ganeti-masterd \
b/autotools/build-bash-completion
357 357
          WriteCompReply(sw, "-W \"$(_ganeti_instances)\"", cur=cur)
358 358
        elif suggest == cli.OPT_COMPL_ONE_OS:
359 359
          WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
360
        elif suggest == cli.OPT_COMPL_ONE_EXTSTORAGE:
361
          WriteCompReply(sw, "-W \"$(_ganeti_extstorage)\"", cur=cur)
360 362
        elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
361 363
          WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
362 364
        elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
......
467 469
          choices = "$(_ganeti_jobs)"
468 470
        elif isinstance(arg, cli.ArgOs):
469 471
          choices = "$(_ganeti_os)"
472
        elif isinstance(arg, cli.ArgExtStorage):
473
          choices = "$(_ganeti_extstorage)"
470 474
        elif isinstance(arg, cli.ArgFile):
471 475
          choices = ""
472 476
          compgenargs.append("-f")
b/htest/Test/Ganeti/OpCodes.hs
291 291
          genMaybe genNameNE <*> genMaybe genNamesNE
292 292
      "OP_OS_DIAGNOSE" ->
293 293
        OpCodes.OpOsDiagnose <$> genFieldsNE <*> genNamesNE
294
      "OP_EXT_STORAGE_DIAGNOSE" ->
295
        OpCodes.OpOsDiagnose <$> genFieldsNE <*> genNamesNE
294 296
      "OP_BACKUP_QUERY" ->
295 297
        OpCodes.OpBackupQuery <$> arbitrary <*> genNodeNamesNE
296 298
      "OP_BACKUP_PREPARE" ->
b/htools/Ganeti/OpCodes.hs
444 444
  , ("OpOsDiagnose",
445 445
     [ pOutputFields
446 446
     , pNames ])
447
  , ("OpExtStorageDiagnose",
448
     [ pOutputFields
449
     , pNames ])
447 450
  , ("OpBackupQuery",
448 451
     [ pUseLocking
449 452
     , pNodes
b/lib/backend.py
2481 2481
  return result
2482 2482

  
2483 2483

  
2484
def DiagnoseExtStorage(top_dirs=None):
2485
  """Compute the validity for all ExtStorage Providers.
2486

  
2487
  @type top_dirs: list
2488
  @param top_dirs: the list of directories in which to
2489
      search (if not given defaults to
2490
      L{pathutils.ES_SEARCH_PATH})
2491
  @rtype: list of L{objects.ExtStorage}
2492
  @return: a list of tuples (name, path, status, diagnose, parameters)
2493
      for all (potential) ExtStorage Providers under all
2494
      search paths, where:
2495
          - name is the (potential) ExtStorage Provider
2496
          - path is the full path to the ExtStorage Provider
2497
          - status True/False is the validity of the ExtStorage Provider
2498
          - diagnose is the error message for an invalid ExtStorage Provider,
2499
            otherwise empty
2500
          - parameters is a list of (name, help) parameters, if any
2501

  
2502
  """
2503
  if top_dirs is None:
2504
    top_dirs = pathutils.ES_SEARCH_PATH
2505

  
2506
  result = []
2507
  for dir_name in top_dirs:
2508
    if os.path.isdir(dir_name):
2509
      try:
2510
        f_names = utils.ListVisibleFiles(dir_name)
2511
      except EnvironmentError, err:
2512
        logging.exception("Can't list the ExtStorage directory %s: %s",
2513
                          dir_name, err)
2514
        break
2515
      for name in f_names:
2516
        es_path = utils.PathJoin(dir_name, name)
2517
        status, es_inst = bdev.ExtStorageFromDisk(name, base_dir=dir_name)
2518
        if status:
2519
          diagnose = ""
2520
          parameters = es_inst.supported_parameters
2521
        else:
2522
          diagnose = es_inst
2523
          parameters = []
2524
        result.append((name, es_path, status, diagnose, parameters))
2525

  
2526
  return result
2527

  
2528

  
2484 2529
def BlockdevGrow(disk, amount, dryrun, backingstore):
2485 2530
  """Grow a stack of block devices.
2486 2531

  
b/lib/cli.py
262 262
  "ArgNetwork",
263 263
  "ArgNode",
264 264
  "ArgOs",
265
  "ArgExtStorage",
265 266
  "ArgSuggest",
266 267
  "ArgUnknown",
267 268
  "OPT_COMPL_INST_ADD_NODES",
......
272 273
  "OPT_COMPL_ONE_NODEGROUP",
273 274
  "OPT_COMPL_ONE_NETWORK",
274 275
  "OPT_COMPL_ONE_OS",
276
  "OPT_COMPL_ONE_EXTSTORAGE",
275 277
  "cli_option",
276 278
  "SplitNodeOption",
277 279
  "CalculateOSNames",
......
422 424
  """
423 425

  
424 426

  
427
class ArgExtStorage(_Argument):
428
  """ExtStorage argument.
429

  
430
  """
431

  
432

  
425 433
ARGS_NONE = []
426 434
ARGS_MANY_INSTANCES = [ArgInstance()]
427 435
ARGS_MANY_NETWORKS = [ArgNetwork()]
......
672 680
 OPT_COMPL_ONE_NODE,
673 681
 OPT_COMPL_ONE_INSTANCE,
674 682
 OPT_COMPL_ONE_OS,
683
 OPT_COMPL_ONE_EXTSTORAGE,
675 684
 OPT_COMPL_ONE_IALLOCATOR,
676 685
 OPT_COMPL_ONE_NETWORK,
677 686
 OPT_COMPL_INST_ADD_NODES,
678
 OPT_COMPL_ONE_NODEGROUP) = range(100, 108)
687
 OPT_COMPL_ONE_NODEGROUP) = range(100, 109)
679 688

  
680 689
OPT_COMPL_ALL = compat.UniqueFrozenset([
681 690
  OPT_COMPL_MANY_NODES,
682 691
  OPT_COMPL_ONE_NODE,
683 692
  OPT_COMPL_ONE_INSTANCE,
684 693
  OPT_COMPL_ONE_OS,
694
  OPT_COMPL_ONE_EXTSTORAGE,
685 695
  OPT_COMPL_ONE_IALLOCATOR,
686 696
  OPT_COMPL_ONE_NETWORK,
687 697
  OPT_COMPL_INST_ADD_NODES,
b/lib/client/gnt_storage.py
1
#
2
#
3

  
4
# Copyright (C) 2012 Google Inc.
5
#
6
# This program is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 2 of the License, or
9
# (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful, but
12
# WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14
# General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19
# 02110-1301, USA.
20

  
21
"""External Storage related commands"""
22

  
23
# pylint: disable=W0401,W0613,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0613: Unused argument, since all functions follow the same API
26
# W0614: Unused import %s from wildcard import (since we need cli)
27
# C0103: Invalid name gnt-storage
28

  
29
from ganeti.cli import *
30
from ganeti import opcodes
31
from ganeti import utils
32

  
33

  
34
def ShowExtStorageInfo(opts, args):
35
  """List detailed information about ExtStorage providers.
36

  
37
  @param opts: the command line options selected by the user
38
  @type args: list
39
  @param args: empty list or list of ExtStorage providers' names
40
  @rtype: int
41
  @return: the desired exit code
42

  
43
  """
44
  op = opcodes.OpExtStorageDiagnose(output_fields=["name", "nodegroup_status",
45
                                                   "parameters"],
46
                                    names=[])
47

  
48
  result = SubmitOpCode(op, opts=opts)
49

  
50
  if not result:
51
    ToStderr("Can't get the ExtStorage providers list")
52
    return 1
53

  
54
  do_filter = bool(args)
55

  
56
  for (name, nodegroup_data, parameters) in result:
57
    if do_filter:
58
      if name not in args:
59
        continue
60
      else:
61
        args.remove(name)
62

  
63
    nodegroups_valid = []
64
    for nodegroup_name, nodegroup_status in nodegroup_data.iteritems():
65
      if nodegroup_status:
66
        nodegroups_valid.append(nodegroup_name)
67

  
68
    ToStdout("%s:", name)
69

  
70
    if nodegroups_valid != []:
71
      ToStdout("  - Valid for nodegroups:")
72
      for ndgrp in utils.NiceSort(nodegroups_valid):
73
        ToStdout("      %s", ndgrp)
74
      ToStdout("  - Supported parameters:")
75
      for pname, pdesc in parameters:
76
        ToStdout("      %s: %s", pname, pdesc)
77
    else:
78
      ToStdout("  - Invalid for all nodegroups")
79

  
80
    ToStdout("")
81

  
82
  if args:
83
    for name in args:
84
      ToStdout("%s: Not Found", name)
85
      ToStdout("")
86

  
87
  return 0
88

  
89

  
90
def _ExtStorageStatus(status, diagnose):
91
  """Beautifier function for ExtStorage status.
92

  
93
  @type status: boolean
94
  @param status: is the ExtStorage provider valid
95
  @type diagnose: string
96
  @param diagnose: the error message for invalid ExtStorages
97
  @rtype: string
98
  @return: a formatted status
99

  
100
  """
101
  if status:
102
    return "valid"
103
  else:
104
    return "invalid - %s" % diagnose
105

  
106

  
107
def DiagnoseExtStorage(opts, args):
108
  """Analyse all ExtStorage providers.
109

  
110
  @param opts: the command line options selected by the user
111
  @type args: list
112
  @param args: should be an empty list
113
  @rtype: int
114
  @return: the desired exit code
115

  
116
  """
117
  op = opcodes.OpExtStorageDiagnose(output_fields=["name", "node_status",
118
                                                   "nodegroup_status"],
119
                                    names=[])
120

  
121
  result = SubmitOpCode(op, opts=opts)
122

  
123
  if not result:
124
    ToStderr("Can't get the list of ExtStorage providers")
125
    return 1
126

  
127
  for provider_name, node_data, nodegroup_data in result:
128

  
129
    nodes_valid = {}
130
    nodes_bad = {}
131
    nodegroups_valid = {}
132
    nodegroups_bad = {}
133

  
134
    # Per node diagnose
135
    for node_name, node_info in node_data.iteritems():
136
      if node_info: # at least one entry in the per-node list
137
        (fo_path, fo_status, fo_msg, fo_params) = node_info.pop(0)
138
        fo_msg = "%s (path: %s)" % (_ExtStorageStatus(fo_status, fo_msg),
139
                                    fo_path)
140
        if fo_params:
141
          fo_msg += (" [parameters: %s]" %
142
                     utils.CommaJoin([v[0] for v in fo_params]))
143
        else:
144
          fo_msg += " [no parameters]"
145
        if fo_status:
146
          nodes_valid[node_name] = fo_msg
147
        else:
148
          nodes_bad[node_name] = fo_msg
149
      else:
150
        nodes_bad[node_name] = "ExtStorage provider not found"
151

  
152
    # Per nodegroup diagnose
153
    for nodegroup_name, nodegroup_status in nodegroup_data.iteritems():
154
      status = nodegroup_status
155
      if status:
156
        nodegroups_valid[nodegroup_name] = "valid"
157
      else:
158
        nodegroups_bad[nodegroup_name] = "invalid"
159

  
160
    def _OutputPerNodegroupStatus(msg_map):
161
      map_k = utils.NiceSort(msg_map.keys())
162
      for nodegroup in map_k:
163
        ToStdout("  For nodegroup: %s --> %s", nodegroup,
164
                 msg_map[nodegroup])
165

  
166
    def _OutputPerNodeStatus(msg_map):
167
      map_k = utils.NiceSort(msg_map.keys())
168
      for node_name in map_k:
169
        ToStdout("  Node: %s, status: %s", node_name, msg_map[node_name])
170

  
171
    # Print the output
172
    st_msg = "Provider: %s" % provider_name
173
    ToStdout(st_msg)
174
    ToStdout("---")
175
    _OutputPerNodeStatus(nodes_valid)
176
    _OutputPerNodeStatus(nodes_bad)
177
    ToStdout("  --")
178
    _OutputPerNodegroupStatus(nodegroups_valid)
179
    _OutputPerNodegroupStatus(nodegroups_bad)
180
    ToStdout("")
181

  
182
  return 0
183

  
184

  
185
commands = {
186
  "diagnose": (
187
    DiagnoseExtStorage, ARGS_NONE, [PRIORITY_OPT],
188
    "", "Diagnose all ExtStorage providers"),
189
  "info": (
190
    ShowExtStorageInfo, [ArgOs()], [PRIORITY_OPT],
191
    "", "Show info about ExtStorage providers"),
192
  }
193

  
194

  
195
def Main():
196
  return GenericMain(commands)
b/lib/cmdlib.py
5163 5163
    return self.oq.OldStyleQuery(self)
5164 5164

  
5165 5165

  
5166
class _ExtStorageQuery(_QueryBase):
5167
  FIELDS = query.EXTSTORAGE_FIELDS
5168

  
5169
  def ExpandNames(self, lu):
5170
    # Lock all nodes in shared mode
5171
    # Temporary removal of locks, should be reverted later
5172
    # TODO: reintroduce locks when they are lighter-weight
5173
    lu.needed_locks = {}
5174
    #self.share_locks[locking.LEVEL_NODE] = 1
5175
    #self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET
5176

  
5177
    # The following variables interact with _QueryBase._GetNames
5178
    if self.names:
5179
      self.wanted = self.names
5180
    else:
5181
      self.wanted = locking.ALL_SET
5182

  
5183
    self.do_locking = self.use_locking
5184

  
5185
  def DeclareLocks(self, lu, level):
5186
    pass
5187

  
5188
  @staticmethod
5189
  def _DiagnoseByProvider(rlist):
5190
    """Remaps a per-node return list into an a per-provider per-node dictionary
5191

  
5192
    @param rlist: a map with node names as keys and ExtStorage objects as values
5193

  
5194
    @rtype: dict
5195
    @return: a dictionary with extstorage providers as keys and as
5196
        value another map, with nodes as keys and tuples of
5197
        (path, status, diagnose, parameters) as values, eg::
5198

  
5199
          {"provider1": {"node1": [(/usr/lib/..., True, "", [])]
5200
                         "node2": [(/srv/..., False, "missing file")]
5201
                         "node3": [(/srv/..., True, "", [])]
5202
          }
5203

  
5204
    """
5205
    all_es = {}
5206
    # we build here the list of nodes that didn't fail the RPC (at RPC
5207
    # level), so that nodes with a non-responding node daemon don't
5208
    # make all OSes invalid
5209
    good_nodes = [node_name for node_name in rlist
5210
                  if not rlist[node_name].fail_msg]
5211
    for node_name, nr in rlist.items():
5212
      if nr.fail_msg or not nr.payload:
5213
        continue
5214
      for (name, path, status, diagnose, params) in nr.payload:
5215
        if name not in all_es:
5216
          # build a list of nodes for this os containing empty lists
5217
          # for each node in node_list
5218
          all_es[name] = {}
5219
          for nname in good_nodes:
5220
            all_es[name][nname] = []
5221
        # convert params from [name, help] to (name, help)
5222
        params = [tuple(v) for v in params]
5223
        all_es[name][node_name].append((path, status, diagnose, params))
5224
    return all_es
5225

  
5226
  def _GetQueryData(self, lu):
5227
    """Computes the list of nodes and their attributes.
5228

  
5229
    """
5230
    # Locking is not used
5231
    assert not (compat.any(lu.glm.is_owned(level)
5232
                           for level in locking.LEVELS
5233
                           if level != locking.LEVEL_CLUSTER) or
5234
                self.do_locking or self.use_locking)
5235

  
5236
    valid_nodes = [node.name
5237
                   for node in lu.cfg.GetAllNodesInfo().values()
5238
                   if not node.offline and node.vm_capable]
5239
    pol = self._DiagnoseByProvider(lu.rpc.call_extstorage_diagnose(valid_nodes))
5240

  
5241
    data = {}
5242

  
5243
    nodegroup_list = lu.cfg.GetNodeGroupList()
5244

  
5245
    for (es_name, es_data) in pol.items():
5246
      # For every provider compute the nodegroup validity.
5247
      # To do this we need to check the validity of each node in es_data
5248
      # and then construct the corresponding nodegroup dict:
5249
      #      { nodegroup1: status
5250
      #        nodegroup2: status
5251
      #      }
5252
      ndgrp_data = {}
5253
      for nodegroup in nodegroup_list:
5254
        ndgrp = lu.cfg.GetNodeGroup(nodegroup)
5255

  
5256
        nodegroup_nodes = ndgrp.members
5257
        nodegroup_name = ndgrp.name
5258
        node_statuses = []
5259

  
5260
        for node in nodegroup_nodes:
5261
          if node in valid_nodes:
5262
            if es_data[node] != []:
5263
              node_status = es_data[node][0][1]
5264
              node_statuses.append(node_status)
5265
            else:
5266
              node_statuses.append(False)
5267

  
5268
        if False in node_statuses:
5269
          ndgrp_data[nodegroup_name] = False
5270
        else:
5271
          ndgrp_data[nodegroup_name] = True
5272

  
5273
      # Compute the provider's parameters
5274
      parameters = set()
5275
      for idx, esl in enumerate(es_data.values()):
5276
        valid = bool(esl and esl[0][1])
5277
        if not valid:
5278
          break
5279

  
5280
        node_params = esl[0][3]
5281
        if idx == 0:
5282
          # First entry
5283
          parameters.update(node_params)
5284
        else:
5285
          # Filter out inconsistent values
5286
          parameters.intersection_update(node_params)
5287

  
5288
      params = list(parameters)
5289

  
5290
      # Now fill all the info for this provider
5291
      info = query.ExtStorageInfo(name=es_name, node_status=es_data,
5292
                                  nodegroup_status=ndgrp_data,
5293
                                  parameters=params)
5294

  
5295
      data[es_name] = info
5296

  
5297
    # Prepare data in requested order
5298
    return [data[name] for name in self._GetNames(lu, pol.keys(), None)
5299
            if name in data]
5300

  
5301

  
5302
class LUExtStorageDiagnose(NoHooksLU):
5303
  """Logical unit for ExtStorage diagnose/query.
5304

  
5305
  """
5306
  REQ_BGL = False
5307

  
5308
  def CheckArguments(self):
5309
    self.eq = _ExtStorageQuery(qlang.MakeSimpleFilter("name", self.op.names),
5310
                               self.op.output_fields, False)
5311

  
5312
  def ExpandNames(self):
5313
    self.eq.ExpandNames(self)
5314

  
5315
  def Exec(self, feedback_fn):
5316
    return self.eq.OldStyleQuery(self)
5317

  
5318

  
5166 5319
class LUNodeRemove(LogicalUnit):
5167 5320
  """Logical unit for removing a node.
5168 5321

  
......
16604 16757
  constants.QR_GROUP: _GroupQuery,
16605 16758
  constants.QR_NETWORK: _NetworkQuery,
16606 16759
  constants.QR_OS: _OsQuery,
16760
  constants.QR_EXTSTORAGE: _ExtStorageQuery,
16607 16761
  constants.QR_EXPORT: _ExportQuery,
16608 16762
  }
16609 16763

  
b/lib/constants.py
1732 1732
QR_JOB = "job"
1733 1733
QR_EXPORT = "export"
1734 1734
QR_NETWORK = "network"
1735
QR_EXTSTORAGE = "extstorage"
1735 1736

  
1736 1737
#: List of resources which can be queried using L{opcodes.OpQuery}
1737 1738
QR_VIA_OP = compat.UniqueFrozenset([
......
1742 1743
  QR_OS,
1743 1744
  QR_EXPORT,
1744 1745
  QR_NETWORK,
1746
  QR_EXTSTORAGE,
1745 1747
  ])
1746 1748

  
1747 1749
#: List of resources which can be queried using Local UniX Interface
b/lib/opcodes.py
1807 1807
  OP_RESULT = _TOldQueryResult
1808 1808

  
1809 1809

  
1810
# ExtStorage opcodes
1811
class OpExtStorageDiagnose(OpCode):
1812
  """Compute the list of external storage providers."""
1813
  OP_PARAMS = [
1814
    _POutputFields,
1815
    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
1816
     "Which ExtStorage Provider to diagnose"),
1817
    ]
1818
  OP_RESULT = _TOldQueryResult
1819

  
1820

  
1810 1821
# Exports opcodes
1811 1822
class OpBackupQuery(OpCode):
1812 1823
  """Compute the list of exported images."""
b/lib/query.py
2234 2234
  return _PrepareFieldList(fields, [])
2235 2235

  
2236 2236

  
2237
class ExtStorageInfo(objects.ConfigObject):
2238
  __slots__ = [
2239
    "name",
2240
    "node_status",
2241
    "nodegroup_status",
2242
    "parameters",
2243
    ]
2244

  
2245

  
2246
def _BuildExtStorageFields():
2247
  """Builds list of fields for extstorage provider queries.
2248

  
2249
  """
2250
  fields = [
2251
    (_MakeField("name", "Name", QFT_TEXT, "ExtStorage provider name"),
2252
     None, 0, _GetItemAttr("name")),
2253
    (_MakeField("node_status", "NodeStatus", QFT_OTHER,
2254
                "Status from node"),
2255
     None, 0, _GetItemAttr("node_status")),
2256
    (_MakeField("nodegroup_status", "NodegroupStatus", QFT_OTHER,
2257
                "Overall Nodegroup status"),
2258
     None, 0, _GetItemAttr("nodegroup_status")),
2259
    (_MakeField("parameters", "Parameters", QFT_OTHER,
2260
                "ExtStorage provider parameters"),
2261
     None, 0, _GetItemAttr("parameters")),
2262
    ]
2263

  
2264
  return _PrepareFieldList(fields, [])
2265

  
2266

  
2237 2267
def _JobUnavailInner(fn, ctx, (job_id, job)): # pylint: disable=W0613
2238 2268
  """Return L{_FS_UNAVAIL} if job is None.
2239 2269

  
......
2595 2625
#: Fields available for operating system queries
2596 2626
OS_FIELDS = _BuildOsFields()
2597 2627

  
2628
#: Fields available for extstorage provider queries
2629
EXTSTORAGE_FIELDS = _BuildExtStorageFields()
2630

  
2598 2631
#: Fields available for job queries
2599 2632
JOB_FIELDS = _BuildJobFields()
2600 2633

  
......
2612 2645
  constants.QR_LOCK: LOCK_FIELDS,
2613 2646
  constants.QR_GROUP: GROUP_FIELDS,
2614 2647
  constants.QR_OS: OS_FIELDS,
2648
  constants.QR_EXTSTORAGE: EXTSTORAGE_FIELDS,
2615 2649
  constants.QR_JOB: JOB_FIELDS,
2616 2650
  constants.QR_EXPORT: EXPORT_FIELDS,
2617 2651
  constants.QR_NETWORK: NETWORK_FIELDS,
b/lib/rpc_defs.py
451 451
    ], None, _OsGetPostProc, "Returns an OS definition"),
452 452
  ]
453 453

  
454
_EXTSTORAGE_CALLS = [
455
  ("extstorage_diagnose", MULTI, None, constants.RPC_TMO_FAST, [], None, None,
456
   "Request a diagnose of ExtStorage Providers"),
457
  ]
458

  
454 459
_NODE_CALLS = [
455 460
  ("node_has_ip_address", SINGLE, None, constants.RPC_TMO_FAST, [
456 461
    ("address", None, "IP address"),
......
530 535
  "RpcClientDefault":
531 536
    _Prepare(_IMPEXP_CALLS + _X509_CALLS + _OS_CALLS + _NODE_CALLS +
532 537
             _FILE_STORAGE_CALLS + _MISC_CALLS + _INSTANCE_CALLS +
533
             _BLOCKDEV_CALLS + _STORAGE_CALLS),
538
             _BLOCKDEV_CALLS + _STORAGE_CALLS + _EXTSTORAGE_CALLS),
534 539
  "RpcClientJobQueue": _Prepare([
535 540
    ("jobqueue_update", MULTI, None, constants.RPC_TMO_URGENT, [
536 541
      ("file_name", None, None),
b/lib/server/noded.py
869 869
    required, name, checks, params = params
870 870
    return backend.ValidateOS(required, name, checks, params)
871 871

  
872
  # extstorage -----------------------
873

  
874
  @staticmethod
875
  def perspective_extstorage_diagnose(params):
876
    """Query detailed information about existing extstorage providers.
877

  
878
    """
879
    return backend.DiagnoseExtStorage()
880

  
872 881
  # hooks -----------------------
873 882

  
874 883
  @staticmethod
b/test/docs_unittest.py
58 58
  opcodes.OpTagsSearch,
59 59
  opcodes.OpClusterActivateMasterIp,
60 60
  opcodes.OpClusterDeactivateMasterIp,
61
  opcodes.OpExtStorageDiagnose,
61 62

  
62 63
  # Difficult if not impossible
63 64
  opcodes.OpClusterDestroy,

Also available in: Unified diff