Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 4d32c211

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
  }
81

    
82

    
83
#: headers (and full field list for L{ListStorage}
84
_LIST_STOR_HEADERS = {
85
  constants.SF_NODE: "Node",
86
  constants.SF_TYPE: "Type",
87
  constants.SF_NAME: "Name",
88
  constants.SF_SIZE: "Size",
89
  constants.SF_USED: "Used",
90
  constants.SF_FREE: "Free",
91
  constants.SF_ALLOCATABLE: "Allocatable",
92
  }
93

    
94

    
95
#: User-facing storage unit types
96
_USER_STORAGE_TYPE = {
97
  constants.ST_FILE: "file",
98
  constants.ST_LVM_PV: "lvm-pv",
99
  constants.ST_LVM_VG: "lvm-vg",
100
  }
101

    
102
_STORAGE_TYPE_OPT = \
103
  cli_option("-t", "--storage-type",
104
             dest="user_storage_type",
105
             choices=_USER_STORAGE_TYPE.keys(),
106
             default=None,
107
             metavar="STORAGE_TYPE",
108
             help=("Storage type (%s)" %
109
                   utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
110

    
111
_REPAIRABLE_STORAGE_TYPES = \
112
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
113
   if constants.SO_FIX_CONSISTENCY in so]
114

    
115
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
116

    
117

    
118
NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
119
                              action="store_false", dest="node_setup",
120
                              help=("Do not make initial SSH setup on remote"
121
                                    " node (needs to be done manually)"))
122

    
123

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

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

    
134

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

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

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

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

    
153
  cmd.extend(nodes)
154

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

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

    
162

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

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

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

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

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

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

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

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

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

    
215
  op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
216
                         readd=opts.readd, group=opts.nodegroup,
217
                         vm_capable=opts.vm_capable,
218
                         master_capable=opts.master_capable)
219
  SubmitOpCode(op, opts=opts)
220

    
221

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

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

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

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

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

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

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

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

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

    
275
  return 0
276

    
277

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

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

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

    
291
  dst_node = opts.dst_node
292
  iallocator = opts.iallocator
293

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

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

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

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

    
331

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

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

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

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

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

    
356
  pinst = utils.NiceSort(pinst)
357

    
358
  retcode = 0
359

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

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

    
378

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

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

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

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

    
394
  pinst = utils.NiceSort(pinst)
395

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

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

    
412

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

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

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

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

    
456
  return 0
457

    
458

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

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

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

    
474

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

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

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

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

    
497

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

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

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

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

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

    
522
  unitfields = ["size"]
523

    
524
  numfields = ["size"]
525

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

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

    
533
  return 0
534

    
535

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

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

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

    
552
  storage_type = ConvertStorageType(opts.user_storage_type)
553

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

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

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

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

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

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

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

    
595
  return 0
596

    
597

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

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

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

    
610
  storage_type = ConvertStorageType(user_storage_type)
611

    
612
  changes = {}
613

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

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

    
626

    
627
def RepairStorage(opts, args):
628
  """Repairs a storage volume on a node.
629

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

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

    
639
  storage_type = ConvertStorageType(user_storage_type)
640

    
641
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
642
                                   storage_type=storage_type,
643
                                   name=volume_name,
644
                                   ignore_consistency=opts.ignore_consistency)
645
  SubmitOpCode(op, opts=opts)
646

    
647

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

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

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

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

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

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

    
683

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

    
772

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