Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 9133387e

History | View | Annotate | Download (28.9 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', 'master'],
176
                           use_locking=False)
177
    node_exists, sip, is_master = 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
    if is_master:
188
      ToStderr("Node %s is the master, cannot readd", node)
189
      return 1
190
  else:
191
    if node_exists:
192
      ToStderr("Node %s already in the cluster (as %s)"
193
               " - please retry with '--readd'", node, node_exists)
194
      return 1
195
    sip = opts.secondary_ip
196

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

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

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

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

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

    
220

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

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

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

    
233
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
234
                              (",".join, False))
235

    
236
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
237
                     opts.separator, not opts.no_headers,
238
                     format_override=fmtoverride, verbose=opts.verbose,
239
                     force_filter=opts.force_filter)
240

    
241

    
242
def ListNodeFields(opts, args):
243
  """List node fields.
244

245
  @param opts: the command line options selected by the user
246
  @type args: list
247
  @param args: fields to list, or empty for all
248
  @rtype: int
249
  @return: the desired exit code
250

251
  """
252
  return GenericListFields(constants.QR_NODE, args, opts.separator,
253
                           not opts.no_headers)
254

    
255

    
256
def EvacuateNode(opts, args):
257
  """Relocate all secondary instance from a node.
258

259
  @param opts: the command line options selected by the user
260
  @type args: list
261
  @param args: should be an empty list
262
  @rtype: int
263
  @return: the desired exit code
264

265
  """
266
  cl = GetClient()
267
  force = opts.force
268

    
269
  dst_node = opts.dst_node
270
  iallocator = opts.iallocator
271

    
272
  op = opcodes.OpNodeEvacStrategy(nodes=args,
273
                                  iallocator=iallocator,
274
                                  remote_node=dst_node)
275

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

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

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

    
309

    
310
def FailoverNode(opts, args):
311
  """Failover all primary instance on a node.
312

313
  @param opts: the command line options selected by the user
314
  @type args: list
315
  @param args: should be an empty list
316
  @rtype: int
317
  @return: the desired exit code
318

319
  """
320
  cl = GetClient()
321
  force = opts.force
322
  selected_fields = ["name", "pinst_list"]
323

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

    
330
  if not pinst:
331
    ToStderr("No primary instances on node %s, exiting.", node)
332
    return 0
333

    
334
  pinst = utils.NiceSort(pinst)
335

    
336
  retcode = 0
337

    
338
  if not force and not AskUser("Fail over instance(s) %s?" %
339
                               (",".join("'%s'" % name for name in pinst))):
340
    return 2
341

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

    
357

    
358
def MigrateNode(opts, args):
359
  """Migrate all primary instance on a node.
360

361
  """
362
  cl = GetClient()
363
  force = opts.force
364
  selected_fields = ["name", "pinst_list"]
365

    
366
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
367
  node, pinst = result[0]
368

    
369
  if not pinst:
370
    ToStdout("No primary instances on node %s, exiting." % node)
371
    return 0
372

    
373
  pinst = utils.NiceSort(pinst)
374

    
375
  if not force and not AskUser("Migrate instance(s) %s?" %
376
                               (",".join("'%s'" % name for name in pinst))):
377
    return 2
378

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

    
392

    
393
def ShowNodeConfig(opts, args):
394
  """Show node information.
395

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

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

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

    
444
  return 0
445

    
446

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

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

457
  """
458
  op = opcodes.OpNodeRemove(node_name=args[0])
459
  SubmitOpCode(op, opts=opts)
460
  return 0
461

    
462

    
463
def PowercycleNode(opts, args):
464
  """Remove a node from the cluster.
465

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

473
  """
474
  node = args[0]
475
  if (not opts.confirm and
476
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
477
    return 2
478

    
479
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
480
  result = SubmitOpCode(op, opts=opts)
481
  if result:
482
    ToStderr(result)
483
  return 0
484

    
485

    
486
def PowerNode(opts, args):
487
  """Change/ask power state of a node.
488

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

496
  """
497
  command = args.pop(0)
498

    
499
  if opts.no_headers:
500
    headers = None
501
  else:
502
    headers = {"node": "Node", "status": "Status"}
503

    
504
  if command not in _LIST_POWER_COMMANDS:
505
    ToStderr("power subcommand %s not supported." % command)
506
    return constants.EXIT_FAILURE
507

    
508
  oob_command = "power-%s" % command
509

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

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

    
526
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
527
                                         command=oob_command,
528
                                         ignore_status=opts.ignore_status,
529
                                         timeout=opts.oob_timeout,
530
                                         power_delay=opts.power_delay))
531

    
532
  cli.SetGenericOpcodeOpts(opcodelist, opts)
533

    
534
  job_id = cli.SendJob(opcodelist)
535

    
536
  # We just want the OOB Opcode status
537
  # If it fails PollJob gives us the error message in it
538
  result = cli.PollJob(job_id)[-1]
539

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

    
560
  data = GenerateTable(separator=opts.separator, headers=headers,
561
                       fields=["node", "status"], data=data)
562

    
563
  for line in data:
564
    ToStdout(line)
565

    
566
  if errs:
567
    return constants.EXIT_FAILURE
568
  else:
569
    return constants.EXIT_SUCCESS
570

    
571

    
572
def Health(opts, args):
573
  """Show health of a node using OOB.
574

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

582
  """
583
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
584
                            timeout=opts.oob_timeout)
585
  result = SubmitOpCode(op, opts=opts)
586

    
587
  if opts.no_headers:
588
    headers = None
589
  else:
590
    headers = {"node": "Node", "status": "Status"}
591

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

    
606
  data = GenerateTable(separator=opts.separator, headers=headers,
607
                       fields=["node", "status"], data=data)
608

    
609
  for line in data:
610
    ToStdout(line)
611

    
612
  if errs:
613
    return constants.EXIT_FAILURE
614
  else:
615
    return constants.EXIT_SUCCESS
616

    
617

    
618
def ListVolumes(opts, args):
619
  """List logical volumes on node(s).
620

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

629
  """
630
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
631

    
632
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
633
  output = SubmitOpCode(op, opts=opts)
634

    
635
  if not opts.no_headers:
636
    headers = {"node": "Node", "phys": "PhysDev",
637
               "vg": "VG", "name": "Name",
638
               "size": "Size", "instance": "Instance"}
639
  else:
640
    headers = None
641

    
642
  unitfields = ["size"]
643

    
644
  numfields = ["size"]
645

    
646
  data = GenerateTable(separator=opts.separator, headers=headers,
647
                       fields=selected_fields, unitfields=unitfields,
648
                       numfields=numfields, data=output, units=opts.units)
649

    
650
  for line in data:
651
    ToStdout(line)
652

    
653
  return 0
654

    
655

    
656
def ListStorage(opts, args):
657
  """List physical volumes on node(s).
658

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

667
  """
668
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
669
  if opts.user_storage_type is None:
670
    opts.user_storage_type = constants.ST_LVM_PV
671

    
672
  storage_type = ConvertStorageType(opts.user_storage_type)
673

    
674
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
675

    
676
  op = opcodes.OpNodeQueryStorage(nodes=args,
677
                                  storage_type=storage_type,
678
                                  output_fields=selected_fields)
679
  output = SubmitOpCode(op, opts=opts)
680

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

    
694
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
695
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
696

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

    
708
  data = GenerateTable(separator=opts.separator, headers=headers,
709
                       fields=selected_fields, unitfields=unitfields,
710
                       numfields=numfields, data=output, units=opts.units)
711

    
712
  for line in data:
713
    ToStdout(line)
714

    
715
  return 0
716

    
717

    
718
def ModifyStorage(opts, args):
719
  """Modify storage volume on a node.
720

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

727
  """
728
  (node_name, user_storage_type, volume_name) = args
729

    
730
  storage_type = ConvertStorageType(user_storage_type)
731

    
732
  changes = {}
733

    
734
  if opts.allocatable is not None:
735
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
736

    
737
  if changes:
738
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
739
                                     storage_type=storage_type,
740
                                     name=volume_name,
741
                                     changes=changes)
742
    SubmitOpCode(op, opts=opts)
743
  else:
744
    ToStderr("No changes to perform, exiting.")
745

    
746

    
747
def RepairStorage(opts, args):
748
  """Repairs a storage volume on a node.
749

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

756
  """
757
  (node_name, user_storage_type, volume_name) = args
758

    
759
  storage_type = ConvertStorageType(user_storage_type)
760

    
761
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
762
                                   storage_type=storage_type,
763
                                   name=volume_name,
764
                                   ignore_consistency=opts.ignore_consistency)
765
  SubmitOpCode(op, opts=opts)
766

    
767

    
768
def SetNodeParams(opts, args):
769
  """Modifies a node.
770

771
  @param opts: the command line options selected by the user
772
  @type args: list
773
  @param args: should contain only one element, the node name
774
  @rtype: int
775
  @return: the desired exit code
776

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

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

    
797
  # even if here we process the result, we allow submit only
798
  result = SubmitOrSend(op, opts)
799

    
800
  if result:
801
    ToStdout("Modified node %s", args[0])
802
    for param, data in result:
803
      ToStdout(" - %-5s -> %s", param, data)
804
  return 0
805

    
806

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

    
918

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