Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ f0b1bafe

History | View | Annotate | Download (25.6 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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
  if options.force_join:
138
    cmd.append("--force-join")
139

    
140
  cmd.extend(nodes)
141

    
142
  result = utils.RunCmd(cmd, interactive=True)
143

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

    
149

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

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

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

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

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

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

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

    
197
  if opts.node_setup:
198
    _RunSetupSSH(opts, [node])
199

    
200
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
201

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

    
208

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

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

218
  """
219
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
220

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

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

    
228

    
229
def ListNodeFields(opts, args):
230
  """List node fields.
231

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

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

    
242

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

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

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

    
256
  dst_node = opts.dst_node
257
  iallocator = opts.iallocator
258

    
259
  op = opcodes.OpNodeEvacStrategy(nodes=args,
260
                                  iallocator=iallocator,
261
                                  remote_node=dst_node)
262

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

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

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

    
296

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

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

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

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

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

    
321
  pinst = utils.NiceSort(pinst)
322

    
323
  retcode = 0
324

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

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

    
343

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

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

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

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

    
359
  pinst = utils.NiceSort(pinst)
360

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

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

    
377

    
378
def ShowNodeConfig(opts, args):
379
  """Show node information.
380

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

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

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

    
429
  return 0
430

    
431

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

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

442
  """
443
  op = opcodes.OpNodeRemove(node_name=args[0])
444
  SubmitOpCode(op, opts=opts)
445
  return 0
446

    
447

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

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

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

    
464
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
465
  result = SubmitOpCode(op, opts=opts)
466
  if result:
467
    ToStderr(result)
468
  return 0
469

    
470

    
471
def PowerNode(opts, args):
472
  """Change/ask power state of a node.
473

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

481
  """
482
  command = args[0]
483
  node = args[1]
484

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

    
489
  oob_command = "power-%s" % command
490

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

    
496
  opcodelist.append(opcodes.OpOobCommand(node_names=[node],
497
                                         command=oob_command))
498

    
499
  cli.SetGenericOpcodeOpts(opcodelist, opts)
500

    
501
  job_id = cli.SendJob(opcodelist)
502

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

    
507
  if result:
508
    (_, data_tuple) = result[0]
509
    if data_tuple[0] != constants.RS_NORMAL:
510
      if data_tuple[0] == constants.RS_UNAVAIL:
511
        result = "OOB is not supported"
512
      else:
513
        result = "RPC failed, look out for warning in the output"
514
      ToStderr(result)
515
      return constants.EXIT_FAILURE
516
    else:
517
      if oob_command == constants.OOB_POWER_STATUS:
518
        text = "The machine is %spowered"
519
        if data_tuple[1][constants.OOB_POWER_STATUS_POWERED]:
520
          result = text % ""
521
        else:
522
          result = text % "not "
523
        ToStdout(result)
524

    
525
  return constants.EXIT_SUCCESS
526

    
527

    
528
def ListVolumes(opts, args):
529
  """List logical volumes on node(s).
530

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

539
  """
540
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
541

    
542
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
543
  output = SubmitOpCode(op, opts=opts)
544

    
545
  if not opts.no_headers:
546
    headers = {"node": "Node", "phys": "PhysDev",
547
               "vg": "VG", "name": "Name",
548
               "size": "Size", "instance": "Instance"}
549
  else:
550
    headers = None
551

    
552
  unitfields = ["size"]
553

    
554
  numfields = ["size"]
555

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

    
560
  for line in data:
561
    ToStdout(line)
562

    
563
  return 0
564

    
565

    
566
def ListStorage(opts, args):
567
  """List physical volumes on node(s).
568

569
  @param opts: the command line options selected by the user
570
  @type args: list
571
  @param args: should either be an empty list, in which case
572
      we list data for all nodes, or contain a list of nodes
573
      to display data only for those
574
  @rtype: int
575
  @return: the desired exit code
576

577
  """
578
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
579
  if opts.user_storage_type is None:
580
    opts.user_storage_type = constants.ST_LVM_PV
581

    
582
  storage_type = ConvertStorageType(opts.user_storage_type)
583

    
584
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
585

    
586
  op = opcodes.OpNodeQueryStorage(nodes=args,
587
                                  storage_type=storage_type,
588
                                  output_fields=selected_fields)
589
  output = SubmitOpCode(op, opts=opts)
590

    
591
  if not opts.no_headers:
592
    headers = {
593
      constants.SF_NODE: "Node",
594
      constants.SF_TYPE: "Type",
595
      constants.SF_NAME: "Name",
596
      constants.SF_SIZE: "Size",
597
      constants.SF_USED: "Used",
598
      constants.SF_FREE: "Free",
599
      constants.SF_ALLOCATABLE: "Allocatable",
600
      }
601
  else:
602
    headers = None
603

    
604
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
605
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
606

    
607
  # change raw values to nicer strings
608
  for row in output:
609
    for idx, field in enumerate(selected_fields):
610
      val = row[idx]
611
      if field == constants.SF_ALLOCATABLE:
612
        if val:
613
          val = "Y"
614
        else:
615
          val = "N"
616
      row[idx] = str(val)
617

    
618
  data = GenerateTable(separator=opts.separator, headers=headers,
619
                       fields=selected_fields, unitfields=unitfields,
620
                       numfields=numfields, data=output, units=opts.units)
621

    
622
  for line in data:
623
    ToStdout(line)
624

    
625
  return 0
626

    
627

    
628
def ModifyStorage(opts, args):
629
  """Modify storage volume on a node.
630

631
  @param opts: the command line options selected by the user
632
  @type args: list
633
  @param args: should contain 3 items: node name, storage type and volume name
634
  @rtype: int
635
  @return: the desired exit code
636

637
  """
638
  (node_name, user_storage_type, volume_name) = args
639

    
640
  storage_type = ConvertStorageType(user_storage_type)
641

    
642
  changes = {}
643

    
644
  if opts.allocatable is not None:
645
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
646

    
647
  if changes:
648
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
649
                                     storage_type=storage_type,
650
                                     name=volume_name,
651
                                     changes=changes)
652
    SubmitOpCode(op, opts=opts)
653
  else:
654
    ToStderr("No changes to perform, exiting.")
655

    
656

    
657
def RepairStorage(opts, args):
658
  """Repairs a storage volume on a node.
659

660
  @param opts: the command line options selected by the user
661
  @type args: list
662
  @param args: should contain 3 items: node name, storage type and volume name
663
  @rtype: int
664
  @return: the desired exit code
665

666
  """
667
  (node_name, user_storage_type, volume_name) = args
668

    
669
  storage_type = ConvertStorageType(user_storage_type)
670

    
671
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
672
                                   storage_type=storage_type,
673
                                   name=volume_name,
674
                                   ignore_consistency=opts.ignore_consistency)
675
  SubmitOpCode(op, opts=opts)
676

    
677

    
678
def SetNodeParams(opts, args):
679
  """Modifies a node.
680

681
  @param opts: the command line options selected by the user
682
  @type args: list
683
  @param args: should contain only one element, the node name
684
  @rtype: int
685
  @return: the desired exit code
686

687
  """
688
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
689
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
690
                 opts.ndparams]
691
  if all_changes.count(None) == len(all_changes):
692
    ToStderr("Please give at least one of the parameters.")
693
    return 1
694

    
695
  op = opcodes.OpNodeSetParams(node_name=args[0],
696
                               master_candidate=opts.master_candidate,
697
                               offline=opts.offline,
698
                               drained=opts.drained,
699
                               master_capable=opts.master_capable,
700
                               vm_capable=opts.vm_capable,
701
                               secondary_ip=opts.secondary_ip,
702
                               force=opts.force,
703
                               ndparams=opts.ndparams,
704
                               auto_promote=opts.auto_promote,
705
                               powered=opts.node_powered)
706

    
707
  # even if here we process the result, we allow submit only
708
  result = SubmitOrSend(op, opts)
709

    
710
  if result:
711
    ToStdout("Modified node %s", args[0])
712
    for param, data in result:
713
      ToStdout(" - %-5s -> %s", param, data)
714
  return 0
715

    
716

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

    
820

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