Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ f8fa4175

History | View | Annotate | Download (29 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
                             target_node=opts.dst_node)
391
  SubmitOpCode(op, cl=cl, opts=opts)
392

    
393

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

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

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

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

    
445
  return 0
446

    
447

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

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

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

    
463

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

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

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

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

    
486

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

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

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

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

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

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

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

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

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

    
533
  cli.SetGenericOpcodeOpts(opcodelist, opts)
534

    
535
  job_id = cli.SendJob(opcodelist)
536

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

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

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

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

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

    
572

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

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

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

    
919

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