Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 064c21f8

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",
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)" %
76
                   utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
77

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

    
82
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
83

    
84

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

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

    
94

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

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

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

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

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

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

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

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

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

    
149

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

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

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

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

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

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

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

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

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

    
205
  return 0
206

    
207

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

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

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

    
221
  dst_node = opts.dst_node
222
  iallocator = opts.iallocator
223

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

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

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

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

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

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

    
253
  sinst = utils.NiceSort(sinst)
254

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

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

    
265

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

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

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

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

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

    
290
  pinst = utils.NiceSort(pinst)
291

    
292
  retcode = 0
293

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

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

    
312

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

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

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

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

    
328
  pinst = utils.NiceSort(pinst)
329

    
330
  retcode = 0
331

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

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

    
339

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

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

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

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

    
379
  return 0
380

    
381

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

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

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

    
397

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

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

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

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

    
419

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

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

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

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

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

    
448
  unitfields = ["size"]
449

    
450
  numfields = ["size"]
451

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

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

    
459
  return 0
460

    
461

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

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

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

    
478
  storage_type = ConvertStorageType(opts.user_storage_type)
479

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

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

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

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

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

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

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

    
531
  return 0
532

    
533

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

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

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

    
546
  storage_type = ConvertStorageType(user_storage_type)
547

    
548
  changes = {}
549

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

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

    
560

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

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

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

    
573
  storage_type = ConvertStorageType(user_storage_type)
574

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

    
580

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

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

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

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

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

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

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

    
623

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

    
701

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