Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 016acd85

History | View | Annotate | Download (24.8 kB)

1
#
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
from ganeti.cli import *
30
from ganeti import cli
31
from ganeti import bootstrap
32
from ganeti import opcodes
33
from ganeti import utils
34
from ganeti import constants
35
from ganeti import errors
36
from ganeti import netutils
37

    
38

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

    
46

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

    
50

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

    
62

    
63
#: default list of power commands
64
_LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
65

    
66

    
67
#: headers (and full field list) for L{ListStorage}
68
_LIST_STOR_HEADERS = {
69
  constants.SF_NODE: "Node",
70
  constants.SF_TYPE: "Type",
71
  constants.SF_NAME: "Name",
72
  constants.SF_SIZE: "Size",
73
  constants.SF_USED: "Used",
74
  constants.SF_FREE: "Free",
75
  constants.SF_ALLOCATABLE: "Allocatable",
76
  }
77

    
78

    
79
#: User-facing storage unit types
80
_USER_STORAGE_TYPE = {
81
  constants.ST_FILE: "file",
82
  constants.ST_LVM_PV: "lvm-pv",
83
  constants.ST_LVM_VG: "lvm-vg",
84
  }
85

    
86
_STORAGE_TYPE_OPT = \
87
  cli_option("-t", "--storage-type",
88
             dest="user_storage_type",
89
             choices=_USER_STORAGE_TYPE.keys(),
90
             default=None,
91
             metavar="STORAGE_TYPE",
92
             help=("Storage type (%s)" %
93
                   utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
94

    
95
_REPAIRABLE_STORAGE_TYPES = \
96
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
97
   if constants.SO_FIX_CONSISTENCY in so]
98

    
99
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
100

    
101

    
102
NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
103
                              action="store_false", dest="node_setup",
104
                              help=("Do not make initial SSH setup on remote"
105
                                    " node (needs to be done manually)"))
106

    
107

    
108
def ConvertStorageType(user_storage_type):
109
  """Converts a user storage type to its internal name.
110

111
  """
112
  try:
113
    return _USER_STORAGE_TYPE[user_storage_type]
114
  except KeyError:
115
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
116
                               errors.ECODE_INVAL)
117

    
118

    
119
def _RunSetupSSH(options, nodes):
120
  """Wrapper around utils.RunCmd to call setup-ssh
121

122
  @param options: The command line options
123
  @param nodes: The nodes to setup
124

125
  """
126
  cmd = [constants.SETUP_SSH]
127

    
128
  # Pass --debug|--verbose to the external script if set on our invocation
129
  # --debug overrides --verbose
130
  if options.debug:
131
    cmd.append("--debug")
132
  elif options.verbose:
133
    cmd.append("--verbose")
134
  if not options.ssh_key_check:
135
    cmd.append("--no-ssh-key-check")
136

    
137
  cmd.extend(nodes)
138

    
139
  result = utils.RunCmd(cmd, interactive=True)
140

    
141
  if result.failed:
142
    errmsg = ("Command '%s' failed with exit code %s; output %r" %
143
              (result.cmd, result.exit_code, result.output))
144
    raise errors.OpExecError(errmsg)
145

    
146

    
147
@UsesRPC
148
def AddNode(opts, args):
149
  """Add a node to the cluster.
150

151
  @param opts: the command line options selected by the user
152
  @type args: list
153
  @param args: should contain only one element, the new node name
154
  @rtype: int
155
  @return: the desired exit code
156

157
  """
158
  cl = GetClient()
159
  node = netutils.GetHostname(name=args[0]).name
160
  readd = opts.readd
161

    
162
  try:
163
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
164
                           use_locking=False)
165
    node_exists, sip = output[0]
166
  except (errors.OpPrereqError, errors.OpExecError):
167
    node_exists = ""
168
    sip = None
169

    
170
  if readd:
171
    if not node_exists:
172
      ToStderr("Node %s not in the cluster"
173
               " - please retry without '--readd'", node)
174
      return 1
175
  else:
176
    if node_exists:
177
      ToStderr("Node %s already in the cluster (as %s)"
178
               " - please retry with '--readd'", node, node_exists)
179
      return 1
180
    sip = opts.secondary_ip
181

    
182
  # read the cluster name from the master
183
  output = cl.QueryConfigValues(['cluster_name'])
184
  cluster_name = output[0]
185

    
186
  if not readd and opts.node_setup:
187
    ToStderr("-- WARNING -- \n"
188
             "Performing this operation is going to replace the ssh daemon"
189
             " keypair\n"
190
             "on the target machine (%s) with the ones of the"
191
             " current one\n"
192
             "and grant full intra-cluster ssh root access to/from it\n", node)
193

    
194
  if opts.node_setup:
195
    _RunSetupSSH(opts, [node])
196

    
197
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
198

    
199
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
200
                         readd=opts.readd, group=opts.nodegroup,
201
                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
202
                         master_capable=opts.master_capable)
203
  SubmitOpCode(op, opts=opts)
204

    
205

    
206
def ListNodes(opts, args):
207
  """List nodes and their properties.
208

209
  @param opts: the command line options selected by the user
210
  @type args: list
211
  @param args: nodes to list, or empty for all
212
  @rtype: int
213
  @return: the desired exit code
214

215
  """
216
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
217

    
218
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
219
                              (",".join, False))
220

    
221
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
222
                     opts.separator, not opts.no_headers,
223
                     format_override=fmtoverride)
224

    
225

    
226
def ListNodeFields(opts, args):
227
  """List node fields.
228

229
  @param opts: the command line options selected by the user
230
  @type args: list
231
  @param args: fields to list, or empty for all
232
  @rtype: int
233
  @return: the desired exit code
234

235
  """
236
  return GenericListFields(constants.QR_NODE, args, opts.separator,
237
                           not opts.no_headers)
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
                                 "master_capable", "vm_capable", "powered"],
392
                         names=args, use_locking=False)
393

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

    
420
  return 0
421

    
422

    
423
def RemoveNode(opts, args):
424
  """Remove a node from the cluster.
425

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

433
  """
434
  op = opcodes.OpRemoveNode(node_name=args[0])
435
  SubmitOpCode(op, opts=opts)
436
  return 0
437

    
438

    
439
def PowercycleNode(opts, args):
440
  """Remove a node from the cluster.
441

442
  @param opts: the command line options selected by the user
443
  @type args: list
444
  @param args: should contain only one element, the name of
445
      the node to be removed
446
  @rtype: int
447
  @return: the desired exit code
448

449
  """
450
  node = args[0]
451
  if (not opts.confirm and
452
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
453
    return 2
454

    
455
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
456
  result = SubmitOpCode(op, opts=opts)
457
  if result:
458
    ToStderr(result)
459
  return 0
460

    
461

    
462
def PowerNode(opts, args):
463
  """Change/ask power state of a node.
464

465
  @param opts: the command line options selected by the user
466
  @type args: list
467
  @param args: should contain only one element, the name of
468
      the node to be removed
469
  @rtype: int
470
  @return: the desired exit code
471

472
  """
473
  command = args[0]
474
  node = args[1]
475

    
476
  if command not in _LIST_POWER_COMMANDS:
477
    ToStderr("power subcommand %s not supported." % command)
478
    return constants.EXIT_FAILURE
479

    
480
  oob_command = "power-%s" % command
481

    
482
  opcodelist = []
483
  if oob_command == constants.OOB_POWER_OFF:
484
    opcodelist.append(opcodes.OpSetNodeParams(node_name=node, offline=True,
485
                                              auto_promote=opts.auto_promote))
486

    
487
  opcodelist.append(opcodes.OpOobCommand(node_name=node, command=oob_command))
488

    
489
  cli.SetGenericOpcodeOpts(opcodelist, opts)
490

    
491
  job_id = cli.SendJob(opcodelist)
492

    
493
  # We just want the OOB Opcode status
494
  # If it fails PollJob gives us the error message in it
495
  result = cli.PollJob(job_id)[-1]
496

    
497
  if result:
498
    if oob_command == constants.OOB_POWER_STATUS:
499
      text = "The machine is %spowered"
500
      if result[constants.OOB_POWER_STATUS_POWERED]:
501
        result = text % ""
502
      else:
503
        result = text % "not "
504
    ToStderr(result)
505

    
506
  return constants.EXIT_SUCCESS
507

    
508

    
509
def ListVolumes(opts, args):
510
  """List logical volumes on node(s).
511

512
  @param opts: the command line options selected by the user
513
  @type args: list
514
  @param args: should either be an empty list, in which case
515
      we list data for all nodes, or contain a list of nodes
516
      to display data only for those
517
  @rtype: int
518
  @return: the desired exit code
519

520
  """
521
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
522

    
523
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
524
  output = SubmitOpCode(op, opts=opts)
525

    
526
  if not opts.no_headers:
527
    headers = {"node": "Node", "phys": "PhysDev",
528
               "vg": "VG", "name": "Name",
529
               "size": "Size", "instance": "Instance"}
530
  else:
531
    headers = None
532

    
533
  unitfields = ["size"]
534

    
535
  numfields = ["size"]
536

    
537
  data = GenerateTable(separator=opts.separator, headers=headers,
538
                       fields=selected_fields, unitfields=unitfields,
539
                       numfields=numfields, data=output, units=opts.units)
540

    
541
  for line in data:
542
    ToStdout(line)
543

    
544
  return 0
545

    
546

    
547
def ListStorage(opts, args):
548
  """List physical volumes on node(s).
549

550
  @param opts: the command line options selected by the user
551
  @type args: list
552
  @param args: should either be an empty list, in which case
553
      we list data for all nodes, or contain a list of nodes
554
      to display data only for those
555
  @rtype: int
556
  @return: the desired exit code
557

558
  """
559
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
560
  if opts.user_storage_type is None:
561
    opts.user_storage_type = constants.ST_LVM_PV
562

    
563
  storage_type = ConvertStorageType(opts.user_storage_type)
564

    
565
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
566

    
567
  op = opcodes.OpQueryNodeStorage(nodes=args,
568
                                  storage_type=storage_type,
569
                                  output_fields=selected_fields)
570
  output = SubmitOpCode(op, opts=opts)
571

    
572
  if not opts.no_headers:
573
    headers = {
574
      constants.SF_NODE: "Node",
575
      constants.SF_TYPE: "Type",
576
      constants.SF_NAME: "Name",
577
      constants.SF_SIZE: "Size",
578
      constants.SF_USED: "Used",
579
      constants.SF_FREE: "Free",
580
      constants.SF_ALLOCATABLE: "Allocatable",
581
      }
582
  else:
583
    headers = None
584

    
585
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
586
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
587

    
588
  # change raw values to nicer strings
589
  for row in output:
590
    for idx, field in enumerate(selected_fields):
591
      val = row[idx]
592
      if field == constants.SF_ALLOCATABLE:
593
        if val:
594
          val = "Y"
595
        else:
596
          val = "N"
597
      row[idx] = str(val)
598

    
599
  data = GenerateTable(separator=opts.separator, headers=headers,
600
                       fields=selected_fields, unitfields=unitfields,
601
                       numfields=numfields, data=output, units=opts.units)
602

    
603
  for line in data:
604
    ToStdout(line)
605

    
606
  return 0
607

    
608

    
609
def ModifyStorage(opts, args):
610
  """Modify storage volume on a node.
611

612
  @param opts: the command line options selected by the user
613
  @type args: list
614
  @param args: should contain 3 items: node name, storage type and volume name
615
  @rtype: int
616
  @return: the desired exit code
617

618
  """
619
  (node_name, user_storage_type, volume_name) = args
620

    
621
  storage_type = ConvertStorageType(user_storage_type)
622

    
623
  changes = {}
624

    
625
  if opts.allocatable is not None:
626
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
627

    
628
  if changes:
629
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
630
                                     storage_type=storage_type,
631
                                     name=volume_name,
632
                                     changes=changes)
633
    SubmitOpCode(op, opts=opts)
634
  else:
635
    ToStderr("No changes to perform, exiting.")
636

    
637

    
638
def RepairStorage(opts, args):
639
  """Repairs a storage volume on a node.
640

641
  @param opts: the command line options selected by the user
642
  @type args: list
643
  @param args: should contain 3 items: node name, storage type and volume name
644
  @rtype: int
645
  @return: the desired exit code
646

647
  """
648
  (node_name, user_storage_type, volume_name) = args
649

    
650
  storage_type = ConvertStorageType(user_storage_type)
651

    
652
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
653
                                   storage_type=storage_type,
654
                                   name=volume_name,
655
                                   ignore_consistency=opts.ignore_consistency)
656
  SubmitOpCode(op, opts=opts)
657

    
658

    
659
def SetNodeParams(opts, args):
660
  """Modifies a node.
661

662
  @param opts: the command line options selected by the user
663
  @type args: list
664
  @param args: should contain only one element, the node name
665
  @rtype: int
666
  @return: the desired exit code
667

668
  """
669
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
670
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
671
                 opts.ndparams]
672
  if all_changes.count(None) == len(all_changes):
673
    ToStderr("Please give at least one of the parameters.")
674
    return 1
675

    
676
  op = opcodes.OpSetNodeParams(node_name=args[0],
677
                               master_candidate=opts.master_candidate,
678
                               offline=opts.offline,
679
                               drained=opts.drained,
680
                               master_capable=opts.master_capable,
681
                               vm_capable=opts.vm_capable,
682
                               secondary_ip=opts.secondary_ip,
683
                               force=opts.force,
684
                               ndparams=opts.ndparams,
685
                               auto_promote=opts.auto_promote,
686
                               powered=opts.node_powered)
687

    
688
  # even if here we process the result, we allow submit only
689
  result = SubmitOrSend(op, opts)
690

    
691
  if result:
692
    ToStdout("Modified node %s", args[0])
693
    for param, data in result:
694
      ToStdout(" - %-5s -> %s", param, data)
695
  return 0
696

    
697

    
698
commands = {
699
  'add': (
700
    AddNode, [ArgHost(min=1, max=1)],
701
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
702
     VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
703
     CAPAB_VM_OPT, NODE_PARAMS_OPT],
704
    "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup]  [--verbose] "
705
    " <node_name>",
706
    "Add a node to the cluster"),
707
  'evacuate': (
708
    EvacuateNode, [ArgNode(min=1)],
709
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
710
     PRIORITY_OPT],
711
    "[-f] {-I <iallocator> | -n <dst>} <node>",
712
    "Relocate the secondary instances from a node"
713
    " to other nodes (only for instances with drbd disk template)"),
714
  'failover': (
715
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
716
    "[-f] <node>",
717
    "Stops the primary instances on a node and start them on their"
718
    " secondary node (only for instances with drbd disk template)"),
719
  'migrate': (
720
    MigrateNode, ARGS_ONE_NODE,
721
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
722
    "[-f] <node>",
723
    "Migrate all the primary instance on a node away from it"
724
    " (only for instances of type drbd)"),
725
  'info': (
726
    ShowNodeConfig, ARGS_MANY_NODES, [],
727
    "[<node_name>...]", "Show information about the node(s)"),
728
  'list': (
729
    ListNodes, ARGS_MANY_NODES,
730
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
731
    "[nodes...]",
732
    "Lists the nodes in the cluster. The available fields can be shown using"
733
    " the \"list-fields\" command (see the man page for details)."
734
    " The default field list is (in order): %s." %
735
    utils.CommaJoin(_LIST_DEF_FIELDS)),
736
  "list-fields": (
737
    ListNodeFields, [ArgUnknown()],
738
    [NOHDR_OPT, SEP_OPT],
739
    "[fields...]",
740
    "Lists all available fields for nodes"),
741
  'modify': (
742
    SetNodeParams, ARGS_ONE_NODE,
743
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
744
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
745
     AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
746
     NODE_POWERED_OPT],
747
    "<node_name>", "Alters the parameters of a node"),
748
  'powercycle': (
749
    PowercycleNode, ARGS_ONE_NODE,
750
    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
751
    "<node_name>", "Tries to forcefully powercycle a node"),
752
  'power': (
753
    PowerNode,
754
    [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
755
     ArgNode(min=1, max=1)],
756
    [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT],
757
    "on|off|cycle|status <node>",
758
    "Change power state of node by calling out-of-band helper."),
759
  'remove': (
760
    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
761
    "<node_name>", "Removes a node from the cluster"),
762
  'volumes': (
763
    ListVolumes, [ArgNode()],
764
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
765
    "[<node_name>...]", "List logical volumes on node(s)"),
766
  'list-storage': (
767
    ListStorage, ARGS_MANY_NODES,
768
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
769
     PRIORITY_OPT],
770
    "[<node_name>...]", "List physical volumes on node(s). The available"
771
    " fields are (see the man page for details): %s." %
772
    (utils.CommaJoin(_LIST_STOR_HEADERS))),
773
  'modify-storage': (
774
    ModifyStorage,
775
    [ArgNode(min=1, max=1),
776
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
777
     ArgFile(min=1, max=1)],
778
    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
779
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
780
  'repair-storage': (
781
    RepairStorage,
782
    [ArgNode(min=1, max=1),
783
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
784
     ArgFile(min=1, max=1)],
785
    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
786
    "<node_name> <storage_type> <name>",
787
    "Repairs a storage volume on a node"),
788
  'list-tags': (
789
    ListTags, ARGS_ONE_NODE, [],
790
    "<node_name>", "List the tags of the given node"),
791
  'add-tags': (
792
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
793
    "<node_name> tag...", "Add tags to the given node"),
794
  'remove-tags': (
795
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
796
    [TAG_SRC_OPT, PRIORITY_OPT],
797
    "<node_name> tag...", "Remove tags from the given node"),
798
  }
799

    
800

    
801
def Main():
802
  return GenericMain(commands, override={"tag_type": constants.TAG_NODE})