Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ efae0fdd

History | View | Annotate | Download (27.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
IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
109
                               action="store_true", dest="ignore_status",
110
                               help=("Ignore the Node(s) offline status"
111
                                     " (potentially DANGEROUS)"))
112

    
113
FORCE_MASTER_OPT = cli_option("--force-master", default=False,
114
                              action="store_true", dest="force_master",
115
                              help=("Operate on the master node too"
116
                                    " (potentially DANGEROUS)"))
117

    
118

    
119
def ConvertStorageType(user_storage_type):
120
  """Converts a user storage type to its internal name.
121

122
  """
123
  try:
124
    return _USER_STORAGE_TYPE[user_storage_type]
125
  except KeyError:
126
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
127
                               errors.ECODE_INVAL)
128

    
129

    
130
def _RunSetupSSH(options, nodes):
131
  """Wrapper around utils.RunCmd to call setup-ssh
132

133
  @param options: The command line options
134
  @param nodes: The nodes to setup
135

136
  """
137
  cmd = [constants.SETUP_SSH]
138

    
139
  # Pass --debug|--verbose to the external script if set on our invocation
140
  # --debug overrides --verbose
141
  if options.debug:
142
    cmd.append("--debug")
143
  elif options.verbose:
144
    cmd.append("--verbose")
145
  if not options.ssh_key_check:
146
    cmd.append("--no-ssh-key-check")
147

    
148
  cmd.extend(nodes)
149

    
150
  result = utils.RunCmd(cmd, interactive=True)
151

    
152
  if result.failed:
153
    errmsg = ("Command '%s' failed with exit code %s; output %r" %
154
              (result.cmd, result.exit_code, result.output))
155
    raise errors.OpExecError(errmsg)
156

    
157

    
158
@UsesRPC
159
def AddNode(opts, args):
160
  """Add a node to the cluster.
161

162
  @param opts: the command line options selected by the user
163
  @type args: list
164
  @param args: should contain only one element, the new node name
165
  @rtype: int
166
  @return: the desired exit code
167

168
  """
169
  cl = GetClient()
170
  node = netutils.GetHostname(name=args[0]).name
171
  readd = opts.readd
172

    
173
  try:
174
    output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
175
                           use_locking=False)
176
    node_exists, sip = output[0]
177
  except (errors.OpPrereqError, errors.OpExecError):
178
    node_exists = ""
179
    sip = None
180

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

    
193
  # read the cluster name from the master
194
  output = cl.QueryConfigValues(['cluster_name'])
195
  cluster_name = output[0]
196

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

    
205
  if opts.node_setup:
206
    _RunSetupSSH(opts, [node])
207

    
208
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
209

    
210
  op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
211
                         readd=opts.readd, group=opts.nodegroup,
212
                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
213
                         master_capable=opts.master_capable)
214
  SubmitOpCode(op, opts=opts)
215

    
216

    
217
def ListNodes(opts, args):
218
  """List nodes and their properties.
219

220
  @param opts: the command line options selected by the user
221
  @type args: list
222
  @param args: nodes to list, or empty for all
223
  @rtype: int
224
  @return: the desired exit code
225

226
  """
227
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
228

    
229
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
230
                              (",".join, False))
231

    
232
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
233
                     opts.separator, not opts.no_headers,
234
                     format_override=fmtoverride)
235

    
236

    
237
def ListNodeFields(opts, args):
238
  """List node fields.
239

240
  @param opts: the command line options selected by the user
241
  @type args: list
242
  @param args: fields to list, or empty for all
243
  @rtype: int
244
  @return: the desired exit code
245

246
  """
247
  return GenericListFields(constants.QR_NODE, args, opts.separator,
248
                           not opts.no_headers)
249

    
250

    
251
def EvacuateNode(opts, args):
252
  """Relocate all secondary instance from a node.
253

254
  @param opts: the command line options selected by the user
255
  @type args: list
256
  @param args: should be an empty list
257
  @rtype: int
258
  @return: the desired exit code
259

260
  """
261
  cl = GetClient()
262
  force = opts.force
263

    
264
  dst_node = opts.dst_node
265
  iallocator = opts.iallocator
266

    
267
  op = opcodes.OpNodeEvacStrategy(nodes=args,
268
                                  iallocator=iallocator,
269
                                  remote_node=dst_node)
270

    
271
  result = SubmitOpCode(op, cl=cl, opts=opts)
272
  if not result:
273
    # no instances to migrate
274
    ToStderr("No secondary instances on node(s) %s, exiting.",
275
             utils.CommaJoin(args))
276
    return constants.EXIT_SUCCESS
277

    
278
  if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
279
                               (",".join("'%s'" % name[0] for name in result),
280
                               utils.CommaJoin(args))):
281
    return constants.EXIT_CONFIRMATION
282

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

    
304

    
305
def FailoverNode(opts, args):
306
  """Failover all primary instance on a node.
307

308
  @param opts: the command line options selected by the user
309
  @type args: list
310
  @param args: should be an empty list
311
  @rtype: int
312
  @return: the desired exit code
313

314
  """
315
  cl = GetClient()
316
  force = opts.force
317
  selected_fields = ["name", "pinst_list"]
318

    
319
  # these fields are static data anyway, so it doesn't matter, but
320
  # locking=True should be safer
321
  result = cl.QueryNodes(names=args, fields=selected_fields,
322
                         use_locking=False)
323
  node, pinst = result[0]
324

    
325
  if not pinst:
326
    ToStderr("No primary instances on node %s, exiting.", node)
327
    return 0
328

    
329
  pinst = utils.NiceSort(pinst)
330

    
331
  retcode = 0
332

    
333
  if not force and not AskUser("Fail over instance(s) %s?" %
334
                               (",".join("'%s'" % name for name in pinst))):
335
    return 2
336

    
337
  jex = JobExecutor(cl=cl, opts=opts)
338
  for iname in pinst:
339
    op = opcodes.OpInstanceFailover(instance_name=iname,
340
                                    ignore_consistency=opts.ignore_consistency)
341
    jex.QueueJob(iname, op)
342
  results = jex.GetResults()
343
  bad_cnt = len([row for row in results if not row[0]])
344
  if bad_cnt == 0:
345
    ToStdout("All %d instance(s) failed over successfully.", len(results))
346
  else:
347
    ToStdout("There were errors during the failover:\n"
348
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
349
  return retcode
350

    
351

    
352
def MigrateNode(opts, args):
353
  """Migrate all primary instance on a node.
354

355
  """
356
  cl = GetClient()
357
  force = opts.force
358
  selected_fields = ["name", "pinst_list"]
359

    
360
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
361
  node, pinst = result[0]
362

    
363
  if not pinst:
364
    ToStdout("No primary instances on node %s, exiting." % node)
365
    return 0
366

    
367
  pinst = utils.NiceSort(pinst)
368

    
369
  if not force and not AskUser("Migrate instance(s) %s?" %
370
                               (",".join("'%s'" % name for name in pinst))):
371
    return 2
372

    
373
  # this should be removed once --non-live is deprecated
374
  if not opts.live and opts.migration_mode is not None:
375
    raise errors.OpPrereqError("Only one of the --non-live and "
376
                               "--migration-mode options can be passed",
377
                               errors.ECODE_INVAL)
378
  if not opts.live: # --non-live passed
379
    mode = constants.HT_MIGRATION_NONLIVE
380
  else:
381
    mode = opts.migration_mode
382
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode)
383
  SubmitOpCode(op, cl=cl, opts=opts)
384

    
385

    
386
def ShowNodeConfig(opts, args):
387
  """Show node information.
388

389
  @param opts: the command line options selected by the user
390
  @type args: list
391
  @param args: should either be an empty list, in which case
392
      we show information about all nodes, or should contain
393
      a list of nodes to be queried for information
394
  @rtype: int
395
  @return: the desired exit code
396

397
  """
398
  cl = GetClient()
399
  result = cl.QueryNodes(fields=["name", "pip", "sip",
400
                                 "pinst_list", "sinst_list",
401
                                 "master_candidate", "drained", "offline",
402
                                 "master_capable", "vm_capable", "powered",
403
                                 "ndparams", "custom_ndparams"],
404
                         names=args, use_locking=False)
405

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

    
437
  return 0
438

    
439

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

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

450
  """
451
  op = opcodes.OpNodeRemove(node_name=args[0])
452
  SubmitOpCode(op, opts=opts)
453
  return 0
454

    
455

    
456
def PowercycleNode(opts, args):
457
  """Remove a node from the cluster.
458

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

466
  """
467
  node = args[0]
468
  if (not opts.confirm and
469
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
470
    return 2
471

    
472
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
473
  result = SubmitOpCode(op, opts=opts)
474
  if result:
475
    ToStderr(result)
476
  return 0
477

    
478

    
479
def PowerNode(opts, args):
480
  """Change/ask power state of a node.
481

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

489
  """
490
  command = args[0]
491
  node = args[1]
492

    
493
  if command not in _LIST_POWER_COMMANDS:
494
    ToStderr("power subcommand %s not supported." % command)
495
    return constants.EXIT_FAILURE
496

    
497
  oob_command = "power-%s" % command
498

    
499
  opcodelist = []
500
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
501
    opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
502
                                              auto_promote=opts.auto_promote))
503

    
504
  opcodelist.append(opcodes.OpOobCommand(node_names=[node],
505
                                         command=oob_command,
506
                                         ignore_status=opts.ignore_status,
507
                                         force_master=opts.force_master))
508

    
509
  cli.SetGenericOpcodeOpts(opcodelist, opts)
510

    
511
  job_id = cli.SendJob(opcodelist)
512

    
513
  # We just want the OOB Opcode status
514
  # If it fails PollJob gives us the error message in it
515
  result = cli.PollJob(job_id)[-1]
516

    
517
  if result:
518
    (_, data_tuple) = result[0]
519
    if data_tuple[0] != constants.RS_NORMAL:
520
      if data_tuple[0] == constants.RS_UNAVAIL:
521
        result = "OOB is not supported"
522
      else:
523
        result = "RPC failed, look out for warning in the output"
524
      ToStderr(result)
525
      return constants.EXIT_FAILURE
526
    else:
527
      if oob_command == constants.OOB_POWER_STATUS:
528
        text = "The machine is %spowered"
529
        if data_tuple[1][constants.OOB_POWER_STATUS_POWERED]:
530
          result = text % ""
531
        else:
532
          result = text % "not "
533
        ToStdout(result)
534

    
535
  return constants.EXIT_SUCCESS
536

    
537

    
538
def Health(opts, args):
539
  """Show health of a node using OOB.
540

541
  @param opts: the command line options selected by the user
542
  @type args: list
543
  @param args: should contain only one element, the name of
544
      the node to be removed
545
  @rtype: int
546
  @return: the desired exit code
547

548
  """
549
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH)
550
  result = SubmitOpCode(op, opts=opts)
551

    
552
  if opts.no_headers:
553
    headers = None
554
  else:
555
    headers = {"node": "Node", "status": "Status"}
556

    
557
  errs = 0
558
  data = []
559
  for node_result in result:
560
    (node_tuple, data_tuple) = node_result
561
    (_, node_name) = node_tuple
562
    (data_status, data_node) = data_tuple
563
    if data_status == constants.RS_NORMAL:
564
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
565
      for item, status in data_node[1:]:
566
        data.append(["", "%s=%s" % (item, status)])
567
    else:
568
      errs += 1
569
      data.append([node_name, cli.FormatResultError(data_status)])
570

    
571
  data = GenerateTable(separator=opts.separator, headers=headers,
572
                       fields=["node", "status"], data=data)
573

    
574
  for line in data:
575
    ToStdout(line)
576

    
577
  if errs:
578
    return constants.EXIT_FAILURE
579
  else:
580
    return constants.EXIT_SUCCESS
581

    
582

    
583
def ListVolumes(opts, args):
584
  """List logical volumes on node(s).
585

586
  @param opts: the command line options selected by the user
587
  @type args: list
588
  @param args: should either be an empty list, in which case
589
      we list data for all nodes, or contain a list of nodes
590
      to display data only for those
591
  @rtype: int
592
  @return: the desired exit code
593

594
  """
595
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
596

    
597
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
598
  output = SubmitOpCode(op, opts=opts)
599

    
600
  if not opts.no_headers:
601
    headers = {"node": "Node", "phys": "PhysDev",
602
               "vg": "VG", "name": "Name",
603
               "size": "Size", "instance": "Instance"}
604
  else:
605
    headers = None
606

    
607
  unitfields = ["size"]
608

    
609
  numfields = ["size"]
610

    
611
  data = GenerateTable(separator=opts.separator, headers=headers,
612
                       fields=selected_fields, unitfields=unitfields,
613
                       numfields=numfields, data=output, units=opts.units)
614

    
615
  for line in data:
616
    ToStdout(line)
617

    
618
  return 0
619

    
620

    
621
def ListStorage(opts, args):
622
  """List physical volumes on node(s).
623

624
  @param opts: the command line options selected by the user
625
  @type args: list
626
  @param args: should either be an empty list, in which case
627
      we list data for all nodes, or contain a list of nodes
628
      to display data only for those
629
  @rtype: int
630
  @return: the desired exit code
631

632
  """
633
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
634
  if opts.user_storage_type is None:
635
    opts.user_storage_type = constants.ST_LVM_PV
636

    
637
  storage_type = ConvertStorageType(opts.user_storage_type)
638

    
639
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
640

    
641
  op = opcodes.OpNodeQueryStorage(nodes=args,
642
                                  storage_type=storage_type,
643
                                  output_fields=selected_fields)
644
  output = SubmitOpCode(op, opts=opts)
645

    
646
  if not opts.no_headers:
647
    headers = {
648
      constants.SF_NODE: "Node",
649
      constants.SF_TYPE: "Type",
650
      constants.SF_NAME: "Name",
651
      constants.SF_SIZE: "Size",
652
      constants.SF_USED: "Used",
653
      constants.SF_FREE: "Free",
654
      constants.SF_ALLOCATABLE: "Allocatable",
655
      }
656
  else:
657
    headers = None
658

    
659
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
660
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
661

    
662
  # change raw values to nicer strings
663
  for row in output:
664
    for idx, field in enumerate(selected_fields):
665
      val = row[idx]
666
      if field == constants.SF_ALLOCATABLE:
667
        if val:
668
          val = "Y"
669
        else:
670
          val = "N"
671
      row[idx] = str(val)
672

    
673
  data = GenerateTable(separator=opts.separator, headers=headers,
674
                       fields=selected_fields, unitfields=unitfields,
675
                       numfields=numfields, data=output, units=opts.units)
676

    
677
  for line in data:
678
    ToStdout(line)
679

    
680
  return 0
681

    
682

    
683
def ModifyStorage(opts, args):
684
  """Modify storage volume on a node.
685

686
  @param opts: the command line options selected by the user
687
  @type args: list
688
  @param args: should contain 3 items: node name, storage type and volume name
689
  @rtype: int
690
  @return: the desired exit code
691

692
  """
693
  (node_name, user_storage_type, volume_name) = args
694

    
695
  storage_type = ConvertStorageType(user_storage_type)
696

    
697
  changes = {}
698

    
699
  if opts.allocatable is not None:
700
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
701

    
702
  if changes:
703
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
704
                                     storage_type=storage_type,
705
                                     name=volume_name,
706
                                     changes=changes)
707
    SubmitOpCode(op, opts=opts)
708
  else:
709
    ToStderr("No changes to perform, exiting.")
710

    
711

    
712
def RepairStorage(opts, args):
713
  """Repairs a storage volume on a node.
714

715
  @param opts: the command line options selected by the user
716
  @type args: list
717
  @param args: should contain 3 items: node name, storage type and volume name
718
  @rtype: int
719
  @return: the desired exit code
720

721
  """
722
  (node_name, user_storage_type, volume_name) = args
723

    
724
  storage_type = ConvertStorageType(user_storage_type)
725

    
726
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
727
                                   storage_type=storage_type,
728
                                   name=volume_name,
729
                                   ignore_consistency=opts.ignore_consistency)
730
  SubmitOpCode(op, opts=opts)
731

    
732

    
733
def SetNodeParams(opts, args):
734
  """Modifies a node.
735

736
  @param opts: the command line options selected by the user
737
  @type args: list
738
  @param args: should contain only one element, the node name
739
  @rtype: int
740
  @return: the desired exit code
741

742
  """
743
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
744
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
745
                 opts.ndparams]
746
  if all_changes.count(None) == len(all_changes):
747
    ToStderr("Please give at least one of the parameters.")
748
    return 1
749

    
750
  op = opcodes.OpNodeSetParams(node_name=args[0],
751
                               master_candidate=opts.master_candidate,
752
                               offline=opts.offline,
753
                               drained=opts.drained,
754
                               master_capable=opts.master_capable,
755
                               vm_capable=opts.vm_capable,
756
                               secondary_ip=opts.secondary_ip,
757
                               force=opts.force,
758
                               ndparams=opts.ndparams,
759
                               auto_promote=opts.auto_promote,
760
                               powered=opts.node_powered)
761

    
762
  # even if here we process the result, we allow submit only
763
  result = SubmitOrSend(op, opts)
764

    
765
  if result:
766
    ToStdout("Modified node %s", args[0])
767
    for param, data in result:
768
      ToStdout(" - %-5s -> %s", param, data)
769
  return 0
770

    
771

    
772
commands = {
773
  'add': (
774
    AddNode, [ArgHost(min=1, max=1)],
775
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
776
     VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
777
     CAPAB_VM_OPT, NODE_PARAMS_OPT],
778
    "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup]  [--verbose] "
779
    " <node_name>",
780
    "Add a node to the cluster"),
781
  'evacuate': (
782
    EvacuateNode, [ArgNode(min=1)],
783
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
784
     PRIORITY_OPT],
785
    "[-f] {-I <iallocator> | -n <dst>} <node>",
786
    "Relocate the secondary instances from a node"
787
    " to other nodes (only for instances with drbd disk template)"),
788
  'failover': (
789
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
790
    "[-f] <node>",
791
    "Stops the primary instances on a node and start them on their"
792
    " secondary node (only for instances with drbd disk template)"),
793
  'migrate': (
794
    MigrateNode, ARGS_ONE_NODE,
795
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
796
    "[-f] <node>",
797
    "Migrate all the primary instance on a node away from it"
798
    " (only for instances of type drbd)"),
799
  'info': (
800
    ShowNodeConfig, ARGS_MANY_NODES, [],
801
    "[<node_name>...]", "Show information about the node(s)"),
802
  'list': (
803
    ListNodes, ARGS_MANY_NODES,
804
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
805
    "[nodes...]",
806
    "Lists the nodes in the cluster. The available fields can be shown using"
807
    " the \"list-fields\" command (see the man page for details)."
808
    " The default field list is (in order): %s." %
809
    utils.CommaJoin(_LIST_DEF_FIELDS)),
810
  "list-fields": (
811
    ListNodeFields, [ArgUnknown()],
812
    [NOHDR_OPT, SEP_OPT],
813
    "[fields...]",
814
    "Lists all available fields for nodes"),
815
  'modify': (
816
    SetNodeParams, ARGS_ONE_NODE,
817
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
818
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
819
     AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
820
     NODE_POWERED_OPT],
821
    "<node_name>", "Alters the parameters of a node"),
822
  'powercycle': (
823
    PowercycleNode, ARGS_ONE_NODE,
824
    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
825
    "<node_name>", "Tries to forcefully powercycle a node"),
826
  'power': (
827
    PowerNode,
828
    [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
829
     ArgNode(min=1, max=1)],
830
    [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
831
     FORCE_MASTER_OPT],
832
    "on|off|cycle|status <node>",
833
    "Change power state of node by calling out-of-band helper."),
834
  'remove': (
835
    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
836
    "<node_name>", "Removes a node from the cluster"),
837
  'volumes': (
838
    ListVolumes, [ArgNode()],
839
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
840
    "[<node_name>...]", "List logical volumes on node(s)"),
841
  'list-storage': (
842
    ListStorage, ARGS_MANY_NODES,
843
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
844
     PRIORITY_OPT],
845
    "[<node_name>...]", "List physical volumes on node(s). The available"
846
    " fields are (see the man page for details): %s." %
847
    (utils.CommaJoin(_LIST_STOR_HEADERS))),
848
  'modify-storage': (
849
    ModifyStorage,
850
    [ArgNode(min=1, max=1),
851
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
852
     ArgFile(min=1, max=1)],
853
    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
854
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
855
  'repair-storage': (
856
    RepairStorage,
857
    [ArgNode(min=1, max=1),
858
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
859
     ArgFile(min=1, max=1)],
860
    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
861
    "<node_name> <storage_type> <name>",
862
    "Repairs a storage volume on a node"),
863
  'list-tags': (
864
    ListTags, ARGS_ONE_NODE, [],
865
    "<node_name>", "List the tags of the given node"),
866
  'add-tags': (
867
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
868
    "<node_name> tag...", "Add tags to the given node"),
869
  'remove-tags': (
870
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
871
    [TAG_SRC_OPT, PRIORITY_OPT],
872
    "<node_name> tag...", "Remove tags from the given node"),
873
  "health": (
874
    Health, ARGS_MANY_NODES,
875
    [NOHDR_OPT, SEP_OPT, SUBMIT_OPT, PRIORITY_OPT],
876
    "[<node_name>...]", "List health of node(s) using out-of-band"),
877
  }
878

    
879

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