Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 620a85fd

History | View | Annotate | Download (21.3 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008 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

    
22
# pylint: disable-msg=W0401,W0614
23
# W0401: Wildcard import ganeti.cli
24
# W0614: Unused import %s from wildcard import (since we need cli)
25

    
26
import sys
27

    
28
from ganeti.cli import *
29
from ganeti import cli
30
from ganeti import opcodes
31
from ganeti import utils
32
from ganeti import constants
33
from ganeti import errors
34
from ganeti import bootstrap
35

    
36

    
37
#: default list of field for L{ListNodes}
38
_LIST_DEF_FIELDS = [
39
  "name", "dtotal", "dfree",
40
  "mtotal", "mnode", "mfree",
41
  "pinst_cnt", "sinst_cnt",
42
  ]
43

    
44

    
45
#: default list of field for L{ListStorage}
46
_LIST_STOR_DEF_FIELDS = [
47
  constants.SF_NODE,
48
  constants.SF_TYPE,
49
  constants.SF_NAME,
50
  constants.SF_SIZE,
51
  constants.SF_USED,
52
  constants.SF_FREE,
53
  constants.SF_ALLOCATABLE,
54
  ]
55

    
56

    
57
#: headers (and full field list for L{ListNodes}
58
_LIST_HEADERS = {
59
  "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
60
  "pinst_list": "PriInstances", "sinst_list": "SecInstances",
61
  "pip": "PrimaryIP", "sip": "SecondaryIP",
62
  "dtotal": "DTotal", "dfree": "DFree",
63
  "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
64
  "bootid": "BootID",
65
  "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
66
  "tags": "Tags",
67
  "serial_no": "SerialNo",
68
  "master_candidate": "MasterC",
69
  "master": "IsMaster",
70
  "offline": "Offline", "drained": "Drained",
71
  "role": "Role",
72
  "ctime": "CTime", "mtime": "MTime", "uuid": "UUID"
73
  }
74

    
75

    
76
#: headers (and full field list for L{ListStorage}
77
_LIST_STOR_HEADERS = {
78
  constants.SF_NODE: "Node",
79
  constants.SF_TYPE: "Type",
80
  constants.SF_NAME: "Name",
81
  constants.SF_SIZE: "Size",
82
  constants.SF_USED: "Used",
83
  constants.SF_FREE: "Free",
84
  constants.SF_ALLOCATABLE: "Allocatable",
85
  }
86

    
87

    
88
#: User-facing storage unit types
89
_USER_STORAGE_TYPE = {
90
  constants.ST_FILE: "file",
91
  constants.ST_LVM_PV: "lvm-pv",
92
  constants.ST_LVM_VG: "lvm-vg",
93
  }
94

    
95
_STORAGE_TYPE_OPT = \
96
  cli_option("-t", "--storage-type",
97
             dest="user_storage_type",
98
             choices=_USER_STORAGE_TYPE.keys(),
99
             default=None,
100
             metavar="STORAGE_TYPE",
101
             help=("Storage type (%s)" % " ,".join(_USER_STORAGE_TYPE.keys())))
102

    
103
_REPAIRABLE_STORAGE_TYPES = \
104
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
105
   if constants.SO_FIX_CONSISTENCY in so]
106

    
107
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
108

    
109

    
110
def ConvertStorageType(user_storage_type):
111
  """Converts a user storage type to its internal name.
112

    
113
  """
114
  try:
115
    return _USER_STORAGE_TYPE[user_storage_type]
116
  except KeyError:
117
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type)
118

    
119

    
120
@UsesRPC
121
def AddNode(opts, args):
122
  """Add a node to the cluster.
123

    
124
  @param opts: the command line options selected by the user
125
  @type args: list
126
  @param args: should contain only one element, the new node name
127
  @rtype: int
128
  @return: the desired exit code
129

    
130
  """
131
  cl = GetClient()
132
  dns_data = utils.HostInfo(args[0])
133
  node = dns_data.name
134
  readd = opts.readd
135

    
136
  try:
137
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
138
                           use_locking=False)
139
    node_exists, sip = output[0]
140
  except (errors.OpPrereqError, errors.OpExecError):
141
    node_exists = ""
142
    sip = None
143

    
144
  if readd:
145
    if not node_exists:
146
      ToStderr("Node %s not in the cluster"
147
               " - please retry without '--readd'", node)
148
      return 1
149
  else:
150
    if node_exists:
151
      ToStderr("Node %s already in the cluster (as %s)"
152
               " - please retry with '--readd'", node, node_exists)
153
      return 1
154
    sip = opts.secondary_ip
155

    
156
  # read the cluster name from the master
157
  output = cl.QueryConfigValues(['cluster_name'])
158
  cluster_name = output[0]
159

    
160
  if not readd:
161
    ToStderr("-- WARNING -- \n"
162
             "Performing this operation is going to replace the ssh daemon"
163
             " keypair\n"
164
             "on the target machine (%s) with the ones of the"
165
             " current one\n"
166
             "and grant full intra-cluster ssh root access to/from it\n", node)
167

    
168
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
169

    
170
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
171
                         readd=opts.readd)
172
  SubmitOpCode(op)
173

    
174

    
175
def ListNodes(opts, args):
176
  """List nodes and their properties.
177

    
178
  @param opts: the command line options selected by the user
179
  @type args: list
180
  @param args: should be an empty list
181
  @rtype: int
182
  @return: the desired exit code
183

    
184
  """
185
  if opts.output is None:
186
    selected_fields = _LIST_DEF_FIELDS
187
  elif opts.output.startswith("+"):
188
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
189
  else:
190
    selected_fields = opts.output.split(",")
191

    
192
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
193

    
194
  if not opts.no_headers:
195
    headers = _LIST_HEADERS
196
  else:
197
    headers = None
198

    
199
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
200

    
201
  numfields = ["dtotal", "dfree",
202
               "mtotal", "mnode", "mfree",
203
               "pinst_cnt", "sinst_cnt",
204
               "ctotal", "serial_no"]
205

    
206
  list_type_fields = ("pinst_list", "sinst_list", "tags")
207
  # change raw values to nicer strings
208
  for row in output:
209
    for idx, field in enumerate(selected_fields):
210
      val = row[idx]
211
      if field in list_type_fields:
212
        val = ",".join(val)
213
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
214
        if val:
215
          val = 'Y'
216
        else:
217
          val = 'N'
218
      elif field == "ctime" or field == "mtime":
219
        val = utils.FormatTime(val)
220
      elif val is None:
221
        val = "?"
222
      row[idx] = str(val)
223

    
224
  data = GenerateTable(separator=opts.separator, headers=headers,
225
                       fields=selected_fields, unitfields=unitfields,
226
                       numfields=numfields, data=output, units=opts.units)
227
  for line in data:
228
    ToStdout(line)
229

    
230
  return 0
231

    
232

    
233
def EvacuateNode(opts, args):
234
  """Relocate all secondary instance from a node.
235

    
236
  @param opts: the command line options selected by the user
237
  @type args: list
238
  @param args: should be an empty list
239
  @rtype: int
240
  @return: the desired exit code
241

    
242
  """
243
  cl = GetClient()
244
  force = opts.force
245

    
246
  dst_node = opts.dst_node
247
  iallocator = opts.iallocator
248

    
249
  cnt = [dst_node, iallocator].count(None)
250
  if cnt != 1:
251
    raise errors.OpPrereqError("One and only one of the -n and -I"
252
                               " options must be passed")
253

    
254
  selected_fields = ["name", "sinst_list"]
255
  src_node = args[0]
256

    
257
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
258
                         use_locking=False)
259
  src_node, sinst = result[0]
260

    
261
  if not sinst:
262
    ToStderr("No secondary instances on node %s, exiting.", src_node)
263
    return constants.EXIT_SUCCESS
264

    
265
  if dst_node is not None:
266
    result = cl.QueryNodes(names=[dst_node], fields=["name"],
267
                           use_locking=False)
268
    dst_node = result[0][0]
269

    
270
    if src_node == dst_node:
271
      raise errors.OpPrereqError("Evacuate node needs different source and"
272
                                 " target nodes (node %s given twice)" %
273
                                 src_node)
274
    txt_msg = "to node %s" % dst_node
275
  else:
276
    txt_msg = "using iallocator %s" % iallocator
277

    
278
  sinst = utils.NiceSort(sinst)
279

    
280
  if not force and not AskUser("Relocate instance(s) %s from node\n"
281
                               " %s %s?" %
282
                               (",".join("'%s'" % name for name in sinst),
283
                               src_node, txt_msg)):
284
    return constants.EXIT_CONFIRMATION
285

    
286
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
287
                              iallocator=iallocator)
288
  SubmitOpCode(op, cl=cl)
289

    
290

    
291
def FailoverNode(opts, args):
292
  """Failover all primary instance on a node.
293

    
294
  @param opts: the command line options selected by the user
295
  @type args: list
296
  @param args: should be an empty list
297
  @rtype: int
298
  @return: the desired exit code
299

    
300
  """
301
  cl = GetClient()
302
  force = opts.force
303
  selected_fields = ["name", "pinst_list"]
304

    
305
  # these fields are static data anyway, so it doesn't matter, but
306
  # locking=True should be safer
307
  result = cl.QueryNodes(names=args, fields=selected_fields,
308
                         use_locking=False)
309
  node, pinst = result[0]
310

    
311
  if not pinst:
312
    ToStderr("No primary instances on node %s, exiting.", node)
313
    return 0
314

    
315
  pinst = utils.NiceSort(pinst)
316

    
317
  retcode = 0
318

    
319
  if not force and not AskUser("Fail over instance(s) %s?" %
320
                               (",".join("'%s'" % name for name in pinst))):
321
    return 2
322

    
323
  jex = JobExecutor(cl=cl)
324
  for iname in pinst:
325
    op = opcodes.OpFailoverInstance(instance_name=iname,
326
                                    ignore_consistency=opts.ignore_consistency)
327
    jex.QueueJob(iname, op)
328
  results = jex.GetResults()
329
  bad_cnt = len([row for row in results if not row[0]])
330
  if bad_cnt == 0:
331
    ToStdout("All %d instance(s) failed over successfully.", len(results))
332
  else:
333
    ToStdout("There were errors during the failover:\n"
334
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
335
  return retcode
336

    
337

    
338
def MigrateNode(opts, args):
339
  """Migrate all primary instance on a node.
340

    
341
  """
342
  cl = GetClient()
343
  force = opts.force
344
  selected_fields = ["name", "pinst_list"]
345

    
346
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
347
  node, pinst = result[0]
348

    
349
  if not pinst:
350
    ToStdout("No primary instances on node %s, exiting." % node)
351
    return 0
352

    
353
  pinst = utils.NiceSort(pinst)
354

    
355
  retcode = 0
356

    
357
  if not force and not AskUser("Migrate instance(s) %s?" %
358
                               (",".join("'%s'" % name for name in pinst))):
359
    return 2
360

    
361
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
362
  SubmitOpCode(op, cl=cl)
363

    
364

    
365
def ShowNodeConfig(opts, args):
366
  """Show node information.
367

    
368
  @param opts: the command line options selected by the user
369
  @type args: list
370
  @param args: should either be an empty list, in which case
371
      we show information about all nodes, or should contain
372
      a list of nodes to be queried for information
373
  @rtype: int
374
  @return: the desired exit code
375

    
376
  """
377
  cl = GetClient()
378
  result = cl.QueryNodes(fields=["name", "pip", "sip",
379
                                 "pinst_list", "sinst_list",
380
                                 "master_candidate", "drained", "offline"],
381
                         names=args, use_locking=False)
382

    
383
  for (name, primary_ip, secondary_ip, pinst, sinst,
384
       is_mc, drained, offline) in result:
385
    ToStdout("Node name: %s", name)
386
    ToStdout("  primary ip: %s", primary_ip)
387
    ToStdout("  secondary ip: %s", secondary_ip)
388
    ToStdout("  master candidate: %s", is_mc)
389
    ToStdout("  drained: %s", drained)
390
    ToStdout("  offline: %s", offline)
391
    if pinst:
392
      ToStdout("  primary for instances:")
393
      for iname in utils.NiceSort(pinst):
394
        ToStdout("    - %s", iname)
395
    else:
396
      ToStdout("  primary for no instances")
397
    if sinst:
398
      ToStdout("  secondary for instances:")
399
      for iname in utils.NiceSort(sinst):
400
        ToStdout("    - %s", iname)
401
    else:
402
      ToStdout("  secondary for no instances")
403

    
404
  return 0
405

    
406

    
407
def RemoveNode(opts, args):
408
  """Remove a node from the cluster.
409

    
410
  @param opts: the command line options selected by the user
411
  @type args: list
412
  @param args: should contain only one element, the name of
413
      the node to be removed
414
  @rtype: int
415
  @return: the desired exit code
416

    
417
  """
418
  op = opcodes.OpRemoveNode(node_name=args[0])
419
  SubmitOpCode(op)
420
  return 0
421

    
422

    
423
def PowercycleNode(opts, args):
424
  """Remove a node from the cluster.
425

    
426
  @param opts: the command line options selected by the user
427
  @type args: list
428
  @param args: should contain only one element, the name of
429
      the node to be removed
430
  @rtype: int
431
  @return: the desired exit code
432

    
433
  """
434
  node = args[0]
435
  if (not opts.confirm and
436
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
437
    return 2
438

    
439
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
440
  result = SubmitOpCode(op)
441
  ToStderr(result)
442
  return 0
443

    
444

    
445
def ListVolumes(opts, args):
446
  """List logical volumes on node(s).
447

    
448
  @param opts: the command line options selected by the user
449
  @type args: list
450
  @param args: should either be an empty list, in which case
451
      we list data for all nodes, or contain a list of nodes
452
      to display data only for those
453
  @rtype: int
454
  @return: the desired exit code
455

    
456
  """
457
  if opts.output is None:
458
    selected_fields = ["node", "phys", "vg",
459
                       "name", "size", "instance"]
460
  else:
461
    selected_fields = opts.output.split(",")
462

    
463
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
464
  output = SubmitOpCode(op)
465

    
466
  if not opts.no_headers:
467
    headers = {"node": "Node", "phys": "PhysDev",
468
               "vg": "VG", "name": "Name",
469
               "size": "Size", "instance": "Instance"}
470
  else:
471
    headers = None
472

    
473
  unitfields = ["size"]
474

    
475
  numfields = ["size"]
476

    
477
  data = GenerateTable(separator=opts.separator, headers=headers,
478
                       fields=selected_fields, unitfields=unitfields,
479
                       numfields=numfields, data=output, units=opts.units)
480

    
481
  for line in data:
482
    ToStdout(line)
483

    
484
  return 0
485

    
486

    
487
def ListStorage(opts, args):
488
  """List physical volumes on node(s).
489

    
490
  @param opts: the command line options selected by the user
491
  @type args: list
492
  @param args: should either be an empty list, in which case
493
      we list data for all nodes, or contain a list of nodes
494
      to display data only for those
495
  @rtype: int
496
  @return: the desired exit code
497

    
498
  """
499
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
500
  if opts.user_storage_type is None:
501
    opts.user_storage_type = constants.ST_LVM_PV
502

    
503
  storage_type = ConvertStorageType(opts.user_storage_type)
504

    
505
  if opts.output is None:
506
    selected_fields = _LIST_STOR_DEF_FIELDS
507
  elif opts.output.startswith("+"):
508
    selected_fields = _LIST_STOR_DEF_FIELDS + opts.output[1:].split(",")
509
  else:
510
    selected_fields = opts.output.split(",")
511

    
512
  op = opcodes.OpQueryNodeStorage(nodes=args,
513
                                  storage_type=storage_type,
514
                                  output_fields=selected_fields)
515
  output = SubmitOpCode(op)
516

    
517
  if not opts.no_headers:
518
    headers = {
519
      constants.SF_NODE: "Node",
520
      constants.SF_TYPE: "Type",
521
      constants.SF_NAME: "Name",
522
      constants.SF_SIZE: "Size",
523
      constants.SF_USED: "Used",
524
      constants.SF_FREE: "Free",
525
      constants.SF_ALLOCATABLE: "Allocatable",
526
      }
527
  else:
528
    headers = None
529

    
530
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
531
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
532

    
533
  # change raw values to nicer strings
534
  for row in output:
535
    for idx, field in enumerate(selected_fields):
536
      val = row[idx]
537
      if field == constants.SF_ALLOCATABLE:
538
        if val:
539
          val = "Y"
540
        else:
541
          val = "N"
542
      row[idx] = str(val)
543

    
544
  data = GenerateTable(separator=opts.separator, headers=headers,
545
                       fields=selected_fields, unitfields=unitfields,
546
                       numfields=numfields, data=output, units=opts.units)
547

    
548
  for line in data:
549
    ToStdout(line)
550

    
551
  return 0
552

    
553

    
554
def ModifyStorage(opts, args):
555
  """Modify storage volume on a node.
556

    
557
  @param opts: the command line options selected by the user
558
  @type args: list
559
  @param args: should contain 3 items: node name, storage type and volume name
560
  @rtype: int
561
  @return: the desired exit code
562

    
563
  """
564
  (node_name, user_storage_type, volume_name) = args
565

    
566
  storage_type = ConvertStorageType(user_storage_type)
567

    
568
  changes = {}
569

    
570
  if opts.allocatable is not None:
571
    changes[constants.SF_ALLOCATABLE] = (opts.allocatable == "yes")
572

    
573
  if changes:
574
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
575
                                     storage_type=storage_type,
576
                                     name=volume_name,
577
                                     changes=changes)
578
    SubmitOpCode(op)
579
  else:
580
    ToStderr("No changes to perform, exiting.")
581

    
582

    
583
def RepairStorage(opts, args):
584
  """Repairs a storage volume on a node.
585

    
586
  @param opts: the command line options selected by the user
587
  @type args: list
588
  @param args: should contain 3 items: node name, storage type and volume name
589
  @rtype: int
590
  @return: the desired exit code
591

    
592
  """
593
  (node_name, user_storage_type, volume_name) = args
594

    
595
  storage_type = ConvertStorageType(user_storage_type)
596

    
597
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
598
                                   storage_type=storage_type,
599
                                   name=volume_name)
600
  SubmitOpCode(op)
601

    
602

    
603
def SetNodeParams(opts, args):
604
  """Modifies a node.
605

    
606
  @param opts: the command line options selected by the user
607
  @type args: list
608
  @param args: should contain only one element, the node name
609
  @rtype: int
610
  @return: the desired exit code
611

    
612
  """
613
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
614
    ToStderr("Please give at least one of the parameters.")
615
    return 1
616

    
617
  if opts.master_candidate is not None:
618
    candidate = opts.master_candidate == 'yes'
619
  else:
620
    candidate = None
621
  if opts.offline is not None:
622
    offline = opts.offline == 'yes'
623
  else:
624
    offline = None
625

    
626
  if opts.drained is not None:
627
    drained = opts.drained == 'yes'
628
  else:
629
    drained = None
630
  op = opcodes.OpSetNodeParams(node_name=args[0],
631
                               master_candidate=candidate,
632
                               offline=offline,
633
                               drained=drained,
634
                               force=opts.force)
635

    
636
  # even if here we process the result, we allow submit only
637
  result = SubmitOrSend(op, opts)
638

    
639
  if result:
640
    ToStdout("Modified node %s", args[0])
641
    for param, data in result:
642
      ToStdout(" - %-5s -> %s", param, data)
643
  return 0
644

    
645

    
646
commands = {
647
  'add': (
648
    AddNode, [ArgHost(min=1, max=1)],
649
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT],
650
    "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
651
    "Add a node to the cluster"),
652
  'evacuate': (
653
    EvacuateNode, ARGS_ONE_NODE,
654
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT],
655
    "[-f] {-I <iallocator> | -n <dst>} <node>",
656
    "Relocate the secondary instances from a node"
657
    " to other nodes (only for instances with drbd disk template)"),
658
  'failover': (
659
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT],
660
    "[-f] <node>",
661
    "Stops the primary instances on a node and start them on their"
662
    " secondary node (only for instances with drbd disk template)"),
663
  'migrate': (
664
    MigrateNode, ARGS_ONE_NODE, [FORCE_OPT, NONLIVE_OPT],
665
    "[-f] <node>",
666
    "Migrate all the primary instance on a node away from it"
667
    " (only for instances of type drbd)"),
668
  'info': (
669
    ShowNodeConfig, ARGS_MANY_NODES, [],
670
    "[<node_name>...]", "Show information about the node(s)"),
671
  'list': (
672
    ListNodes, ARGS_MANY_NODES,
673
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
674
    "[nodes...]",
675
    "Lists the nodes in the cluster. The available fields are (see the man"
676
    " page for details): %s. The default field list is (in order): %s." %
677
    (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
678
  'modify': (
679
    SetNodeParams, ARGS_ONE_NODE,
680
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT],
681
    "<node_name>", "Alters the parameters of a node"),
682
  'powercycle': (
683
    PowercycleNode, ARGS_ONE_NODE,
684
    [FORCE_OPT, CONFIRM_OPT],
685
    "<node_name>", "Tries to forcefully powercycle a node"),
686
  'remove': (
687
    RemoveNode, ARGS_ONE_NODE, [],
688
    "<node_name>", "Removes a node from the cluster"),
689
  'volumes': (
690
    ListVolumes, [ArgNode()],
691
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
692
    "[<node_name>...]", "List logical volumes on node(s)"),
693
  'list-storage': (
694
    ListStorage, ARGS_MANY_NODES,
695
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT],
696
    "[<node_name>...]", "List physical volumes on node(s). The available"
697
    " fields are (see the man page for details): %s." %
698
    (", ".join(_LIST_STOR_HEADERS))),
699
  'modify-storage': (
700
    ModifyStorage,
701
    [ArgNode(min=1, max=1),
702
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
703
     ArgFile(min=1, max=1)],
704
    [ALLOCATABLE_OPT],
705
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
706
  'repair-storage': (
707
    RepairStorage,
708
    [ArgNode(min=1, max=1),
709
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
710
     ArgFile(min=1, max=1)],
711
    [],
712
    "<node_name> <storage_type> <name>",
713
    "Repairs a storage volume on a node"),
714
  'list-tags': (
715
    ListTags, ARGS_ONE_NODE, [],
716
    "<node_name>", "List the tags of the given node"),
717
  'add-tags': (
718
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
719
    "<node_name> tag...", "Add tags to the given node"),
720
  'remove-tags': (
721
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
722
    "<node_name> tag...", "Remove tags from the given node"),
723
  }
724

    
725

    
726
if __name__ == '__main__':
727
  sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))