Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ c832b6c8

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
FORCE_MASTER_OPT = cli_option("--force-master", default=False,
118
                              action="store_true", dest="force_master",
119
                              help=("Operate on the master node too"
120
                                    " (potentially DANGEROUS)"))
121

    
122

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

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

    
133

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

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

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

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

    
152
  cmd.extend(nodes)
153

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

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

    
161

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

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

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

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

    
185
  if readd:
186
    if not node_exists:
187
      ToStderr("Node %s not in the cluster"
188
               " - please retry without '--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)
239

    
240

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

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

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

    
254

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

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

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

    
268
  dst_node = opts.dst_node
269
  iallocator = opts.iallocator
270

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

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

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

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

    
308

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

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

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

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

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

    
333
  pinst = utils.NiceSort(pinst)
334

    
335
  retcode = 0
336

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

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

    
355

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

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

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

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

    
371
  pinst = utils.NiceSort(pinst)
372

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

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

    
389

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

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

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

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

    
441
  return 0
442

    
443

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

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

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

    
459

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

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

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

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

    
482

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

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

493
  """
494
  client = GetClient()
495
  command = args.pop(0)
496

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

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

    
506
  nodes = [node for (node, ) in client.QueryNodes(args, ["name"], False)]
507
  oob_command = "power-%s" % command
508

    
509
  if oob_command in _OOB_COMMAND_ASK:
510
    if not args and not opts.show_all:
511
      ToStderr("Please provide at least one node or use --all for this command"
512
               " as this is a potentially harmful command")
513
      return constants.EXIT_FAILURE
514
    elif args and opts.show_all:
515
      ToStderr("Please provide either nodes or use --all, can not use both at"
516
               " the same time")
517
      return constants.EXIT_FAILURE
518
    elif not opts.force and not ConfirmOperation(nodes, "nodes",
519
                                                 "power %s" % command):
520
      return constants.EXIT_FAILURE
521

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

    
529
  opcodelist.append(opcodes.OpOobCommand(node_names=nodes,
530
                                         command=oob_command,
531
                                         ignore_status=opts.ignore_status,
532
                                         force_master=opts.force_master))
533

    
534
  cli.SetGenericOpcodeOpts(opcodelist, opts)
535

    
536
  job_id = cli.SendJob(opcodelist)
537

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

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

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

    
565
  for line in data:
566
    ToStdout(line)
567

    
568
  if errs:
569
    return constants.EXIT_FAILURE
570
  else:
571
    return constants.EXIT_SUCCESS
572

    
573

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

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

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

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

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

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

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

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

    
618

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

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

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

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

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

    
643
  unitfields = ["size"]
644

    
645
  numfields = ["size"]
646

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

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

    
654
  return 0
655

    
656

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

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

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

    
673
  storage_type = ConvertStorageType(opts.user_storage_type)
674

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

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

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

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

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

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

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

    
716
  return 0
717

    
718

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

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

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

    
731
  storage_type = ConvertStorageType(user_storage_type)
732

    
733
  changes = {}
734

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

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

    
747

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

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

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

    
760
  storage_type = ConvertStorageType(user_storage_type)
761

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

    
768

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

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

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

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

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

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

    
807

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

    
915

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