Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 0a1fc31c

History | View | Annotate | Download (28.7 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
_OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
104
                              constants.OOB_POWER_CYCLE])
105

    
106

    
107
NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
108
                              action="store_false", dest="node_setup",
109
                              help=("Do not make initial SSH setup on remote"
110
                                    " node (needs to be done manually)"))
111

    
112
IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
113
                               action="store_true", dest="ignore_status",
114
                               help=("Ignore the Node(s) offline status"
115
                                     " (potentially DANGEROUS)"))
116

    
117

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

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

    
128

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

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

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

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

    
149
  cmd.extend(nodes)
150

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

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

    
158

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

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

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

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

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

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

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

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

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

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

    
217

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

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

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

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

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

    
237

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

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

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

    
251

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

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

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

    
265
  dst_node = opts.dst_node
266
  iallocator = opts.iallocator
267

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

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

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

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

    
305

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

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

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

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

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

    
330
  pinst = utils.NiceSort(pinst)
331

    
332
  retcode = 0
333

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

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

    
353

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

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

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

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

    
369
  pinst = utils.NiceSort(pinst)
370

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

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

    
388

    
389
def ShowNodeConfig(opts, args):
390
  """Show node information.
391

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

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

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

    
440
  return 0
441

    
442

    
443
def RemoveNode(opts, args):
444
  """Remove a node from the cluster.
445

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

453
  """
454
  op = opcodes.OpNodeRemove(node_name=args[0])
455
  SubmitOpCode(op, opts=opts)
456
  return 0
457

    
458

    
459
def PowercycleNode(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
  node = args[0]
471
  if (not opts.confirm and
472
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
473
    return 2
474

    
475
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
476
  result = SubmitOpCode(op, opts=opts)
477
  if result:
478
    ToStderr(result)
479
  return 0
480

    
481

    
482
def PowerNode(opts, args):
483
  """Change/ask power state of a node.
484

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

492
  """
493
  command = args.pop(0)
494

    
495
  if opts.no_headers:
496
    headers = None
497
  else:
498
    headers = {"node": "Node", "status": "Status"}
499

    
500
  if command not in _LIST_POWER_COMMANDS:
501
    ToStderr("power subcommand %s not supported." % command)
502
    return constants.EXIT_FAILURE
503

    
504
  oob_command = "power-%s" % command
505

    
506
  if oob_command in _OOB_COMMAND_ASK:
507
    if not args:
508
      ToStderr("Please provide at least one node for this command")
509
      return constants.EXIT_FAILURE
510
    elif not opts.force and not ConfirmOperation(args, "nodes",
511
                                                 "power %s" % command):
512
      return constants.EXIT_FAILURE
513
    assert len(args) > 0
514

    
515
  opcodelist = []
516
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
517
    # TODO: This is a little ugly as we can't catch and revert
518
    for node in args:
519
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
520
                                                auto_promote=opts.auto_promote))
521

    
522
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
523
                                         command=oob_command,
524
                                         ignore_status=opts.ignore_status,
525
                                         timeout=opts.oob_timeout,
526
                                         power_delay=opts.power_delay))
527

    
528
  cli.SetGenericOpcodeOpts(opcodelist, opts)
529

    
530
  job_id = cli.SendJob(opcodelist)
531

    
532
  # We just want the OOB Opcode status
533
  # If it fails PollJob gives us the error message in it
534
  result = cli.PollJob(job_id)[-1]
535

    
536
  errs = 0
537
  data = []
538
  for node_result in result:
539
    (node_tuple, data_tuple) = node_result
540
    (_, node_name) = node_tuple
541
    (data_status, data_node) = data_tuple
542
    if data_status == constants.RS_NORMAL:
543
      if oob_command == constants.OOB_POWER_STATUS:
544
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
545
          text = "powered"
546
        else:
547
          text = "unpowered"
548
        data.append([node_name, text])
549
      else:
550
        # We don't expect data here, so we just say, it was successfully invoked
551
        data.append([node_name, "invoked"])
552
    else:
553
      errs += 1
554
      data.append([node_name, cli.FormatResultError(data_status, True)])
555

    
556
  data = GenerateTable(separator=opts.separator, headers=headers,
557
                       fields=["node", "status"], data=data)
558

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

    
562
  if errs:
563
    return constants.EXIT_FAILURE
564
  else:
565
    return constants.EXIT_SUCCESS
566

    
567

    
568
def Health(opts, args):
569
  """Show health of a node using OOB.
570

571
  @param opts: the command line options selected by the user
572
  @type args: list
573
  @param args: should contain only one element, the name of
574
      the node to be removed
575
  @rtype: int
576
  @return: the desired exit code
577

578
  """
579
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
580
                            timeout=opts.oob_timeout)
581
  result = SubmitOpCode(op, opts=opts)
582

    
583
  if opts.no_headers:
584
    headers = None
585
  else:
586
    headers = {"node": "Node", "status": "Status"}
587

    
588
  errs = 0
589
  data = []
590
  for node_result in result:
591
    (node_tuple, data_tuple) = node_result
592
    (_, node_name) = node_tuple
593
    (data_status, data_node) = data_tuple
594
    if data_status == constants.RS_NORMAL:
595
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
596
      for item, status in data_node[1:]:
597
        data.append(["", "%s=%s" % (item, status)])
598
    else:
599
      errs += 1
600
      data.append([node_name, cli.FormatResultError(data_status, True)])
601

    
602
  data = GenerateTable(separator=opts.separator, headers=headers,
603
                       fields=["node", "status"], data=data)
604

    
605
  for line in data:
606
    ToStdout(line)
607

    
608
  if errs:
609
    return constants.EXIT_FAILURE
610
  else:
611
    return constants.EXIT_SUCCESS
612

    
613

    
614
def ListVolumes(opts, args):
615
  """List logical volumes on node(s).
616

617
  @param opts: the command line options selected by the user
618
  @type args: list
619
  @param args: should either be an empty list, in which case
620
      we list data for all nodes, or contain a list of nodes
621
      to display data only for those
622
  @rtype: int
623
  @return: the desired exit code
624

625
  """
626
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
627

    
628
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
629
  output = SubmitOpCode(op, opts=opts)
630

    
631
  if not opts.no_headers:
632
    headers = {"node": "Node", "phys": "PhysDev",
633
               "vg": "VG", "name": "Name",
634
               "size": "Size", "instance": "Instance"}
635
  else:
636
    headers = None
637

    
638
  unitfields = ["size"]
639

    
640
  numfields = ["size"]
641

    
642
  data = GenerateTable(separator=opts.separator, headers=headers,
643
                       fields=selected_fields, unitfields=unitfields,
644
                       numfields=numfields, data=output, units=opts.units)
645

    
646
  for line in data:
647
    ToStdout(line)
648

    
649
  return 0
650

    
651

    
652
def ListStorage(opts, args):
653
  """List physical volumes on node(s).
654

655
  @param opts: the command line options selected by the user
656
  @type args: list
657
  @param args: should either be an empty list, in which case
658
      we list data for all nodes, or contain a list of nodes
659
      to display data only for those
660
  @rtype: int
661
  @return: the desired exit code
662

663
  """
664
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
665
  if opts.user_storage_type is None:
666
    opts.user_storage_type = constants.ST_LVM_PV
667

    
668
  storage_type = ConvertStorageType(opts.user_storage_type)
669

    
670
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
671

    
672
  op = opcodes.OpNodeQueryStorage(nodes=args,
673
                                  storage_type=storage_type,
674
                                  output_fields=selected_fields)
675
  output = SubmitOpCode(op, opts=opts)
676

    
677
  if not opts.no_headers:
678
    headers = {
679
      constants.SF_NODE: "Node",
680
      constants.SF_TYPE: "Type",
681
      constants.SF_NAME: "Name",
682
      constants.SF_SIZE: "Size",
683
      constants.SF_USED: "Used",
684
      constants.SF_FREE: "Free",
685
      constants.SF_ALLOCATABLE: "Allocatable",
686
      }
687
  else:
688
    headers = None
689

    
690
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
691
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
692

    
693
  # change raw values to nicer strings
694
  for row in output:
695
    for idx, field in enumerate(selected_fields):
696
      val = row[idx]
697
      if field == constants.SF_ALLOCATABLE:
698
        if val:
699
          val = "Y"
700
        else:
701
          val = "N"
702
      row[idx] = str(val)
703

    
704
  data = GenerateTable(separator=opts.separator, headers=headers,
705
                       fields=selected_fields, unitfields=unitfields,
706
                       numfields=numfields, data=output, units=opts.units)
707

    
708
  for line in data:
709
    ToStdout(line)
710

    
711
  return 0
712

    
713

    
714
def ModifyStorage(opts, args):
715
  """Modify storage volume on a node.
716

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

723
  """
724
  (node_name, user_storage_type, volume_name) = args
725

    
726
  storage_type = ConvertStorageType(user_storage_type)
727

    
728
  changes = {}
729

    
730
  if opts.allocatable is not None:
731
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
732

    
733
  if changes:
734
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
735
                                     storage_type=storage_type,
736
                                     name=volume_name,
737
                                     changes=changes)
738
    SubmitOpCode(op, opts=opts)
739
  else:
740
    ToStderr("No changes to perform, exiting.")
741

    
742

    
743
def RepairStorage(opts, args):
744
  """Repairs a storage volume on a node.
745

746
  @param opts: the command line options selected by the user
747
  @type args: list
748
  @param args: should contain 3 items: node name, storage type and volume name
749
  @rtype: int
750
  @return: the desired exit code
751

752
  """
753
  (node_name, user_storage_type, volume_name) = args
754

    
755
  storage_type = ConvertStorageType(user_storage_type)
756

    
757
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
758
                                   storage_type=storage_type,
759
                                   name=volume_name,
760
                                   ignore_consistency=opts.ignore_consistency)
761
  SubmitOpCode(op, opts=opts)
762

    
763

    
764
def SetNodeParams(opts, args):
765
  """Modifies a node.
766

767
  @param opts: the command line options selected by the user
768
  @type args: list
769
  @param args: should contain only one element, the node name
770
  @rtype: int
771
  @return: the desired exit code
772

773
  """
774
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
775
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
776
                 opts.ndparams]
777
  if all_changes.count(None) == len(all_changes):
778
    ToStderr("Please give at least one of the parameters.")
779
    return 1
780

    
781
  op = opcodes.OpNodeSetParams(node_name=args[0],
782
                               master_candidate=opts.master_candidate,
783
                               offline=opts.offline,
784
                               drained=opts.drained,
785
                               master_capable=opts.master_capable,
786
                               vm_capable=opts.vm_capable,
787
                               secondary_ip=opts.secondary_ip,
788
                               force=opts.force,
789
                               ndparams=opts.ndparams,
790
                               auto_promote=opts.auto_promote,
791
                               powered=opts.node_powered)
792

    
793
  # even if here we process the result, we allow submit only
794
  result = SubmitOrSend(op, opts)
795

    
796
  if result:
797
    ToStdout("Modified node %s", args[0])
798
    for param, data in result:
799
      ToStdout(" - %-5s -> %s", param, data)
800
  return 0
801

    
802

    
803
commands = {
804
  'add': (
805
    AddNode, [ArgHost(min=1, max=1)],
806
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
807
     NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
808
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT],
809
    "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
810
    " [--no-node-setup] [--verbose]"
811
    " <node_name>",
812
    "Add a node to the cluster"),
813
  'evacuate': (
814
    EvacuateNode, [ArgNode(min=1)],
815
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
816
     PRIORITY_OPT],
817
    "[-f] {-I <iallocator> | -n <dst>} <node>",
818
    "Relocate the secondary instances from a node"
819
    " to other nodes (only for instances with drbd disk template)"),
820
  'failover': (
821
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
822
                                  IALLOCATOR_OPT, PRIORITY_OPT],
823
    "[-f] <node>",
824
    "Stops the primary instances on a node and start them on their"
825
    " secondary node (only for instances with drbd disk template)"),
826
  'migrate': (
827
    MigrateNode, ARGS_ONE_NODE,
828
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT,
829
     IALLOCATOR_OPT, PRIORITY_OPT],
830
    "[-f] <node>",
831
    "Migrate all the primary instance on a node away from it"
832
    " (only for instances of type drbd)"),
833
  'info': (
834
    ShowNodeConfig, ARGS_MANY_NODES, [],
835
    "[<node_name>...]", "Show information about the node(s)"),
836
  'list': (
837
    ListNodes, ARGS_MANY_NODES,
838
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT],
839
    "[nodes...]",
840
    "Lists the nodes in the cluster. The available fields can be shown using"
841
    " the \"list-fields\" command (see the man page for details)."
842
    " The default field list is (in order): %s." %
843
    utils.CommaJoin(_LIST_DEF_FIELDS)),
844
  "list-fields": (
845
    ListNodeFields, [ArgUnknown()],
846
    [NOHDR_OPT, SEP_OPT],
847
    "[fields...]",
848
    "Lists all available fields for nodes"),
849
  'modify': (
850
    SetNodeParams, ARGS_ONE_NODE,
851
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
852
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
853
     AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
854
     NODE_POWERED_OPT],
855
    "<node_name>", "Alters the parameters of a node"),
856
  'powercycle': (
857
    PowercycleNode, ARGS_ONE_NODE,
858
    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
859
    "<node_name>", "Tries to forcefully powercycle a node"),
860
  'power': (
861
    PowerNode,
862
    [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
863
     ArgNode()],
864
    [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
865
     FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
866
    "on|off|cycle|status [nodes...]",
867
    "Change power state of node by calling out-of-band helper."),
868
  'remove': (
869
    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
870
    "<node_name>", "Removes a node from the cluster"),
871
  'volumes': (
872
    ListVolumes, [ArgNode()],
873
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
874
    "[<node_name>...]", "List logical volumes on node(s)"),
875
  'list-storage': (
876
    ListStorage, ARGS_MANY_NODES,
877
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
878
     PRIORITY_OPT],
879
    "[<node_name>...]", "List physical volumes on node(s). The available"
880
    " fields are (see the man page for details): %s." %
881
    (utils.CommaJoin(_LIST_STOR_HEADERS))),
882
  'modify-storage': (
883
    ModifyStorage,
884
    [ArgNode(min=1, max=1),
885
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
886
     ArgFile(min=1, max=1)],
887
    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
888
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
889
  'repair-storage': (
890
    RepairStorage,
891
    [ArgNode(min=1, max=1),
892
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
893
     ArgFile(min=1, max=1)],
894
    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
895
    "<node_name> <storage_type> <name>",
896
    "Repairs a storage volume on a node"),
897
  'list-tags': (
898
    ListTags, ARGS_ONE_NODE, [],
899
    "<node_name>", "List the tags of the given node"),
900
  'add-tags': (
901
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
902
    "<node_name> tag...", "Add tags to the given node"),
903
  'remove-tags': (
904
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
905
    [TAG_SRC_OPT, PRIORITY_OPT],
906
    "<node_name> tag...", "Remove tags from the given node"),
907
  "health": (
908
    Health, ARGS_MANY_NODES,
909
    [NOHDR_OPT, SEP_OPT, SUBMIT_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
910
    "[<node_name>...]", "List health of node(s) using out-of-band"),
911
  }
912

    
913

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