Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 6915bc28

History | View | Annotate | Download (20.6 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
#: headers (and full field list for L{ListNodes}
45
_LIST_HEADERS = {
46
  "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
47
  "pinst_list": "PriInstances", "sinst_list": "SecInstances",
48
  "pip": "PrimaryIP", "sip": "SecondaryIP",
49
  "dtotal": "DTotal", "dfree": "DFree",
50
  "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
51
  "bootid": "BootID",
52
  "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
53
  "tags": "Tags",
54
  "serial_no": "SerialNo",
55
  "master_candidate": "MasterC",
56
  "master": "IsMaster",
57
  "offline": "Offline", "drained": "Drained",
58
  "role": "Role",
59
  "ctime": "CTime", "mtime": "MTime", "uuid": "UUID"
60
  }
61

    
62
#: User-facing storage unit types
63
_USER_STORAGE_TYPE = {
64
  constants.ST_FILE: "file",
65
  constants.ST_LVM_PV: "lvm-pv",
66
  constants.ST_LVM_VG: "lvm-vg",
67
  }
68

    
69
_STORAGE_TYPE_OPT = \
70
  cli_option("--storage-type",
71
             dest="user_storage_type",
72
             choices=_USER_STORAGE_TYPE.keys(),
73
             default=None,
74
             metavar="STORAGE_TYPE",
75
             help=("Storage type (%s)" % " ,".join(_USER_STORAGE_TYPE.keys())))
76

    
77
_REPAIRABLE_STORAGE_TYPES = \
78
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
79
   if constants.SO_FIX_CONSISTENCY in so]
80

    
81
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
82

    
83

    
84
def ConvertStorageType(user_storage_type):
85
  """Converts a user storage type to its internal name.
86

    
87
  """
88
  try:
89
    return _USER_STORAGE_TYPE[user_storage_type]
90
  except KeyError:
91
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type)
92

    
93

    
94
@UsesRPC
95
def AddNode(opts, args):
96
  """Add a node to the cluster.
97

    
98
  @param opts: the command line options selected by the user
99
  @type args: list
100
  @param args: should contain only one element, the new node name
101
  @rtype: int
102
  @return: the desired exit code
103

    
104
  """
105
  cl = GetClient()
106
  dns_data = utils.HostInfo(args[0])
107
  node = dns_data.name
108
  readd = opts.readd
109

    
110
  try:
111
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
112
                           use_locking=False)
113
    node_exists, sip = output[0]
114
  except (errors.OpPrereqError, errors.OpExecError):
115
    node_exists = ""
116
    sip = None
117

    
118
  if readd:
119
    if not node_exists:
120
      ToStderr("Node %s not in the cluster"
121
               " - please retry without '--readd'", node)
122
      return 1
123
  else:
124
    if node_exists:
125
      ToStderr("Node %s already in the cluster (as %s)"
126
               " - please retry with '--readd'", node, node_exists)
127
      return 1
128
    sip = opts.secondary_ip
129

    
130
  # read the cluster name from the master
131
  output = cl.QueryConfigValues(['cluster_name'])
132
  cluster_name = output[0]
133

    
134
  if not readd:
135
    ToStderr("-- WARNING -- \n"
136
             "Performing this operation is going to replace the ssh daemon"
137
             " keypair\n"
138
             "on the target machine (%s) with the ones of the"
139
             " current one\n"
140
             "and grant full intra-cluster ssh root access to/from it\n", node)
141

    
142
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
143

    
144
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
145
                         readd=opts.readd)
146
  SubmitOpCode(op)
147

    
148

    
149
def ListNodes(opts, args):
150
  """List nodes and their properties.
151

    
152
  @param opts: the command line options selected by the user
153
  @type args: list
154
  @param args: should be an empty list
155
  @rtype: int
156
  @return: the desired exit code
157

    
158
  """
159
  if opts.output is None:
160
    selected_fields = _LIST_DEF_FIELDS
161
  elif opts.output.startswith("+"):
162
    selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",")
163
  else:
164
    selected_fields = opts.output.split(",")
165

    
166
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
167

    
168
  if not opts.no_headers:
169
    headers = _LIST_HEADERS
170
  else:
171
    headers = None
172

    
173
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
174

    
175
  numfields = ["dtotal", "dfree",
176
               "mtotal", "mnode", "mfree",
177
               "pinst_cnt", "sinst_cnt",
178
               "ctotal", "serial_no"]
179

    
180
  list_type_fields = ("pinst_list", "sinst_list", "tags")
181
  # change raw values to nicer strings
182
  for row in output:
183
    for idx, field in enumerate(selected_fields):
184
      val = row[idx]
185
      if field in list_type_fields:
186
        val = ",".join(val)
187
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
188
        if val:
189
          val = 'Y'
190
        else:
191
          val = 'N'
192
      elif field == "ctime" or field == "mtime":
193
        val = utils.FormatTime(val)
194
      elif val is None:
195
        val = "?"
196
      row[idx] = str(val)
197

    
198
  data = GenerateTable(separator=opts.separator, headers=headers,
199
                       fields=selected_fields, unitfields=unitfields,
200
                       numfields=numfields, data=output, units=opts.units)
201
  for line in data:
202
    ToStdout(line)
203

    
204
  return 0
205

    
206

    
207
def EvacuateNode(opts, args):
208
  """Relocate all secondary instance from a node.
209

    
210
  @param opts: the command line options selected by the user
211
  @type args: list
212
  @param args: should be an empty list
213
  @rtype: int
214
  @return: the desired exit code
215

    
216
  """
217
  cl = GetClient()
218
  force = opts.force
219

    
220
  dst_node = opts.dst_node
221
  iallocator = opts.iallocator
222

    
223
  cnt = [dst_node, iallocator].count(None)
224
  if cnt != 1:
225
    raise errors.OpPrereqError("One and only one of the -n and -I"
226
                               " options must be passed")
227

    
228
  selected_fields = ["name", "sinst_list"]
229
  src_node = args[0]
230

    
231
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
232
                         use_locking=False)
233
  src_node, sinst = result[0]
234

    
235
  if not sinst:
236
    ToStderr("No secondary instances on node %s, exiting.", src_node)
237
    return constants.EXIT_SUCCESS
238

    
239
  if dst_node is not None:
240
    result = cl.QueryNodes(names=[dst_node], fields=["name"],
241
                           use_locking=False)
242
    dst_node = result[0][0]
243

    
244
    if src_node == dst_node:
245
      raise errors.OpPrereqError("Evacuate node needs different source and"
246
                                 " target nodes (node %s given twice)" %
247
                                 src_node)
248
    txt_msg = "to node %s" % dst_node
249
  else:
250
    txt_msg = "using iallocator %s" % iallocator
251

    
252
  sinst = utils.NiceSort(sinst)
253

    
254
  if not force and not AskUser("Relocate instance(s) %s from node\n"
255
                               " %s %s?" %
256
                               (",".join("'%s'" % name for name in sinst),
257
                               src_node, txt_msg)):
258
    return constants.EXIT_CONFIRMATION
259

    
260
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
261
                              iallocator=iallocator)
262
  SubmitOpCode(op, cl=cl)
263

    
264

    
265
def FailoverNode(opts, args):
266
  """Failover all primary instance on a node.
267

    
268
  @param opts: the command line options selected by the user
269
  @type args: list
270
  @param args: should be an empty list
271
  @rtype: int
272
  @return: the desired exit code
273

    
274
  """
275
  cl = GetClient()
276
  force = opts.force
277
  selected_fields = ["name", "pinst_list"]
278

    
279
  # these fields are static data anyway, so it doesn't matter, but
280
  # locking=True should be safer
281
  result = cl.QueryNodes(names=args, fields=selected_fields,
282
                         use_locking=False)
283
  node, pinst = result[0]
284

    
285
  if not pinst:
286
    ToStderr("No primary instances on node %s, exiting.", node)
287
    return 0
288

    
289
  pinst = utils.NiceSort(pinst)
290

    
291
  retcode = 0
292

    
293
  if not force and not AskUser("Fail over instance(s) %s?" %
294
                               (",".join("'%s'" % name for name in pinst))):
295
    return 2
296

    
297
  jex = JobExecutor(cl=cl)
298
  for iname in pinst:
299
    op = opcodes.OpFailoverInstance(instance_name=iname,
300
                                    ignore_consistency=opts.ignore_consistency)
301
    jex.QueueJob(iname, op)
302
  results = jex.GetResults()
303
  bad_cnt = len([row for row in results if not row[0]])
304
  if bad_cnt == 0:
305
    ToStdout("All %d instance(s) failed over successfully.", len(results))
306
  else:
307
    ToStdout("There were errors during the failover:\n"
308
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
309
  return retcode
310

    
311

    
312
def MigrateNode(opts, args):
313
  """Migrate all primary instance on a node.
314

    
315
  """
316
  cl = GetClient()
317
  force = opts.force
318
  selected_fields = ["name", "pinst_list"]
319

    
320
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
321
  node, pinst = result[0]
322

    
323
  if not pinst:
324
    ToStdout("No primary instances on node %s, exiting." % node)
325
    return 0
326

    
327
  pinst = utils.NiceSort(pinst)
328

    
329
  retcode = 0
330

    
331
  if not force and not AskUser("Migrate instance(s) %s?" %
332
                               (",".join("'%s'" % name for name in pinst))):
333
    return 2
334

    
335
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
336
  SubmitOpCode(op, cl=cl)
337

    
338

    
339
def ShowNodeConfig(opts, args):
340
  """Show node information.
341

    
342
  @param opts: the command line options selected by the user
343
  @type args: list
344
  @param args: should either be an empty list, in which case
345
      we show information about all nodes, or should contain
346
      a list of nodes to be queried for information
347
  @rtype: int
348
  @return: the desired exit code
349

    
350
  """
351
  cl = GetClient()
352
  result = cl.QueryNodes(fields=["name", "pip", "sip",
353
                                 "pinst_list", "sinst_list",
354
                                 "master_candidate", "drained", "offline"],
355
                         names=args, use_locking=False)
356

    
357
  for (name, primary_ip, secondary_ip, pinst, sinst,
358
       is_mc, drained, offline) in result:
359
    ToStdout("Node name: %s", name)
360
    ToStdout("  primary ip: %s", primary_ip)
361
    ToStdout("  secondary ip: %s", secondary_ip)
362
    ToStdout("  master candidate: %s", is_mc)
363
    ToStdout("  drained: %s", drained)
364
    ToStdout("  offline: %s", offline)
365
    if pinst:
366
      ToStdout("  primary for instances:")
367
      for iname in utils.NiceSort(pinst):
368
        ToStdout("    - %s", iname)
369
    else:
370
      ToStdout("  primary for no instances")
371
    if sinst:
372
      ToStdout("  secondary for instances:")
373
      for iname in utils.NiceSort(sinst):
374
        ToStdout("    - %s", iname)
375
    else:
376
      ToStdout("  secondary for no instances")
377

    
378
  return 0
379

    
380

    
381
def RemoveNode(opts, args):
382
  """Remove a node from the cluster.
383

    
384
  @param opts: the command line options selected by the user
385
  @type args: list
386
  @param args: should contain only one element, the name of
387
      the node to be removed
388
  @rtype: int
389
  @return: the desired exit code
390

    
391
  """
392
  op = opcodes.OpRemoveNode(node_name=args[0])
393
  SubmitOpCode(op)
394
  return 0
395

    
396

    
397
def PowercycleNode(opts, args):
398
  """Remove a node from the cluster.
399

    
400
  @param opts: the command line options selected by the user
401
  @type args: list
402
  @param args: should contain only one element, the name of
403
      the node to be removed
404
  @rtype: int
405
  @return: the desired exit code
406

    
407
  """
408
  node = args[0]
409
  if (not opts.confirm and
410
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
411
    return 2
412

    
413
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
414
  result = SubmitOpCode(op)
415
  ToStderr(result)
416
  return 0
417

    
418

    
419
def ListVolumes(opts, args):
420
  """List logical volumes on node(s).
421

    
422
  @param opts: the command line options selected by the user
423
  @type args: list
424
  @param args: should either be an empty list, in which case
425
      we list data for all nodes, or contain a list of nodes
426
      to display data only for those
427
  @rtype: int
428
  @return: the desired exit code
429

    
430
  """
431
  if opts.output is None:
432
    selected_fields = ["node", "phys", "vg",
433
                       "name", "size", "instance"]
434
  else:
435
    selected_fields = opts.output.split(",")
436

    
437
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
438
  output = SubmitOpCode(op)
439

    
440
  if not opts.no_headers:
441
    headers = {"node": "Node", "phys": "PhysDev",
442
               "vg": "VG", "name": "Name",
443
               "size": "Size", "instance": "Instance"}
444
  else:
445
    headers = None
446

    
447
  unitfields = ["size"]
448

    
449
  numfields = ["size"]
450

    
451
  data = GenerateTable(separator=opts.separator, headers=headers,
452
                       fields=selected_fields, unitfields=unitfields,
453
                       numfields=numfields, data=output, units=opts.units)
454

    
455
  for line in data:
456
    ToStdout(line)
457

    
458
  return 0
459

    
460

    
461
def ListPhysicalVolumes(opts, args):
462
  """List physical volumes on node(s).
463

    
464
  @param opts: the command line options selected by the user
465
  @type args: list
466
  @param args: should either be an empty list, in which case
467
      we list data for all nodes, or contain a list of nodes
468
      to display data only for those
469
  @rtype: int
470
  @return: the desired exit code
471

    
472
  """
473
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
474
  if opts.user_storage_type is None:
475
    opts.user_storage_type = constants.ST_LVM_PV
476

    
477
  storage_type = ConvertStorageType(opts.user_storage_type)
478

    
479
  default_fields = {
480
    constants.ST_FILE: [
481
      constants.SF_NAME,
482
      constants.SF_USED,
483
      constants.SF_FREE,
484
      ],
485
    constants.ST_LVM_PV: [
486
      constants.SF_NAME,
487
      constants.SF_SIZE,
488
      constants.SF_USED,
489
      constants.SF_FREE,
490
      ],
491
    constants.ST_LVM_VG: [
492
      constants.SF_NAME,
493
      constants.SF_SIZE,
494
      ],
495
  }
496

    
497
  if opts.output is None:
498
    selected_fields = ["node"]
499
    selected_fields.extend(default_fields[storage_type])
500
  else:
501
    selected_fields = opts.output.split(",")
502

    
503
  op = opcodes.OpQueryNodeStorage(nodes=args,
504
                                  storage_type=storage_type,
505
                                  output_fields=selected_fields)
506
  output = SubmitOpCode(op)
507

    
508
  if not opts.no_headers:
509
    headers = {
510
      "node": "Node",
511
      constants.SF_NAME: "Name",
512
      constants.SF_SIZE: "Size",
513
      constants.SF_USED: "Used",
514
      constants.SF_FREE: "Free",
515
      constants.SF_ALLOCATABLE: "Allocatable",
516
      }
517
  else:
518
    headers = None
519

    
520
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
521
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
522

    
523
  data = GenerateTable(separator=opts.separator, headers=headers,
524
                       fields=selected_fields, unitfields=unitfields,
525
                       numfields=numfields, data=output, units=opts.units)
526

    
527
  for line in data:
528
    ToStdout(line)
529

    
530
  return 0
531

    
532

    
533
def ModifyVolume(opts, args):
534
  """Modify storage volume on a node.
535

    
536
  @param opts: the command line options selected by the user
537
  @type args: list
538
  @param args: should contain 3 items: node name, storage type and volume name
539
  @rtype: int
540
  @return: the desired exit code
541

    
542
  """
543
  (node_name, user_storage_type, volume_name) = args
544

    
545
  storage_type = ConvertStorageType(user_storage_type)
546

    
547
  changes = {}
548

    
549
  if opts.allocatable is not None:
550
    changes[constants.SF_ALLOCATABLE] = (opts.allocatable == "yes")
551

    
552
  if changes:
553
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
554
                                     storage_type=storage_type,
555
                                     name=volume_name,
556
                                     changes=changes)
557
    SubmitOpCode(op)
558

    
559

    
560
def RepairVolume(opts, args):
561
  """Repairs a storage volume on a node.
562

    
563
  @param opts: the command line options selected by the user
564
  @type args: list
565
  @param args: should contain 3 items: node name, storage type and volume name
566
  @rtype: int
567
  @return: the desired exit code
568

    
569
  """
570
  (node_name, user_storage_type, volume_name) = args
571

    
572
  storage_type = ConvertStorageType(user_storage_type)
573

    
574
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
575
                                   storage_type=storage_type,
576
                                   name=volume_name)
577
  SubmitOpCode(op)
578

    
579

    
580
def SetNodeParams(opts, args):
581
  """Modifies a node.
582

    
583
  @param opts: the command line options selected by the user
584
  @type args: list
585
  @param args: should contain only one element, the node name
586
  @rtype: int
587
  @return: the desired exit code
588

    
589
  """
590
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
591
    ToStderr("Please give at least one of the parameters.")
592
    return 1
593

    
594
  if opts.master_candidate is not None:
595
    candidate = opts.master_candidate == 'yes'
596
  else:
597
    candidate = None
598
  if opts.offline is not None:
599
    offline = opts.offline == 'yes'
600
  else:
601
    offline = None
602

    
603
  if opts.drained is not None:
604
    drained = opts.drained == 'yes'
605
  else:
606
    drained = None
607
  op = opcodes.OpSetNodeParams(node_name=args[0],
608
                               master_candidate=candidate,
609
                               offline=offline,
610
                               drained=drained,
611
                               force=opts.force)
612

    
613
  # even if here we process the result, we allow submit only
614
  result = SubmitOrSend(op, opts)
615

    
616
  if result:
617
    ToStdout("Modified node %s", args[0])
618
    for param, data in result:
619
      ToStdout(" - %-5s -> %s", param, data)
620
  return 0
621

    
622

    
623
commands = {
624
  'add': (
625
    AddNode, [ArgHost(min=1, max=1)],
626
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT],
627
    "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
628
    "Add a node to the cluster"),
629
  'evacuate': (
630
    EvacuateNode, ARGS_ONE_NODE,
631
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT],
632
    "[-f] {-I <iallocator> | -n <dst>} <node>",
633
    "Relocate the secondary instances from a node"
634
    " to other nodes (only for instances with drbd disk template)"),
635
  'failover': (
636
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT],
637
    "[-f] <node>",
638
    "Stops the primary instances on a node and start them on their"
639
    " secondary node (only for instances with drbd disk template)"),
640
  'migrate': (
641
    MigrateNode, ARGS_ONE_NODE, [FORCE_OPT, NONLIVE_OPT],
642
    "[-f] <node>",
643
    "Migrate all the primary instance on a node away from it"
644
    " (only for instances of type drbd)"),
645
  'info': (
646
    ShowNodeConfig, ARGS_MANY_NODES, [],
647
    "[<node_name>...]", "Show information about the node(s)"),
648
  'list': (
649
    ListNodes, ARGS_MANY_NODES,
650
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT],
651
    "[nodes...]",
652
    "Lists the nodes in the cluster. The available fields are (see the man"
653
    " page for details): %s. The default field list is (in order): %s." %
654
    (", ".join(_LIST_HEADERS), ", ".join(_LIST_DEF_FIELDS))),
655
  'modify': (
656
    SetNodeParams, ARGS_ONE_NODE,
657
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT],
658
    "<node_name>", "Alters the parameters of a node"),
659
  'powercycle': (
660
    PowercycleNode, ARGS_ONE_NODE,
661
    [FORCE_OPT, CONFIRM_OPT],
662
    "<node_name>", "Tries to forcefully powercycle a node"),
663
  'remove': (
664
    RemoveNode, ARGS_ONE_NODE, [],
665
    "<node_name>", "Removes a node from the cluster"),
666
  'volumes': (
667
    ListVolumes, [ArgNode()],
668
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
669
    "[<node_name>...]", "List logical volumes on node(s)"),
670
  'physical-volumes': (
671
    ListPhysicalVolumes, ARGS_MANY_NODES,
672
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT],
673
    "[<node_name>...]", "List physical volumes on node(s)"),
674
  'modify-volume': (
675
    ModifyVolume,
676
    [ArgNode(min=1, max=1),
677
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
678
     ArgFile(min=1, max=1)],
679
    [],
680
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
681
  'repair-volume': (
682
    RepairVolume,
683
    [ArgNode(min=1, max=1),
684
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
685
     ArgFile(min=1, max=1)],
686
    [],
687
    "<node_name> <storage_type> <name>",
688
    "Repairs a storage volume on a node"),
689
  'list-tags': (
690
    ListTags, ARGS_ONE_NODE, [],
691
    "<node_name>", "List the tags of the given node"),
692
  'add-tags': (
693
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
694
    "<node_name> tag...", "Add tags to the given node"),
695
  'remove-tags': (
696
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
697
    "<node_name> tag...", "Remove tags from the given node"),
698
  }
699

    
700

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