Statistics
| Branch: | Tag: | Revision:

root / scripts / gnt-node @ 3ef51126

History | View | Annotate | Download (23.1 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 bootstrap
33
from ganeti import opcodes
34
from ganeti import utils
35
from ganeti import constants
36
from ganeti import compat
37
from ganeti import errors
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
NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
120
                              action="store_false", dest="node_setup",
121
                              help=("Do not make initial SSH setup on remote"
122
                                    " node (needs to be done manually)"))
123

    
124

    
125
def ConvertStorageType(user_storage_type):
126
  """Converts a user storage type to its internal name.
127

    
128
  """
129
  try:
130
    return _USER_STORAGE_TYPE[user_storage_type]
131
  except KeyError:
132
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
133
                               errors.ECODE_INVAL)
134

    
135

    
136
def _RunSetupSSH(options, nodes):
137
  """Wrapper around utils.RunCmd to call setup-ssh
138

    
139
  @param options: The command line options
140
  @param nodes: The nodes to setup
141

    
142
  """
143
  cmd = [constants.SETUP_SSH]
144

    
145
  # Pass --debug|--verbose to the external script if set on our invocation
146
  # --debug overrides --verbose
147
  if options.debug:
148
    cmd.append("--debug")
149
  elif options.verbose:
150
    cmd.append("--verbose")
151
  if options.ssh_key_check:
152
    cmd.append("--no-ssh-key-check")
153

    
154
  cmd.extend(nodes)
155

    
156
  result = utils.RunCmd(cmd, interactive=True)
157

    
158
  if result.failed:
159
    errmsg = ("Command '%s' failed with exit code %s; output %r" %
160
              (result.cmd, result.exit_code, result.output))
161
    raise errors.OpExecError(errmsg)
162

    
163

    
164
@UsesRPC
165
def AddNode(opts, args):
166
  """Add a node to the cluster.
167

    
168
  @param opts: the command line options selected by the user
169
  @type args: list
170
  @param args: should contain only one element, the new node name
171
  @rtype: int
172
  @return: the desired exit code
173

    
174
  """
175
  cl = GetClient()
176
  node = netutils.GetHostname(name=args[0]).name
177
  readd = opts.readd
178

    
179
  try:
180
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
181
                           use_locking=False)
182
    node_exists, sip = output[0]
183
  except (errors.OpPrereqError, errors.OpExecError):
184
    node_exists = ""
185
    sip = None
186

    
187
  if readd:
188
    if not node_exists:
189
      ToStderr("Node %s not in the cluster"
190
               " - please retry without '--readd'", node)
191
      return 1
192
  else:
193
    if node_exists:
194
      ToStderr("Node %s already in the cluster (as %s)"
195
               " - please retry with '--readd'", node, node_exists)
196
      return 1
197
    sip = opts.secondary_ip
198

    
199
  # read the cluster name from the master
200
  output = cl.QueryConfigValues(['cluster_name'])
201
  cluster_name = output[0]
202

    
203
  if not readd and opts.node_setup:
204
    ToStderr("-- WARNING -- \n"
205
             "Performing this operation is going to replace the ssh daemon"
206
             " keypair\n"
207
             "on the target machine (%s) with the ones of the"
208
             " current one\n"
209
             "and grant full intra-cluster ssh root access to/from it\n", node)
210

    
211
  if opts.node_setup:
212
    _RunSetupSSH(opts, [node])
213

    
214
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
215

    
216
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
217
                         readd=opts.readd)
218
  SubmitOpCode(op, opts=opts)
219

    
220

    
221
def ListNodes(opts, args):
222
  """List nodes and their properties.
223

    
224
  @param opts: the command line options selected by the user
225
  @type args: list
226
  @param args: should be an empty list
227
  @rtype: int
228
  @return: the desired exit code
229

    
230
  """
231
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
232

    
233
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
234

    
235
  if not opts.no_headers:
236
    headers = _LIST_HEADERS
237
  else:
238
    headers = None
239

    
240
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
241

    
242
  numfields = ["dtotal", "dfree",
243
               "mtotal", "mnode", "mfree",
244
               "pinst_cnt", "sinst_cnt",
245
               "ctotal", "serial_no"]
246

    
247
  list_type_fields = ("pinst_list", "sinst_list", "tags")
248
  # change raw values to nicer strings
249
  for row in output:
250
    for idx, field in enumerate(selected_fields):
251
      val = row[idx]
252
      if field in list_type_fields:
253
        val = ",".join(val)
254
      elif field in ('master', 'master_candidate', 'offline', 'drained'):
255
        if val:
256
          val = 'Y'
257
        else:
258
          val = 'N'
259
      elif field == "ctime" or field == "mtime":
260
        val = utils.FormatTime(val)
261
      elif val is None:
262
        val = "?"
263
      elif opts.roman_integers and isinstance(val, int):
264
        val = compat.TryToRoman(val)
265
      row[idx] = str(val)
266

    
267
  data = GenerateTable(separator=opts.separator, headers=headers,
268
                       fields=selected_fields, unitfields=unitfields,
269
                       numfields=numfields, data=output, units=opts.units)
270
  for line in data:
271
    ToStdout(line)
272

    
273
  return 0
274

    
275

    
276
def EvacuateNode(opts, args):
277
  """Relocate all secondary instance from a node.
278

    
279
  @param opts: the command line options selected by the user
280
  @type args: list
281
  @param args: should be an empty list
282
  @rtype: int
283
  @return: the desired exit code
284

    
285
  """
286
  cl = GetClient()
287
  force = opts.force
288

    
289
  dst_node = opts.dst_node
290
  iallocator = opts.iallocator
291

    
292
  op = opcodes.OpNodeEvacuationStrategy(nodes=args,
293
                                        iallocator=iallocator,
294
                                        remote_node=dst_node)
295

    
296
  result = SubmitOpCode(op, cl=cl, opts=opts)
297
  if not result:
298
    # no instances to migrate
299
    ToStderr("No secondary instances on node(s) %s, exiting.",
300
             utils.CommaJoin(args))
301
    return constants.EXIT_SUCCESS
302

    
303
  if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
304
                               (",".join("'%s'" % name[0] for name in result),
305
                               utils.CommaJoin(args))):
306
    return constants.EXIT_CONFIRMATION
307

    
308
  jex = JobExecutor(cl=cl, opts=opts)
309
  for row in result:
310
    iname = row[0]
311
    node = row[1]
312
    ToStdout("Will relocate instance %s to node %s", iname, node)
313
    op = opcodes.OpReplaceDisks(instance_name=iname,
314
                                remote_node=node, disks=[],
315
                                mode=constants.REPLACE_DISK_CHG,
316
                                early_release=opts.early_release)
317
    jex.QueueJob(iname, op)
318
  results = jex.GetResults()
319
  bad_cnt = len([row for row in results if not row[0]])
320
  if bad_cnt == 0:
321
    ToStdout("All %d instance(s) failed over successfully.", len(results))
322
    rcode = constants.EXIT_SUCCESS
323
  else:
324
    ToStdout("There were errors during the failover:\n"
325
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
326
    rcode = constants.EXIT_FAILURE
327
  return rcode
328

    
329

    
330
def FailoverNode(opts, args):
331
  """Failover all primary instance on a node.
332

    
333
  @param opts: the command line options selected by the user
334
  @type args: list
335
  @param args: should be an empty list
336
  @rtype: int
337
  @return: the desired exit code
338

    
339
  """
340
  cl = GetClient()
341
  force = opts.force
342
  selected_fields = ["name", "pinst_list"]
343

    
344
  # these fields are static data anyway, so it doesn't matter, but
345
  # locking=True should be safer
346
  result = cl.QueryNodes(names=args, fields=selected_fields,
347
                         use_locking=False)
348
  node, pinst = result[0]
349

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

    
354
  pinst = utils.NiceSort(pinst)
355

    
356
  retcode = 0
357

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

    
362
  jex = JobExecutor(cl=cl, opts=opts)
363
  for iname in pinst:
364
    op = opcodes.OpFailoverInstance(instance_name=iname,
365
                                    ignore_consistency=opts.ignore_consistency)
366
    jex.QueueJob(iname, op)
367
  results = jex.GetResults()
368
  bad_cnt = len([row for row in results if not row[0]])
369
  if bad_cnt == 0:
370
    ToStdout("All %d instance(s) failed over successfully.", len(results))
371
  else:
372
    ToStdout("There were errors during the failover:\n"
373
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
374
  return retcode
375

    
376

    
377
def MigrateNode(opts, args):
378
  """Migrate all primary instance on a node.
379

    
380
  """
381
  cl = GetClient()
382
  force = opts.force
383
  selected_fields = ["name", "pinst_list"]
384

    
385
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
386
  node, pinst = result[0]
387

    
388
  if not pinst:
389
    ToStdout("No primary instances on node %s, exiting." % node)
390
    return 0
391

    
392
  pinst = utils.NiceSort(pinst)
393

    
394
  if not force and not AskUser("Migrate instance(s) %s?" %
395
                               (",".join("'%s'" % name for name in pinst))):
396
    return 2
397

    
398
  # this should be removed once --non-live is deprecated
399
  if not opts.live and opts.migration_mode is not None:
400
    raise errors.OpPrereqError("Only one of the --non-live and "
401
                               "--migration-mode options can be passed",
402
                               errors.ECODE_INVAL)
403
  if not opts.live: # --non-live passed
404
    mode = constants.HT_MIGRATION_NONLIVE
405
  else:
406
    mode = opts.migration_mode
407
  op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
408
  SubmitOpCode(op, cl=cl, opts=opts)
409

    
410

    
411
def ShowNodeConfig(opts, args):
412
  """Show node information.
413

    
414
  @param opts: the command line options selected by the user
415
  @type args: list
416
  @param args: should either be an empty list, in which case
417
      we show information about all nodes, or should contain
418
      a list of nodes to be queried for information
419
  @rtype: int
420
  @return: the desired exit code
421

    
422
  """
423
  cl = GetClient()
424
  result = cl.QueryNodes(fields=["name", "pip", "sip",
425
                                 "pinst_list", "sinst_list",
426
                                 "master_candidate", "drained", "offline"],
427
                         names=args, use_locking=False)
428

    
429
  for (name, primary_ip, secondary_ip, pinst, sinst,
430
       is_mc, drained, offline) in result:
431
    ToStdout("Node name: %s", name)
432
    ToStdout("  primary ip: %s", primary_ip)
433
    ToStdout("  secondary ip: %s", secondary_ip)
434
    ToStdout("  master candidate: %s", is_mc)
435
    ToStdout("  drained: %s", drained)
436
    ToStdout("  offline: %s", offline)
437
    if pinst:
438
      ToStdout("  primary for instances:")
439
      for iname in utils.NiceSort(pinst):
440
        ToStdout("    - %s", iname)
441
    else:
442
      ToStdout("  primary for no instances")
443
    if sinst:
444
      ToStdout("  secondary for instances:")
445
      for iname in utils.NiceSort(sinst):
446
        ToStdout("    - %s", iname)
447
    else:
448
      ToStdout("  secondary for no instances")
449

    
450
  return 0
451

    
452

    
453
def RemoveNode(opts, args):
454
  """Remove a node from the cluster.
455

    
456
  @param opts: the command line options selected by the user
457
  @type args: list
458
  @param args: should contain only one element, the name of
459
      the node to be removed
460
  @rtype: int
461
  @return: the desired exit code
462

    
463
  """
464
  op = opcodes.OpRemoveNode(node_name=args[0])
465
  SubmitOpCode(op, opts=opts)
466
  return 0
467

    
468

    
469
def PowercycleNode(opts, args):
470
  """Remove a node from the cluster.
471

    
472
  @param opts: the command line options selected by the user
473
  @type args: list
474
  @param args: should contain only one element, the name of
475
      the node to be removed
476
  @rtype: int
477
  @return: the desired exit code
478

    
479
  """
480
  node = args[0]
481
  if (not opts.confirm and
482
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
483
    return 2
484

    
485
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
486
  result = SubmitOpCode(op, opts=opts)
487
  if result:
488
    ToStderr(result)
489
  return 0
490

    
491

    
492
def ListVolumes(opts, args):
493
  """List logical volumes on node(s).
494

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

    
503
  """
504
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
505

    
506
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
507
  output = SubmitOpCode(op, opts=opts)
508

    
509
  if not opts.no_headers:
510
    headers = {"node": "Node", "phys": "PhysDev",
511
               "vg": "VG", "name": "Name",
512
               "size": "Size", "instance": "Instance"}
513
  else:
514
    headers = None
515

    
516
  unitfields = ["size"]
517

    
518
  numfields = ["size"]
519

    
520
  data = GenerateTable(separator=opts.separator, headers=headers,
521
                       fields=selected_fields, unitfields=unitfields,
522
                       numfields=numfields, data=output, units=opts.units)
523

    
524
  for line in data:
525
    ToStdout(line)
526

    
527
  return 0
528

    
529

    
530
def ListStorage(opts, args):
531
  """List physical volumes on node(s).
532

    
533
  @param opts: the command line options selected by the user
534
  @type args: list
535
  @param args: should either be an empty list, in which case
536
      we list data for all nodes, or contain a list of nodes
537
      to display data only for those
538
  @rtype: int
539
  @return: the desired exit code
540

    
541
  """
542
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
543
  if opts.user_storage_type is None:
544
    opts.user_storage_type = constants.ST_LVM_PV
545

    
546
  storage_type = ConvertStorageType(opts.user_storage_type)
547

    
548
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
549

    
550
  op = opcodes.OpQueryNodeStorage(nodes=args,
551
                                  storage_type=storage_type,
552
                                  output_fields=selected_fields)
553
  output = SubmitOpCode(op, opts=opts)
554

    
555
  if not opts.no_headers:
556
    headers = {
557
      constants.SF_NODE: "Node",
558
      constants.SF_TYPE: "Type",
559
      constants.SF_NAME: "Name",
560
      constants.SF_SIZE: "Size",
561
      constants.SF_USED: "Used",
562
      constants.SF_FREE: "Free",
563
      constants.SF_ALLOCATABLE: "Allocatable",
564
      }
565
  else:
566
    headers = None
567

    
568
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
569
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
570

    
571
  # change raw values to nicer strings
572
  for row in output:
573
    for idx, field in enumerate(selected_fields):
574
      val = row[idx]
575
      if field == constants.SF_ALLOCATABLE:
576
        if val:
577
          val = "Y"
578
        else:
579
          val = "N"
580
      row[idx] = str(val)
581

    
582
  data = GenerateTable(separator=opts.separator, headers=headers,
583
                       fields=selected_fields, unitfields=unitfields,
584
                       numfields=numfields, data=output, units=opts.units)
585

    
586
  for line in data:
587
    ToStdout(line)
588

    
589
  return 0
590

    
591

    
592
def ModifyStorage(opts, args):
593
  """Modify storage volume on a node.
594

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

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

    
604
  storage_type = ConvertStorageType(user_storage_type)
605

    
606
  changes = {}
607

    
608
  if opts.allocatable is not None:
609
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
610

    
611
  if changes:
612
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
613
                                     storage_type=storage_type,
614
                                     name=volume_name,
615
                                     changes=changes)
616
    SubmitOpCode(op, opts=opts)
617
  else:
618
    ToStderr("No changes to perform, exiting.")
619

    
620

    
621
def RepairStorage(opts, args):
622
  """Repairs a storage volume on a node.
623

    
624
  @param opts: the command line options selected by the user
625
  @type args: list
626
  @param args: should contain 3 items: node name, storage type and volume name
627
  @rtype: int
628
  @return: the desired exit code
629

    
630
  """
631
  (node_name, user_storage_type, volume_name) = args
632

    
633
  storage_type = ConvertStorageType(user_storage_type)
634

    
635
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
636
                                   storage_type=storage_type,
637
                                   name=volume_name,
638
                                   ignore_consistency=opts.ignore_consistency)
639
  SubmitOpCode(op, opts=opts)
640

    
641

    
642
def SetNodeParams(opts, args):
643
  """Modifies a node.
644

    
645
  @param opts: the command line options selected by the user
646
  @type args: list
647
  @param args: should contain only one element, the node name
648
  @rtype: int
649
  @return: the desired exit code
650

    
651
  """
652
  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
653
    ToStderr("Please give at least one of the parameters.")
654
    return 1
655

    
656
  op = opcodes.OpSetNodeParams(node_name=args[0],
657
                               master_candidate=opts.master_candidate,
658
                               offline=opts.offline,
659
                               drained=opts.drained,
660
                               force=opts.force,
661
                               auto_promote=opts.auto_promote)
662

    
663
  # even if here we process the result, we allow submit only
664
  result = SubmitOrSend(op, opts)
665

    
666
  if result:
667
    ToStdout("Modified node %s", args[0])
668
    for param, data in result:
669
      ToStdout(" - %-5s -> %s", param, data)
670
  return 0
671

    
672

    
673
commands = {
674
  'add': (
675
    AddNode, [ArgHost(min=1, max=1)],
676
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
677
     VERBOSE_OPT],
678
    "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup]  [--verbose] "
679
    " <node_name>",
680
    "Add a node to the cluster"),
681
  'evacuate': (
682
    EvacuateNode, [ArgNode(min=1)],
683
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT],
684
    "[-f] {-I <iallocator> | -n <dst>} <node>",
685
    "Relocate the secondary instances from a node"
686
    " to other nodes (only for instances with drbd disk template)"),
687
  'failover': (
688
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT],
689
    "[-f] <node>",
690
    "Stops the primary instances on a node and start them on their"
691
    " secondary node (only for instances with drbd disk template)"),
692
  'migrate': (
693
    MigrateNode, ARGS_ONE_NODE, [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT],
694
    "[-f] <node>",
695
    "Migrate all the primary instance on a node away from it"
696
    " (only for instances of type drbd)"),
697
  'info': (
698
    ShowNodeConfig, ARGS_MANY_NODES, [],
699
    "[<node_name>...]", "Show information about the node(s)"),
700
  'list': (
701
    ListNodes, ARGS_MANY_NODES,
702
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT, ROMAN_OPT],
703
    "[nodes...]",
704
    "Lists the nodes in the cluster. The available fields are (see the man"
705
    " page for details): %s. The default field list is (in order): %s." %
706
    (utils.CommaJoin(_LIST_HEADERS), utils.CommaJoin(_LIST_DEF_FIELDS))),
707
  'modify': (
708
    SetNodeParams, ARGS_ONE_NODE,
709
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
710
     AUTO_PROMOTE_OPT, DRY_RUN_OPT],
711
    "<node_name>", "Alters the parameters of a node"),
712
  'powercycle': (
713
    PowercycleNode, ARGS_ONE_NODE,
714
    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT],
715
    "<node_name>", "Tries to forcefully powercycle a node"),
716
  'remove': (
717
    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT],
718
    "<node_name>", "Removes a node from the cluster"),
719
  'volumes': (
720
    ListVolumes, [ArgNode()],
721
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
722
    "[<node_name>...]", "List logical volumes on node(s)"),
723
  'list-storage': (
724
    ListStorage, ARGS_MANY_NODES,
725
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT],
726
    "[<node_name>...]", "List physical volumes on node(s). The available"
727
    " fields are (see the man page for details): %s." %
728
    (utils.CommaJoin(_LIST_STOR_HEADERS))),
729
  'modify-storage': (
730
    ModifyStorage,
731
    [ArgNode(min=1, max=1),
732
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
733
     ArgFile(min=1, max=1)],
734
    [ALLOCATABLE_OPT, DRY_RUN_OPT],
735
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
736
  'repair-storage': (
737
    RepairStorage,
738
    [ArgNode(min=1, max=1),
739
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
740
     ArgFile(min=1, max=1)],
741
    [IGNORE_CONSIST_OPT, DRY_RUN_OPT],
742
    "<node_name> <storage_type> <name>",
743
    "Repairs a storage volume on a node"),
744
  'list-tags': (
745
    ListTags, ARGS_ONE_NODE, [],
746
    "<node_name>", "List the tags of the given node"),
747
  'add-tags': (
748
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
749
    "<node_name> tag...", "Add tags to the given node"),
750
  'remove-tags': (
751
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
752
    "<node_name> tag...", "Remove tags from the given node"),
753
  }
754

    
755

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