Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 8572f1fe

History | View | Annotate | Download (25.1 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
from cStringIO import StringIO
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 field list for L{ListVolumes}
49
_LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
50

    
51

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

    
63

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

    
67

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

    
79

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

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

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

    
100
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
101

    
102

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

    
108

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

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

    
119

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

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

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

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

    
138
  cmd.extend(nodes)
139

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

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

    
147

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

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

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

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

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

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

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

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

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

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

    
206

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

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

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

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

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

    
226

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

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

236
  """
237
  return GenericListFields(constants.QR_NODE, args, opts.separator,
238
                           not opts.no_headers)
239

    
240

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

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

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

    
254
  dst_node = opts.dst_node
255
  iallocator = opts.iallocator
256

    
257
  op = opcodes.OpNodeEvacuationStrategy(nodes=args,
258
                                        iallocator=iallocator,
259
                                        remote_node=dst_node)
260

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

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

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

    
294

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

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

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

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

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

    
319
  pinst = utils.NiceSort(pinst)
320

    
321
  retcode = 0
322

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

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

    
341

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

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

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

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

    
357
  pinst = utils.NiceSort(pinst)
358

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

    
363
  # this should be removed once --non-live is deprecated
364
  if not opts.live and opts.migration_mode is not None:
365
    raise errors.OpPrereqError("Only one of the --non-live and "
366
                               "--migration-mode options can be passed",
367
                               errors.ECODE_INVAL)
368
  if not opts.live: # --non-live passed
369
    mode = constants.HT_MIGRATION_NONLIVE
370
  else:
371
    mode = opts.migration_mode
372
  op = opcodes.OpMigrateNode(node_name=args[0], mode=mode)
373
  SubmitOpCode(op, cl=cl, opts=opts)
374

    
375

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

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

387
  """
388
  cl = GetClient()
389
  result = cl.QueryNodes(fields=["name", "pip", "sip",
390
                                 "pinst_list", "sinst_list",
391
                                 "master_candidate", "drained", "offline",
392
                                 "master_capable", "vm_capable", "powered",
393
                                 "ndparams", "custom_ndparams"],
394
                         names=args, use_locking=False)
395

    
396
  for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
397
       master_capable, vm_capable, powered, ndparams,
398
       ndparams_custom) in result:
399
    ToStdout("Node name: %s", name)
400
    ToStdout("  primary ip: %s", primary_ip)
401
    ToStdout("  secondary ip: %s", secondary_ip)
402
    ToStdout("  master candidate: %s", is_mc)
403
    ToStdout("  drained: %s", drained)
404
    ToStdout("  offline: %s", offline)
405
    if powered is not None:
406
      ToStdout("  powered: %s", powered)
407
    ToStdout("  master_capable: %s", master_capable)
408
    ToStdout("  vm_capable: %s", vm_capable)
409
    if vm_capable:
410
      if pinst:
411
        ToStdout("  primary for instances:")
412
        for iname in utils.NiceSort(pinst):
413
          ToStdout("    - %s", iname)
414
      else:
415
        ToStdout("  primary for no instances")
416
      if sinst:
417
        ToStdout("  secondary for instances:")
418
        for iname in utils.NiceSort(sinst):
419
          ToStdout("    - %s", iname)
420
      else:
421
        ToStdout("  secondary for no instances")
422
    ToStdout("  node parameters:")
423
    buf = StringIO()
424
    FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
425
    ToStdout(buf.getvalue().rstrip("\n"))
426

    
427
  return 0
428

    
429

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

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

440
  """
441
  op = opcodes.OpRemoveNode(node_name=args[0])
442
  SubmitOpCode(op, opts=opts)
443
  return 0
444

    
445

    
446
def PowercycleNode(opts, args):
447
  """Remove a node from the cluster.
448

449
  @param opts: the command line options selected by the user
450
  @type args: list
451
  @param args: should contain only one element, the name of
452
      the node to be removed
453
  @rtype: int
454
  @return: the desired exit code
455

456
  """
457
  node = args[0]
458
  if (not opts.confirm and
459
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
460
    return 2
461

    
462
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
463
  result = SubmitOpCode(op, opts=opts)
464
  if result:
465
    ToStderr(result)
466
  return 0
467

    
468

    
469
def PowerNode(opts, args):
470
  """Change/ask power state of a node.
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
  command = args[0]
481
  node = args[1]
482

    
483
  if command not in _LIST_POWER_COMMANDS:
484
    ToStderr("power subcommand %s not supported." % command)
485
    return constants.EXIT_FAILURE
486

    
487
  oob_command = "power-%s" % command
488

    
489
  opcodelist = []
490
  if oob_command == constants.OOB_POWER_OFF:
491
    opcodelist.append(opcodes.OpSetNodeParams(node_name=node, offline=True,
492
                                              auto_promote=opts.auto_promote))
493

    
494
  opcodelist.append(opcodes.OpOobCommand(node_name=node, command=oob_command))
495

    
496
  cli.SetGenericOpcodeOpts(opcodelist, opts)
497

    
498
  job_id = cli.SendJob(opcodelist)
499

    
500
  # We just want the OOB Opcode status
501
  # If it fails PollJob gives us the error message in it
502
  result = cli.PollJob(job_id)[-1]
503

    
504
  if result:
505
    if oob_command == constants.OOB_POWER_STATUS:
506
      text = "The machine is %spowered"
507
      if result[constants.OOB_POWER_STATUS_POWERED]:
508
        result = text % ""
509
      else:
510
        result = text % "not "
511
    ToStderr(result)
512

    
513
  return constants.EXIT_SUCCESS
514

    
515

    
516
def ListVolumes(opts, args):
517
  """List logical volumes on node(s).
518

519
  @param opts: the command line options selected by the user
520
  @type args: list
521
  @param args: should either be an empty list, in which case
522
      we list data for all nodes, or contain a list of nodes
523
      to display data only for those
524
  @rtype: int
525
  @return: the desired exit code
526

527
  """
528
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
529

    
530
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
531
  output = SubmitOpCode(op, opts=opts)
532

    
533
  if not opts.no_headers:
534
    headers = {"node": "Node", "phys": "PhysDev",
535
               "vg": "VG", "name": "Name",
536
               "size": "Size", "instance": "Instance"}
537
  else:
538
    headers = None
539

    
540
  unitfields = ["size"]
541

    
542
  numfields = ["size"]
543

    
544
  data = GenerateTable(separator=opts.separator, headers=headers,
545
                       fields=selected_fields, unitfields=unitfields,
546
                       numfields=numfields, data=output, units=opts.units)
547

    
548
  for line in data:
549
    ToStdout(line)
550

    
551
  return 0
552

    
553

    
554
def ListStorage(opts, args):
555
  """List physical volumes on node(s).
556

557
  @param opts: the command line options selected by the user
558
  @type args: list
559
  @param args: should either be an empty list, in which case
560
      we list data for all nodes, or contain a list of nodes
561
      to display data only for those
562
  @rtype: int
563
  @return: the desired exit code
564

565
  """
566
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
567
  if opts.user_storage_type is None:
568
    opts.user_storage_type = constants.ST_LVM_PV
569

    
570
  storage_type = ConvertStorageType(opts.user_storage_type)
571

    
572
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
573

    
574
  op = opcodes.OpQueryNodeStorage(nodes=args,
575
                                  storage_type=storage_type,
576
                                  output_fields=selected_fields)
577
  output = SubmitOpCode(op, opts=opts)
578

    
579
  if not opts.no_headers:
580
    headers = {
581
      constants.SF_NODE: "Node",
582
      constants.SF_TYPE: "Type",
583
      constants.SF_NAME: "Name",
584
      constants.SF_SIZE: "Size",
585
      constants.SF_USED: "Used",
586
      constants.SF_FREE: "Free",
587
      constants.SF_ALLOCATABLE: "Allocatable",
588
      }
589
  else:
590
    headers = None
591

    
592
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
593
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
594

    
595
  # change raw values to nicer strings
596
  for row in output:
597
    for idx, field in enumerate(selected_fields):
598
      val = row[idx]
599
      if field == constants.SF_ALLOCATABLE:
600
        if val:
601
          val = "Y"
602
        else:
603
          val = "N"
604
      row[idx] = str(val)
605

    
606
  data = GenerateTable(separator=opts.separator, headers=headers,
607
                       fields=selected_fields, unitfields=unitfields,
608
                       numfields=numfields, data=output, units=opts.units)
609

    
610
  for line in data:
611
    ToStdout(line)
612

    
613
  return 0
614

    
615

    
616
def ModifyStorage(opts, args):
617
  """Modify storage volume on a node.
618

619
  @param opts: the command line options selected by the user
620
  @type args: list
621
  @param args: should contain 3 items: node name, storage type and volume name
622
  @rtype: int
623
  @return: the desired exit code
624

625
  """
626
  (node_name, user_storage_type, volume_name) = args
627

    
628
  storage_type = ConvertStorageType(user_storage_type)
629

    
630
  changes = {}
631

    
632
  if opts.allocatable is not None:
633
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
634

    
635
  if changes:
636
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
637
                                     storage_type=storage_type,
638
                                     name=volume_name,
639
                                     changes=changes)
640
    SubmitOpCode(op, opts=opts)
641
  else:
642
    ToStderr("No changes to perform, exiting.")
643

    
644

    
645
def RepairStorage(opts, args):
646
  """Repairs a storage volume on a node.
647

648
  @param opts: the command line options selected by the user
649
  @type args: list
650
  @param args: should contain 3 items: node name, storage type and volume name
651
  @rtype: int
652
  @return: the desired exit code
653

654
  """
655
  (node_name, user_storage_type, volume_name) = args
656

    
657
  storage_type = ConvertStorageType(user_storage_type)
658

    
659
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
660
                                   storage_type=storage_type,
661
                                   name=volume_name,
662
                                   ignore_consistency=opts.ignore_consistency)
663
  SubmitOpCode(op, opts=opts)
664

    
665

    
666
def SetNodeParams(opts, args):
667
  """Modifies a node.
668

669
  @param opts: the command line options selected by the user
670
  @type args: list
671
  @param args: should contain only one element, the node name
672
  @rtype: int
673
  @return: the desired exit code
674

675
  """
676
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
677
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
678
                 opts.ndparams]
679
  if all_changes.count(None) == len(all_changes):
680
    ToStderr("Please give at least one of the parameters.")
681
    return 1
682

    
683
  op = opcodes.OpSetNodeParams(node_name=args[0],
684
                               master_candidate=opts.master_candidate,
685
                               offline=opts.offline,
686
                               drained=opts.drained,
687
                               master_capable=opts.master_capable,
688
                               vm_capable=opts.vm_capable,
689
                               secondary_ip=opts.secondary_ip,
690
                               force=opts.force,
691
                               ndparams=opts.ndparams,
692
                               auto_promote=opts.auto_promote,
693
                               powered=opts.node_powered)
694

    
695
  # even if here we process the result, we allow submit only
696
  result = SubmitOrSend(op, opts)
697

    
698
  if result:
699
    ToStdout("Modified node %s", args[0])
700
    for param, data in result:
701
      ToStdout(" - %-5s -> %s", param, data)
702
  return 0
703

    
704

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

    
807

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