Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ f4ad2ef0

History | View | Annotate | Download (21.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
"""Node related commands"""
22

    
23
# pylint: disable-msg=W0401,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0614: Unused import %s from wildcard import (since we need cli)
26
# C0103: Invalid name gnt-node
27

    
28
import sys
29

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

    
38

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

    
46

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

    
58

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

    
77

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

    
89

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

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

    
106
_REPAIRABLE_STORAGE_TYPES = \
107
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
108
   if constants.SO_FIX_CONSISTENCY in so]
109

    
110
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
111

    
112

    
113
def ConvertStorageType(user_storage_type):
114
  """Converts a user storage type to its internal name.
115

    
116
  """
117
  try:
118
    return _USER_STORAGE_TYPE[user_storage_type]
119
  except KeyError:
120
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
121
                               errors.ECODE_INVAL)
122

    
123

    
124
@UsesRPC
125
def AddNode(opts, args):
126
  """Add a node to the cluster.
127

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

    
134
  """
135
  cl = GetClient()
136
  dns_data = utils.GetHostInfo(args[0])
137
  node = dns_data.name
138
  readd = opts.readd
139

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

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

    
160
  # read the cluster name from the master
161
  output = cl.QueryConfigValues(['cluster_name'])
162
  cluster_name = output[0]
163

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

    
172
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
173

    
174
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
175
                         readd=opts.readd)
176
  SubmitOpCode(op)
177

    
178

    
179
def ListNodes(opts, args):
180
  """List nodes and their properties.
181

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

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

    
196
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
197

    
198
  if not opts.no_headers:
199
    headers = _LIST_HEADERS
200
  else:
201
    headers = None
202

    
203
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
204

    
205
  numfields = ["dtotal", "dfree",
206
               "mtotal", "mnode", "mfree",
207
               "pinst_cnt", "sinst_cnt",
208
               "ctotal", "serial_no"]
209

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

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

    
234
  return 0
235

    
236

    
237
def EvacuateNode(opts, args):
238
  """Relocate all secondary instance from a node.
239

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

    
246
  """
247
  cl = GetClient()
248
  force = opts.force
249

    
250
  dst_node = opts.dst_node
251
  iallocator = opts.iallocator
252

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

    
258
  selected_fields = ["name", "sinst_list"]
259
  src_node = args[0]
260

    
261
  result = cl.QueryNodes(names=[src_node], fields=selected_fields,
262
                         use_locking=False)
263
  src_node, sinst = result[0]
264

    
265
  if not sinst:
266
    ToStderr("No secondary instances on node %s, exiting.", src_node)
267
    return constants.EXIT_SUCCESS
268

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

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

    
282
  sinst = utils.NiceSort(sinst)
283

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

    
290
  op = opcodes.OpEvacuateNode(node_name=args[0], remote_node=dst_node,
291
                              iallocator=iallocator)
292
  SubmitOpCode(op, cl=cl)
293

    
294

    
295
def FailoverNode(opts, args):
296
  """Failover all primary instance on a node.
297

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

    
304
  """
305
  cl = GetClient()
306
  force = opts.force
307
  selected_fields = ["name", "pinst_list"]
308

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

    
315
  if not pinst:
316
    ToStderr("No primary instances on node %s, exiting.", node)
317
    return 0
318

    
319
  pinst = utils.NiceSort(pinst)
320

    
321
  retcode = 0
322

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

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

    
341

    
342
def MigrateNode(opts, args):
343
  """Migrate all primary instance on a node.
344

    
345
  """
346
  cl = GetClient()
347
  force = opts.force
348
  selected_fields = ["name", "pinst_list"]
349

    
350
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
351
  node, pinst = result[0]
352

    
353
  if not pinst:
354
    ToStdout("No primary instances on node %s, exiting." % node)
355
    return 0
356

    
357
  pinst = utils.NiceSort(pinst)
358

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

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

    
366

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

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

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

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

    
406
  return 0
407

    
408

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

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

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

    
424

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

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

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

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

    
446

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

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

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

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

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

    
475
  unitfields = ["size"]
476

    
477
  numfields = ["size"]
478

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

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

    
486
  return 0
487

    
488

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

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

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

    
505
  storage_type = ConvertStorageType(opts.user_storage_type)
506

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

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

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

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

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

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

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

    
553
  return 0
554

    
555

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

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

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

    
568
  storage_type = ConvertStorageType(user_storage_type)
569

    
570
  changes = {}
571

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

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

    
584

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

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

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

    
597
  storage_type = ConvertStorageType(user_storage_type)
598

    
599
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
600
                                   storage_type=storage_type,
601
                                   name=volume_name,
602
                                   ignore_consistency=opts.ignore_consistency)
603
  SubmitOpCode(op)
604

    
605

    
606
def SetNodeParams(opts, args):
607
  """Modifies a node.
608

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

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

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

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

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

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

    
648

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

    
728

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