Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ cc5b94db

History | View | Annotate | Download (21.9 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,W0613,W0614,C0103
24
# W0401: Wildcard import ganeti.cli
25
# W0613: Unused argument, since all functions follow the same API
26
# W0614: Unused import %s from wildcard import (since we need cli)
27
# C0103: Invalid name gnt-node
28

    
29
import sys
30

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

    
39

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

    
47

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

    
59

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

    
78

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

    
90

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

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

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

    
111
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
112

    
113

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

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

    
124

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

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

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

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

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

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

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

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

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

    
179

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

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

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

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

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

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

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

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

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

    
237
  return 0
238

    
239

    
240
def EvacuateNode(opts, args):
241
  """Relocate all secondary instance from a node.
242

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

    
249
  """
250
  cl = GetClient()
251
  force = opts.force
252

    
253
  dst_node = opts.dst_node
254
  iallocator = opts.iallocator
255

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

    
261
  op = opcodes.OpNodeEvacuationStrategy(nodes=args,
262
                                        iallocator=iallocator,
263
                                        remote_node=dst_node)
264

    
265
  result = SubmitOpCode(op, cl=cl, opts=opts)
266
  if not result:
267
    # no instances to migrate
268
    ToStderr("No secondary instances on node(s) %s, exiting.",
269
             utils.CommaJoin(args))
270
    return constants.EXIT_SUCCESS
271

    
272
  if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
273
                               (",".join("'%s'" % name[0] for name in result),
274
                               utils.CommaJoin(args))):
275
    return constants.EXIT_CONFIRMATION
276

    
277
  jex = JobExecutor(cl=cl, opts=opts)
278
  for row in result:
279
    iname = row[0]
280
    node = row[1]
281
    ToStdout("Will relocate instance %s to node %s", iname, node)
282
    op = opcodes.OpReplaceDisks(instance_name=iname,
283
                                remote_node=node, disks=[],
284
                                mode=constants.REPLACE_DISK_CHG,
285
                                early_release=opts.early_release)
286
    jex.QueueJob(iname, op)
287
  results = jex.GetResults()
288
  bad_cnt = len([row for row in results if not row[0]])
289
  if bad_cnt == 0:
290
    ToStdout("All %d instance(s) failed over successfully.", len(results))
291
    rcode = constants.EXIT_SUCCESS
292
  else:
293
    ToStdout("There were errors during the failover:\n"
294
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
295
    rcode = constants.EXIT_FAILURE
296
  return rcode
297

    
298

    
299
def FailoverNode(opts, args):
300
  """Failover all primary instance on a node.
301

    
302
  @param opts: the command line options selected by the user
303
  @type args: list
304
  @param args: should be an empty list
305
  @rtype: int
306
  @return: the desired exit code
307

    
308
  """
309
  cl = GetClient()
310
  force = opts.force
311
  selected_fields = ["name", "pinst_list"]
312

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

    
319
  if not pinst:
320
    ToStderr("No primary instances on node %s, exiting.", node)
321
    return 0
322

    
323
  pinst = utils.NiceSort(pinst)
324

    
325
  retcode = 0
326

    
327
  if not force and not AskUser("Fail over instance(s) %s?" %
328
                               (",".join("'%s'" % name for name in pinst))):
329
    return 2
330

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

    
345

    
346
def MigrateNode(opts, args):
347
  """Migrate all primary instance on a node.
348

    
349
  """
350
  cl = GetClient()
351
  force = opts.force
352
  selected_fields = ["name", "pinst_list"]
353

    
354
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
355
  node, pinst = result[0]
356

    
357
  if not pinst:
358
    ToStdout("No primary instances on node %s, exiting." % node)
359
    return 0
360

    
361
  pinst = utils.NiceSort(pinst)
362

    
363
  if not force and not AskUser("Migrate instance(s) %s?" %
364
                               (",".join("'%s'" % name for name in pinst))):
365
    return 2
366

    
367
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
368
  SubmitOpCode(op, cl=cl, opts=opts)
369

    
370

    
371
def ShowNodeConfig(opts, args):
372
  """Show node information.
373

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

    
382
  """
383
  cl = GetClient()
384
  result = cl.QueryNodes(fields=["name", "pip", "sip",
385
                                 "pinst_list", "sinst_list",
386
                                 "master_candidate", "drained", "offline"],
387
                         names=args, use_locking=False)
388

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

    
410
  return 0
411

    
412

    
413
def RemoveNode(opts, args):
414
  """Remove a node from the cluster.
415

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

    
423
  """
424
  op = opcodes.OpRemoveNode(node_name=args[0])
425
  SubmitOpCode(op, opts=opts)
426
  return 0
427

    
428

    
429
def PowercycleNode(opts, args):
430
  """Remove a node from the cluster.
431

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

    
439
  """
440
  node = args[0]
441
  if (not opts.confirm and
442
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
443
    return 2
444

    
445
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
446
  result = SubmitOpCode(op, opts=opts)
447
  ToStderr(result)
448
  return 0
449

    
450

    
451
def ListVolumes(opts, args):
452
  """List logical volumes on node(s).
453

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

    
462
  """
463
  if opts.output is None:
464
    selected_fields = ["node", "phys", "vg",
465
                       "name", "size", "instance"]
466
  else:
467
    selected_fields = opts.output.split(",")
468

    
469
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
470
  output = SubmitOpCode(op, opts=opts)
471

    
472
  if not opts.no_headers:
473
    headers = {"node": "Node", "phys": "PhysDev",
474
               "vg": "VG", "name": "Name",
475
               "size": "Size", "instance": "Instance"}
476
  else:
477
    headers = None
478

    
479
  unitfields = ["size"]
480

    
481
  numfields = ["size"]
482

    
483
  data = GenerateTable(separator=opts.separator, headers=headers,
484
                       fields=selected_fields, unitfields=unitfields,
485
                       numfields=numfields, data=output, units=opts.units)
486

    
487
  for line in data:
488
    ToStdout(line)
489

    
490
  return 0
491

    
492

    
493
def ListStorage(opts, args):
494
  """List physical volumes on node(s).
495

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

    
504
  """
505
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
506
  if opts.user_storage_type is None:
507
    opts.user_storage_type = constants.ST_LVM_PV
508

    
509
  storage_type = ConvertStorageType(opts.user_storage_type)
510

    
511
  if opts.output is None:
512
    selected_fields = _LIST_STOR_DEF_FIELDS
513
  elif opts.output.startswith("+"):
514
    selected_fields = _LIST_STOR_DEF_FIELDS + opts.output[1:].split(",")
515
  else:
516
    selected_fields = opts.output.split(",")
517

    
518
  op = opcodes.OpQueryNodeStorage(nodes=args,
519
                                  storage_type=storage_type,
520
                                  output_fields=selected_fields)
521
  output = SubmitOpCode(op, opts=opts)
522

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

    
536
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
537
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
538

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

    
550
  data = GenerateTable(separator=opts.separator, headers=headers,
551
                       fields=selected_fields, unitfields=unitfields,
552
                       numfields=numfields, data=output, units=opts.units)
553

    
554
  for line in data:
555
    ToStdout(line)
556

    
557
  return 0
558

    
559

    
560
def ModifyStorage(opts, args):
561
  """Modify 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
  changes = {}
575

    
576
  if opts.allocatable is not None:
577
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
578

    
579
  if changes:
580
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
581
                                     storage_type=storage_type,
582
                                     name=volume_name,
583
                                     changes=changes)
584
    SubmitOpCode(op, opts=opts)
585
  else:
586
    ToStderr("No changes to perform, exiting.")
587

    
588

    
589
def RepairStorage(opts, args):
590
  """Repairs a storage volume on a node.
591

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

    
598
  """
599
  (node_name, user_storage_type, volume_name) = args
600

    
601
  storage_type = ConvertStorageType(user_storage_type)
602

    
603
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
604
                                   storage_type=storage_type,
605
                                   name=volume_name,
606
                                   ignore_consistency=opts.ignore_consistency)
607
  SubmitOpCode(op, opts=opts)
608

    
609

    
610
def SetNodeParams(opts, args):
611
  """Modifies a node.
612

    
613
  @param opts: the command line options selected by the user
614
  @type args: list
615
  @param args: should contain only one element, the node name
616
  @rtype: int
617
  @return: the desired exit code
618

    
619
  """
620
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
621
    ToStderr("Please give at least one of the parameters.")
622
    return 1
623

    
624
  op = opcodes.OpSetNodeParams(node_name=args[0],
625
                               master_candidate=opts.master_candidate,
626
                               offline=opts.offline,
627
                               drained=opts.drained,
628
                               force=opts.force,
629
                               auto_promote=opts.auto_promote)
630

    
631
  # even if here we process the result, we allow submit only
632
  result = SubmitOrSend(op, opts)
633

    
634
  if result:
635
    ToStdout("Modified node %s", args[0])
636
    for param, data in result:
637
      ToStdout(" - %-5s -> %s", param, data)
638
  return 0
639

    
640

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

    
721

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