Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 6396164f

History | View | Annotate | Download (22.1 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
  latinfriendlyfields = ["pinst_cnt", "sinst_cnt",
212
                         "ctotal", "cnodes", "csockets",
213
                         "serial_no"]
214

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

    
235
  data = GenerateTable(separator=opts.separator, headers=headers,
236
                       fields=selected_fields, unitfields=unitfields,
237
                       numfields=numfields, data=output, units=opts.units)
238
  for line in data:
239
    ToStdout(line)
240

    
241
  return 0
242

    
243

    
244
def EvacuateNode(opts, args):
245
  """Relocate all secondary instance from a node.
246

    
247
  @param opts: the command line options selected by the user
248
  @type args: list
249
  @param args: should be an empty list
250
  @rtype: int
251
  @return: the desired exit code
252

    
253
  """
254
  cl = GetClient()
255
  force = opts.force
256

    
257
  dst_node = opts.dst_node
258
  iallocator = opts.iallocator
259

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

    
265
  op = opcodes.OpNodeEvacuationStrategy(nodes=args,
266
                                        iallocator=iallocator,
267
                                        remote_node=dst_node)
268

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

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

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

    
302

    
303
def FailoverNode(opts, args):
304
  """Failover all primary instance on a node.
305

    
306
  @param opts: the command line options selected by the user
307
  @type args: list
308
  @param args: should be an empty list
309
  @rtype: int
310
  @return: the desired exit code
311

    
312
  """
313
  cl = GetClient()
314
  force = opts.force
315
  selected_fields = ["name", "pinst_list"]
316

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

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

    
327
  pinst = utils.NiceSort(pinst)
328

    
329
  retcode = 0
330

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

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

    
349

    
350
def MigrateNode(opts, args):
351
  """Migrate all primary instance on a node.
352

    
353
  """
354
  cl = GetClient()
355
  force = opts.force
356
  selected_fields = ["name", "pinst_list"]
357

    
358
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
359
  node, pinst = result[0]
360

    
361
  if not pinst:
362
    ToStdout("No primary instances on node %s, exiting." % node)
363
    return 0
364

    
365
  pinst = utils.NiceSort(pinst)
366

    
367
  if not force and not AskUser("Migrate instance(s) %s?" %
368
                               (",".join("'%s'" % name for name in pinst))):
369
    return 2
370

    
371
  op = opcodes.OpMigrateNode(node_name=args[0], live=opts.live)
372
  SubmitOpCode(op, cl=cl, opts=opts)
373

    
374

    
375
def ShowNodeConfig(opts, args):
376
  """Show node information.
377

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

    
386
  """
387
  cl = GetClient()
388
  result = cl.QueryNodes(fields=["name", "pip", "sip",
389
                                 "pinst_list", "sinst_list",
390
                                 "master_candidate", "drained", "offline"],
391
                         names=args, use_locking=False)
392

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

    
414
  return 0
415

    
416

    
417
def RemoveNode(opts, args):
418
  """Remove a node from the cluster.
419

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

    
427
  """
428
  op = opcodes.OpRemoveNode(node_name=args[0])
429
  SubmitOpCode(op, opts=opts)
430
  return 0
431

    
432

    
433
def PowercycleNode(opts, args):
434
  """Remove a node from the cluster.
435

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

    
443
  """
444
  node = args[0]
445
  if (not opts.confirm and
446
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
447
    return 2
448

    
449
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
450
  result = SubmitOpCode(op, opts=opts)
451
  ToStderr(result)
452
  return 0
453

    
454

    
455
def ListVolumes(opts, args):
456
  """List logical volumes on node(s).
457

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

    
466
  """
467
  if opts.output is None:
468
    selected_fields = ["node", "phys", "vg",
469
                       "name", "size", "instance"]
470
  else:
471
    selected_fields = opts.output.split(",")
472

    
473
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
474
  output = SubmitOpCode(op, opts=opts)
475

    
476
  if not opts.no_headers:
477
    headers = {"node": "Node", "phys": "PhysDev",
478
               "vg": "VG", "name": "Name",
479
               "size": "Size", "instance": "Instance"}
480
  else:
481
    headers = None
482

    
483
  unitfields = ["size"]
484

    
485
  numfields = ["size"]
486

    
487
  data = GenerateTable(separator=opts.separator, headers=headers,
488
                       fields=selected_fields, unitfields=unitfields,
489
                       numfields=numfields, data=output, units=opts.units)
490

    
491
  for line in data:
492
    ToStdout(line)
493

    
494
  return 0
495

    
496

    
497
def ListStorage(opts, args):
498
  """List physical volumes on node(s).
499

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

    
508
  """
509
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
510
  if opts.user_storage_type is None:
511
    opts.user_storage_type = constants.ST_LVM_PV
512

    
513
  storage_type = ConvertStorageType(opts.user_storage_type)
514

    
515
  if opts.output is None:
516
    selected_fields = _LIST_STOR_DEF_FIELDS
517
  elif opts.output.startswith("+"):
518
    selected_fields = _LIST_STOR_DEF_FIELDS + opts.output[1:].split(",")
519
  else:
520
    selected_fields = opts.output.split(",")
521

    
522
  op = opcodes.OpQueryNodeStorage(nodes=args,
523
                                  storage_type=storage_type,
524
                                  output_fields=selected_fields)
525
  output = SubmitOpCode(op, opts=opts)
526

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

    
540
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
541
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
542

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

    
554
  data = GenerateTable(separator=opts.separator, headers=headers,
555
                       fields=selected_fields, unitfields=unitfields,
556
                       numfields=numfields, data=output, units=opts.units)
557

    
558
  for line in data:
559
    ToStdout(line)
560

    
561
  return 0
562

    
563

    
564
def ModifyStorage(opts, args):
565
  """Modify storage volume on a node.
566

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

    
573
  """
574
  (node_name, user_storage_type, volume_name) = args
575

    
576
  storage_type = ConvertStorageType(user_storage_type)
577

    
578
  changes = {}
579

    
580
  if opts.allocatable is not None:
581
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
582

    
583
  if changes:
584
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
585
                                     storage_type=storage_type,
586
                                     name=volume_name,
587
                                     changes=changes)
588
    SubmitOpCode(op, opts=opts)
589
  else:
590
    ToStderr("No changes to perform, exiting.")
591

    
592

    
593
def RepairStorage(opts, args):
594
  """Repairs a storage volume on a node.
595

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

    
602
  """
603
  (node_name, user_storage_type, volume_name) = args
604

    
605
  storage_type = ConvertStorageType(user_storage_type)
606

    
607
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
608
                                   storage_type=storage_type,
609
                                   name=volume_name,
610
                                   ignore_consistency=opts.ignore_consistency)
611
  SubmitOpCode(op, opts=opts)
612

    
613

    
614
def SetNodeParams(opts, args):
615
  """Modifies a node.
616

    
617
  @param opts: the command line options selected by the user
618
  @type args: list
619
  @param args: should contain only one element, the node name
620
  @rtype: int
621
  @return: the desired exit code
622

    
623
  """
624
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
625
    ToStderr("Please give at least one of the parameters.")
626
    return 1
627

    
628
  op = opcodes.OpSetNodeParams(node_name=args[0],
629
                               master_candidate=opts.master_candidate,
630
                               offline=opts.offline,
631
                               drained=opts.drained,
632
                               force=opts.force,
633
                               auto_promote=opts.auto_promote)
634

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

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

    
644

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

    
725

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