Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ debac808

History | View | Annotate | Download (21.4 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
                               errors.ECODE_INVAL)
119

    
120

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

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

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

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

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

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

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

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

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

    
175

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

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

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

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

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

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

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

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

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

    
231
  return 0
232

    
233

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

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

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

    
247
  dst_node = opts.dst_node
248
  iallocator = opts.iallocator
249

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

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

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

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

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

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

    
279
  sinst = utils.NiceSort(sinst)
280

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

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

    
291

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

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

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

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

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

    
316
  pinst = utils.NiceSort(pinst)
317

    
318
  retcode = 0
319

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

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

    
338

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

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

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

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

    
354
  pinst = utils.NiceSort(pinst)
355

    
356
  retcode = 0
357

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

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

    
365

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

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

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

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

    
405
  return 0
406

    
407

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

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

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

    
423

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

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

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

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

    
445

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

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

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

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

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

    
474
  unitfields = ["size"]
475

    
476
  numfields = ["size"]
477

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

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

    
485
  return 0
486

    
487

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

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

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

    
504
  storage_type = ConvertStorageType(opts.user_storage_type)
505

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

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

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

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

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

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

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

    
552
  return 0
553

    
554

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

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

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

    
567
  storage_type = ConvertStorageType(user_storage_type)
568

    
569
  changes = {}
570

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

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

    
583

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

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

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

    
596
  storage_type = ConvertStorageType(user_storage_type)
597

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

    
603

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

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

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

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

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

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

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

    
646

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

    
726

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