Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ c70e1a9f

History | View | Annotate | Download (24.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 bootstrap
31
from ganeti import opcodes
32
from ganeti import utils
33
from ganeti import constants
34
from ganeti import compat
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
#: headers (and full field list for L{ListNodes}
64
_LIST_HEADERS = {
65
  "name": "Node", "pinst_cnt": "Pinst", "sinst_cnt": "Sinst",
66
  "pinst_list": "PriInstances", "sinst_list": "SecInstances",
67
  "pip": "PrimaryIP", "sip": "SecondaryIP",
68
  "dtotal": "DTotal", "dfree": "DFree",
69
  "mtotal": "MTotal", "mnode": "MNode", "mfree": "MFree",
70
  "bootid": "BootID",
71
  "ctotal": "CTotal", "cnodes": "CNodes", "csockets": "CSockets",
72
  "tags": "Tags",
73
  "serial_no": "SerialNo",
74
  "master_candidate": "MasterC",
75
  "master": "IsMaster",
76
  "offline": "Offline", "drained": "Drained",
77
  "role": "Role",
78
  "ctime": "CTime", "mtime": "MTime", "uuid": "UUID",
79
  "master_capable": "MasterCapable", "vm_capable": "VMCapable",
80
  "group": "Group", "group.uuid": "GroupUUID",
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 not 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, group=opts.nodegroup,
218
                         vm_capable=opts.vm_capable,
219
                         master_capable=opts.master_capable)
220
  SubmitOpCode(op, opts=opts)
221

    
222

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

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

232
  """
233
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
234

    
235
  output = GetClient().QueryNodes(args, selected_fields, opts.do_locking)
236

    
237
  if not opts.no_headers:
238
    headers = _LIST_HEADERS
239
  else:
240
    headers = None
241

    
242
  unitfields = ["dtotal", "dfree", "mtotal", "mnode", "mfree"]
243

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

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

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

    
276
  return 0
277

    
278

    
279
def EvacuateNode(opts, args):
280
  """Relocate all secondary instance from a node.
281

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

288
  """
289
  cl = GetClient()
290
  force = opts.force
291

    
292
  dst_node = opts.dst_node
293
  iallocator = opts.iallocator
294

    
295
  op = opcodes.OpNodeEvacuationStrategy(nodes=args,
296
                                        iallocator=iallocator,
297
                                        remote_node=dst_node)
298

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

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

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

    
332

    
333
def FailoverNode(opts, args):
334
  """Failover all primary instance on a node.
335

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

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

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

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

    
357
  pinst = utils.NiceSort(pinst)
358

    
359
  retcode = 0
360

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

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

    
379

    
380
def MigrateNode(opts, args):
381
  """Migrate all primary instance on a node.
382

383
  """
384
  cl = GetClient()
385
  force = opts.force
386
  selected_fields = ["name", "pinst_list"]
387

    
388
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
389
  node, pinst = result[0]
390

    
391
  if not pinst:
392
    ToStdout("No primary instances on node %s, exiting." % node)
393
    return 0
394

    
395
  pinst = utils.NiceSort(pinst)
396

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

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

    
413

    
414
def ShowNodeConfig(opts, args):
415
  """Show node information.
416

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

425
  """
426
  cl = GetClient()
427
  result = cl.QueryNodes(fields=["name", "pip", "sip",
428
                                 "pinst_list", "sinst_list",
429
                                 "master_candidate", "drained", "offline",
430
                                 "master_capable", "vm_capable"],
431
                         names=args, use_locking=False)
432

    
433
  for (name, primary_ip, secondary_ip, pinst, sinst,
434
       is_mc, drained, offline, master_capable, vm_capable) in result:
435
    ToStdout("Node name: %s", name)
436
    ToStdout("  primary ip: %s", primary_ip)
437
    ToStdout("  secondary ip: %s", secondary_ip)
438
    ToStdout("  master candidate: %s", is_mc)
439
    ToStdout("  drained: %s", drained)
440
    ToStdout("  offline: %s", offline)
441
    ToStdout("  master_capable: %s", master_capable)
442
    ToStdout("  vm_capable: %s", vm_capable)
443
    if vm_capable:
444
      if pinst:
445
        ToStdout("  primary for instances:")
446
        for iname in utils.NiceSort(pinst):
447
          ToStdout("    - %s", iname)
448
      else:
449
        ToStdout("  primary for no instances")
450
      if sinst:
451
        ToStdout("  secondary for instances:")
452
        for iname in utils.NiceSort(sinst):
453
          ToStdout("    - %s", iname)
454
      else:
455
        ToStdout("  secondary for no instances")
456

    
457
  return 0
458

    
459

    
460
def RemoveNode(opts, args):
461
  """Remove a node from the cluster.
462

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

470
  """
471
  op = opcodes.OpRemoveNode(node_name=args[0])
472
  SubmitOpCode(op, opts=opts)
473
  return 0
474

    
475

    
476
def PowercycleNode(opts, args):
477
  """Remove a node from the cluster.
478

479
  @param opts: the command line options selected by the user
480
  @type args: list
481
  @param args: should contain only one element, the name of
482
      the node to be removed
483
  @rtype: int
484
  @return: the desired exit code
485

486
  """
487
  node = args[0]
488
  if (not opts.confirm and
489
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
490
    return 2
491

    
492
  op = opcodes.OpPowercycleNode(node_name=node, force=opts.force)
493
  result = SubmitOpCode(op, opts=opts)
494
  if result:
495
    ToStderr(result)
496
  return 0
497

    
498

    
499
def ListVolumes(opts, args):
500
  """List logical volumes on node(s).
501

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

510
  """
511
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
512

    
513
  op = opcodes.OpQueryNodeVolumes(nodes=args, output_fields=selected_fields)
514
  output = SubmitOpCode(op, opts=opts)
515

    
516
  if not opts.no_headers:
517
    headers = {"node": "Node", "phys": "PhysDev",
518
               "vg": "VG", "name": "Name",
519
               "size": "Size", "instance": "Instance"}
520
  else:
521
    headers = None
522

    
523
  unitfields = ["size"]
524

    
525
  numfields = ["size"]
526

    
527
  data = GenerateTable(separator=opts.separator, headers=headers,
528
                       fields=selected_fields, unitfields=unitfields,
529
                       numfields=numfields, data=output, units=opts.units)
530

    
531
  for line in data:
532
    ToStdout(line)
533

    
534
  return 0
535

    
536

    
537
def ListStorage(opts, args):
538
  """List physical volumes on node(s).
539

540
  @param opts: the command line options selected by the user
541
  @type args: list
542
  @param args: should either be an empty list, in which case
543
      we list data for all nodes, or contain a list of nodes
544
      to display data only for those
545
  @rtype: int
546
  @return: the desired exit code
547

548
  """
549
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
550
  if opts.user_storage_type is None:
551
    opts.user_storage_type = constants.ST_LVM_PV
552

    
553
  storage_type = ConvertStorageType(opts.user_storage_type)
554

    
555
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
556

    
557
  op = opcodes.OpQueryNodeStorage(nodes=args,
558
                                  storage_type=storage_type,
559
                                  output_fields=selected_fields)
560
  output = SubmitOpCode(op, opts=opts)
561

    
562
  if not opts.no_headers:
563
    headers = {
564
      constants.SF_NODE: "Node",
565
      constants.SF_TYPE: "Type",
566
      constants.SF_NAME: "Name",
567
      constants.SF_SIZE: "Size",
568
      constants.SF_USED: "Used",
569
      constants.SF_FREE: "Free",
570
      constants.SF_ALLOCATABLE: "Allocatable",
571
      }
572
  else:
573
    headers = None
574

    
575
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
576
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
577

    
578
  # change raw values to nicer strings
579
  for row in output:
580
    for idx, field in enumerate(selected_fields):
581
      val = row[idx]
582
      if field == constants.SF_ALLOCATABLE:
583
        if val:
584
          val = "Y"
585
        else:
586
          val = "N"
587
      row[idx] = str(val)
588

    
589
  data = GenerateTable(separator=opts.separator, headers=headers,
590
                       fields=selected_fields, unitfields=unitfields,
591
                       numfields=numfields, data=output, units=opts.units)
592

    
593
  for line in data:
594
    ToStdout(line)
595

    
596
  return 0
597

    
598

    
599
def ModifyStorage(opts, args):
600
  """Modify storage volume on a node.
601

602
  @param opts: the command line options selected by the user
603
  @type args: list
604
  @param args: should contain 3 items: node name, storage type and volume name
605
  @rtype: int
606
  @return: the desired exit code
607

608
  """
609
  (node_name, user_storage_type, volume_name) = args
610

    
611
  storage_type = ConvertStorageType(user_storage_type)
612

    
613
  changes = {}
614

    
615
  if opts.allocatable is not None:
616
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
617

    
618
  if changes:
619
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
620
                                     storage_type=storage_type,
621
                                     name=volume_name,
622
                                     changes=changes)
623
    SubmitOpCode(op, opts=opts)
624
  else:
625
    ToStderr("No changes to perform, exiting.")
626

    
627

    
628
def RepairStorage(opts, args):
629
  """Repairs a 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
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
643
                                   storage_type=storage_type,
644
                                   name=volume_name,
645
                                   ignore_consistency=opts.ignore_consistency)
646
  SubmitOpCode(op, opts=opts)
647

    
648

    
649
def SetNodeParams(opts, args):
650
  """Modifies a node.
651

652
  @param opts: the command line options selected by the user
653
  @type args: list
654
  @param args: should contain only one element, the node name
655
  @rtype: int
656
  @return: the desired exit code
657

658
  """
659
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
660
                 opts.master_capable, opts.vm_capable, opts.secondary_ip]
661
  if all_changes.count(None) == len(all_changes):
662
    ToStderr("Please give at least one of the parameters.")
663
    return 1
664

    
665
  op = opcodes.OpSetNodeParams(node_name=args[0],
666
                               master_candidate=opts.master_candidate,
667
                               offline=opts.offline,
668
                               drained=opts.drained,
669
                               master_capable=opts.master_capable,
670
                               vm_capable=opts.vm_capable,
671
                               secondary_ip=opts.secondary_ip,
672
                               force=opts.force,
673
                               auto_promote=opts.auto_promote)
674

    
675
  # even if here we process the result, we allow submit only
676
  result = SubmitOrSend(op, opts)
677

    
678
  if result:
679
    ToStdout("Modified node %s", args[0])
680
    for param, data in result:
681
      ToStdout(" - %-5s -> %s", param, data)
682
  return 0
683

    
684

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

    
773

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