Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ a4ebd726

History | View | Annotate | Download (22 kB)

1
#!/usr/bin/python
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010 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
from ganeti import netutils
39

    
40

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

    
48

    
49
#: Default field list for L{ListVolumes}
50
_LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
51

    
52

    
53
#: default list of field for L{ListStorage}
54
_LIST_STOR_DEF_FIELDS = [
55
  constants.SF_NODE,
56
  constants.SF_TYPE,
57
  constants.SF_NAME,
58
  constants.SF_SIZE,
59
  constants.SF_USED,
60
  constants.SF_FREE,
61
  constants.SF_ALLOCATABLE,
62
  ]
63

    
64

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

    
83

    
84
#: headers (and full field list for L{ListStorage}
85
_LIST_STOR_HEADERS = {
86
  constants.SF_NODE: "Node",
87
  constants.SF_TYPE: "Type",
88
  constants.SF_NAME: "Name",
89
  constants.SF_SIZE: "Size",
90
  constants.SF_USED: "Used",
91
  constants.SF_FREE: "Free",
92
  constants.SF_ALLOCATABLE: "Allocatable",
93
  }
94

    
95

    
96
#: User-facing storage unit types
97
_USER_STORAGE_TYPE = {
98
  constants.ST_FILE: "file",
99
  constants.ST_LVM_PV: "lvm-pv",
100
  constants.ST_LVM_VG: "lvm-vg",
101
  }
102

    
103
_STORAGE_TYPE_OPT = \
104
  cli_option("-t", "--storage-type",
105
             dest="user_storage_type",
106
             choices=_USER_STORAGE_TYPE.keys(),
107
             default=None,
108
             metavar="STORAGE_TYPE",
109
             help=("Storage type (%s)" %
110
                   utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
111

    
112
_REPAIRABLE_STORAGE_TYPES = \
113
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
114
   if constants.SO_FIX_CONSISTENCY in so]
115

    
116
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
117

    
118

    
119
def ConvertStorageType(user_storage_type):
120
  """Converts a user storage type to its internal name.
121

    
122
  """
123
  try:
124
    return _USER_STORAGE_TYPE[user_storage_type]
125
  except KeyError:
126
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
127
                               errors.ECODE_INVAL)
128

    
129

    
130
@UsesRPC
131
def AddNode(opts, args):
132
  """Add a node to the cluster.
133

    
134
  @param opts: the command line options selected by the user
135
  @type args: list
136
  @param args: should contain only one element, the new node name
137
  @rtype: int
138
  @return: the desired exit code
139

    
140
  """
141
  cl = GetClient()
142
  dns_data = netutils.GetHostInfo(netutils.HostInfo.NormalizeName(args[0]))
143
  node = dns_data.name
144
  readd = opts.readd
145

    
146
  try:
147
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
148
                           use_locking=False)
149
    node_exists, sip = output[0]
150
  except (errors.OpPrereqError, errors.OpExecError):
151
    node_exists = ""
152
    sip = None
153

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

    
166
  # read the cluster name from the master
167
  output = cl.QueryConfigValues(['cluster_name'])
168
  cluster_name = output[0]
169

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

    
178
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
179

    
180
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
181
                         readd=opts.readd)
182
  SubmitOpCode(op, opts=opts)
183

    
184

    
185
def ListNodes(opts, args):
186
  """List nodes and their properties.
187

    
188
  @param opts: the command line options selected by the user
189
  @type args: list
190
  @param args: should be an empty list
191
  @rtype: int
192
  @return: the desired exit code
193

    
194
  """
195
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
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
  op = opcodes.OpNodeEvacuationStrategy(nodes=args,
257
                                        iallocator=iallocator,
258
                                        remote_node=dst_node)
259

    
260
  result = SubmitOpCode(op, cl=cl, opts=opts)
261
  if not result:
262
    # no instances to migrate
263
    ToStderr("No secondary instances on node(s) %s, exiting.",
264
             utils.CommaJoin(args))
265
    return constants.EXIT_SUCCESS
266

    
267
  if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
268
                               (",".join("'%s'" % name[0] for name in result),
269
                               utils.CommaJoin(args))):
270
    return constants.EXIT_CONFIRMATION
271

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

    
293

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

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

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

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

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

    
318
  pinst = utils.NiceSort(pinst)
319

    
320
  retcode = 0
321

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

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

    
340

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

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

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

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

    
356
  pinst = utils.NiceSort(pinst)
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
  # this should be removed once --non-live is deprecated
363
  if not opts.live and opts.migration_mode is not None:
364
    raise errors.OpPrereqError("Only one of the --non-live and "
365
                               "--migration-mode options can be passed",
366
                               errors.ECODE_INVAL)
367
  if not opts.live: # --non-live passed
368
    mode = constants.HT_MIGRATION_NONLIVE
369
  else:
370
    mode = opts.migration_mode
371
  op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
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
  if result:
452
    ToStderr(result)
453
  return 0
454

    
455

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

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

    
467
  """
468
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
469

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

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

    
480
  unitfields = ["size"]
481

    
482
  numfields = ["size"]
483

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

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

    
491
  return 0
492

    
493

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

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

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

    
510
  storage_type = ConvertStorageType(opts.user_storage_type)
511

    
512
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
513

    
514
  op = opcodes.OpQueryNodeStorage(nodes=args,
515
                                  storage_type=storage_type,
516
                                  output_fields=selected_fields)
517
  output = SubmitOpCode(op, opts=opts)
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
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, opts=opts)
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, opts=opts)
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
  op = opcodes.OpSetNodeParams(node_name=args[0],
621
                               master_candidate=opts.master_candidate,
622
                               offline=opts.offline,
623
                               drained=opts.drained,
624
                               force=opts.force,
625
                               auto_promote=opts.auto_promote)
626

    
627
  # even if here we process the result, we allow submit only
628
  result = SubmitOrSend(op, opts)
629

    
630
  if result:
631
    ToStdout("Modified node %s", args[0])
632
    for param, data in result:
633
      ToStdout(" - %-5s -> %s", param, data)
634
  return 0
635

    
636

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

    
717

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