Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ d0c8c01d

History | View | Annotate | Download (30.1 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
import itertools
30

    
31
from ganeti.cli import *
32
from ganeti import cli
33
from ganeti import bootstrap
34
from ganeti import opcodes
35
from ganeti import utils
36
from ganeti import constants
37
from ganeti import errors
38
from ganeti import netutils
39
from cStringIO import StringIO
40

    
41

    
42
#: default list of field for L{ListNodes}
43
_LIST_DEF_FIELDS = [
44
  "name", "dtotal", "dfree",
45
  "mtotal", "mnode", "mfree",
46
  "pinst_cnt", "sinst_cnt",
47
  ]
48

    
49

    
50
#: Default field list for L{ListVolumes}
51
_LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
52

    
53

    
54
#: default list of field for L{ListStorage}
55
_LIST_STOR_DEF_FIELDS = [
56
  constants.SF_NODE,
57
  constants.SF_TYPE,
58
  constants.SF_NAME,
59
  constants.SF_SIZE,
60
  constants.SF_USED,
61
  constants.SF_FREE,
62
  constants.SF_ALLOCATABLE,
63
  ]
64

    
65

    
66
#: default list of power commands
67
_LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
68

    
69

    
70
#: headers (and full field list) for L{ListStorage}
71
_LIST_STOR_HEADERS = {
72
  constants.SF_NODE: "Node",
73
  constants.SF_TYPE: "Type",
74
  constants.SF_NAME: "Name",
75
  constants.SF_SIZE: "Size",
76
  constants.SF_USED: "Used",
77
  constants.SF_FREE: "Free",
78
  constants.SF_ALLOCATABLE: "Allocatable",
79
  }
80

    
81

    
82
#: User-facing storage unit types
83
_USER_STORAGE_TYPE = {
84
  constants.ST_FILE: "file",
85
  constants.ST_LVM_PV: "lvm-pv",
86
  constants.ST_LVM_VG: "lvm-vg",
87
  }
88

    
89
_STORAGE_TYPE_OPT = \
90
  cli_option("-t", "--storage-type",
91
             dest="user_storage_type",
92
             choices=_USER_STORAGE_TYPE.keys(),
93
             default=None,
94
             metavar="STORAGE_TYPE",
95
             help=("Storage type (%s)" %
96
                   utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
97

    
98
_REPAIRABLE_STORAGE_TYPES = \
99
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
100
   if constants.SO_FIX_CONSISTENCY in so]
101

    
102
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
103

    
104

    
105
_OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
106
                              constants.OOB_POWER_CYCLE])
107

    
108

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

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

    
119

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

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

    
130

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

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

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

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

    
151
  cmd.extend(nodes)
152

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

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

    
160

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

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

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

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

    
184
  if readd:
185
    if not node_exists:
186
      ToStderr("Node %s not in the cluster"
187
               " - please retry without '--readd'", node)
188
      return 1
189
    if is_master:
190
      ToStderr("Node %s is the master, cannot readd", node)
191
      return 1
192
  else:
193
    if node_exists:
194
      ToStderr("Node %s already in the cluster (as %s)"
195
               " - please retry with '--readd'", node, node_exists)
196
      return 1
197
    sip = opts.secondary_ip
198

    
199
  # read the cluster name from the master
200
  output = cl.QueryConfigValues(["cluster_name"])
201
  cluster_name = output[0]
202

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

    
211
  if opts.node_setup:
212
    _RunSetupSSH(opts, [node])
213

    
214
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
215

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

    
222

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

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

232
  """
233
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
234

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

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

    
243

    
244
def ListNodeFields(opts, args):
245
  """List node fields.
246

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

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

    
257

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

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

267
  """
268
  if opts.dst_node is not None:
269
    ToStderr("New secondary node given (disabling iallocator), hence evacuating"
270
             " secondary instances only.")
271
    opts.secondary_only = True
272
    opts.primary_only = False
273

    
274
  if opts.secondary_only and opts.primary_only:
275
    raise errors.OpPrereqError("Only one of the --primary-only and"
276
                               " --secondary-only options can be passed",
277
                               errors.ECODE_INVAL)
278
  elif opts.primary_only:
279
    mode = constants.IALLOCATOR_NEVAC_PRI
280
  elif opts.secondary_only:
281
    mode = constants.IALLOCATOR_NEVAC_SEC
282
  else:
283
    mode = constants.IALLOCATOR_NEVAC_ALL
284

    
285
  # Determine affected instances
286
  fields = []
287

    
288
  if not opts.secondary_only:
289
    fields.append("pinst_list")
290
  if not opts.primary_only:
291
    fields.append("sinst_list")
292

    
293
  cl = GetClient()
294

    
295
  result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
296
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
297

    
298
  if not instances:
299
    # No instances to evacuate
300
    ToStderr("No instances to evacuate on node(s) %s, exiting.",
301
             utils.CommaJoin(args))
302
    return constants.EXIT_SUCCESS
303

    
304
  if not (opts.force or
305
          AskUser("Relocate instance(s) %s from node(s) %s?" %
306
                  (utils.CommaJoin(utils.NiceSort(instances)),
307
                   utils.CommaJoin(args)))):
308
    return constants.EXIT_CONFIRMATION
309

    
310
  # Evacuate node
311
  op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
312
                              remote_node=opts.dst_node,
313
                              iallocator=opts.iallocator,
314
                              early_release=opts.early_release)
315
  result = SubmitOpCode(op, cl=cl, opts=opts)
316

    
317
  # Keep track of submitted jobs
318
  jex = JobExecutor(cl=cl, opts=opts)
319

    
320
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
321
    jex.AddJobId(None, status, job_id)
322

    
323
  results = jex.GetResults()
324
  bad_cnt = len([row for row in results if not row[0]])
325
  if bad_cnt == 0:
326
    ToStdout("All instances evacuated successfully.")
327
    rcode = constants.EXIT_SUCCESS
328
  else:
329
    ToStdout("There were %s errors during the evacuation.", bad_cnt)
330
    rcode = constants.EXIT_FAILURE
331

    
332
  return rcode
333

    
334

    
335
def FailoverNode(opts, args):
336
  """Failover all primary instance on a node.
337

338
  @param opts: the command line options selected by the user
339
  @type args: list
340
  @param args: should be an empty list
341
  @rtype: int
342
  @return: the desired exit code
343

344
  """
345
  cl = GetClient()
346
  force = opts.force
347
  selected_fields = ["name", "pinst_list"]
348

    
349
  # these fields are static data anyway, so it doesn't matter, but
350
  # locking=True should be safer
351
  result = cl.QueryNodes(names=args, fields=selected_fields,
352
                         use_locking=False)
353
  node, pinst = result[0]
354

    
355
  if not pinst:
356
    ToStderr("No primary instances on node %s, exiting.", node)
357
    return 0
358

    
359
  pinst = utils.NiceSort(pinst)
360

    
361
  retcode = 0
362

    
363
  if not force and not AskUser("Fail over instance(s) %s?" %
364
                               (",".join("'%s'" % name for name in pinst))):
365
    return 2
366

    
367
  jex = JobExecutor(cl=cl, opts=opts)
368
  for iname in pinst:
369
    op = opcodes.OpInstanceFailover(instance_name=iname,
370
                                    ignore_consistency=opts.ignore_consistency,
371
                                    iallocator=opts.iallocator)
372
    jex.QueueJob(iname, op)
373
  results = jex.GetResults()
374
  bad_cnt = len([row for row in results if not row[0]])
375
  if bad_cnt == 0:
376
    ToStdout("All %d instance(s) failed over successfully.", len(results))
377
  else:
378
    ToStdout("There were errors during the failover:\n"
379
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
380
  return retcode
381

    
382

    
383
def MigrateNode(opts, args):
384
  """Migrate all primary instance on a node.
385

386
  """
387
  cl = GetClient()
388
  force = opts.force
389
  selected_fields = ["name", "pinst_list"]
390

    
391
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
392
  ((node, pinst), ) = result
393

    
394
  if not pinst:
395
    ToStdout("No primary instances on node %s, exiting." % node)
396
    return 0
397

    
398
  pinst = utils.NiceSort(pinst)
399

    
400
  if not (force or
401
          AskUser("Migrate instance(s) %s?" %
402
                  utils.CommaJoin(utils.NiceSort(pinst)))):
403
    return constants.EXIT_CONFIRMATION
404

    
405
  # this should be removed once --non-live is deprecated
406
  if not opts.live and opts.migration_mode is not None:
407
    raise errors.OpPrereqError("Only one of the --non-live and "
408
                               "--migration-mode options can be passed",
409
                               errors.ECODE_INVAL)
410
  if not opts.live: # --non-live passed
411
    mode = constants.HT_MIGRATION_NONLIVE
412
  else:
413
    mode = opts.migration_mode
414

    
415
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
416
                             iallocator=opts.iallocator,
417
                             target_node=opts.dst_node)
418

    
419
  result = SubmitOpCode(op, cl=cl, opts=opts)
420

    
421
  # Keep track of submitted jobs
422
  jex = JobExecutor(cl=cl, opts=opts)
423

    
424
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
425
    jex.AddJobId(None, status, job_id)
426

    
427
  results = jex.GetResults()
428
  bad_cnt = len([row for row in results if not row[0]])
429
  if bad_cnt == 0:
430
    ToStdout("All instances migrated successfully.")
431
    rcode = constants.EXIT_SUCCESS
432
  else:
433
    ToStdout("There were %s errors during the node migration.", bad_cnt)
434
    rcode = constants.EXIT_FAILURE
435

    
436
  return rcode
437

    
438

    
439
def ShowNodeConfig(opts, args):
440
  """Show node information.
441

442
  @param opts: the command line options selected by the user
443
  @type args: list
444
  @param args: should either be an empty list, in which case
445
      we show information about all nodes, or should contain
446
      a list of nodes to be queried for information
447
  @rtype: int
448
  @return: the desired exit code
449

450
  """
451
  cl = GetClient()
452
  result = cl.QueryNodes(fields=["name", "pip", "sip",
453
                                 "pinst_list", "sinst_list",
454
                                 "master_candidate", "drained", "offline",
455
                                 "master_capable", "vm_capable", "powered",
456
                                 "ndparams", "custom_ndparams"],
457
                         names=args, use_locking=False)
458

    
459
  for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
460
       master_capable, vm_capable, powered, ndparams,
461
       ndparams_custom) in result:
462
    ToStdout("Node name: %s", name)
463
    ToStdout("  primary ip: %s", primary_ip)
464
    ToStdout("  secondary ip: %s", secondary_ip)
465
    ToStdout("  master candidate: %s", is_mc)
466
    ToStdout("  drained: %s", drained)
467
    ToStdout("  offline: %s", offline)
468
    if powered is not None:
469
      ToStdout("  powered: %s", powered)
470
    ToStdout("  master_capable: %s", master_capable)
471
    ToStdout("  vm_capable: %s", vm_capable)
472
    if vm_capable:
473
      if pinst:
474
        ToStdout("  primary for instances:")
475
        for iname in utils.NiceSort(pinst):
476
          ToStdout("    - %s", iname)
477
      else:
478
        ToStdout("  primary for no instances")
479
      if sinst:
480
        ToStdout("  secondary for instances:")
481
        for iname in utils.NiceSort(sinst):
482
          ToStdout("    - %s", iname)
483
      else:
484
        ToStdout("  secondary for no instances")
485
    ToStdout("  node parameters:")
486
    buf = StringIO()
487
    FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
488
    ToStdout(buf.getvalue().rstrip("\n"))
489

    
490
  return 0
491

    
492

    
493
def RemoveNode(opts, args):
494
  """Remove a node from the cluster.
495

496
  @param opts: the command line options selected by the user
497
  @type args: list
498
  @param args: should contain only one element, the name of
499
      the node to be removed
500
  @rtype: int
501
  @return: the desired exit code
502

503
  """
504
  op = opcodes.OpNodeRemove(node_name=args[0])
505
  SubmitOpCode(op, opts=opts)
506
  return 0
507

    
508

    
509
def PowercycleNode(opts, args):
510
  """Remove a node from the cluster.
511

512
  @param opts: the command line options selected by the user
513
  @type args: list
514
  @param args: should contain only one element, the name of
515
      the node to be removed
516
  @rtype: int
517
  @return: the desired exit code
518

519
  """
520
  node = args[0]
521
  if (not opts.confirm and
522
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
523
    return 2
524

    
525
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
526
  result = SubmitOpCode(op, opts=opts)
527
  if result:
528
    ToStderr(result)
529
  return 0
530

    
531

    
532
def PowerNode(opts, args):
533
  """Change/ask power state of a node.
534

535
  @param opts: the command line options selected by the user
536
  @type args: list
537
  @param args: should contain only one element, the name of
538
      the node to be removed
539
  @rtype: int
540
  @return: the desired exit code
541

542
  """
543
  command = args.pop(0)
544

    
545
  if opts.no_headers:
546
    headers = None
547
  else:
548
    headers = {"node": "Node", "status": "Status"}
549

    
550
  if command not in _LIST_POWER_COMMANDS:
551
    ToStderr("power subcommand %s not supported." % command)
552
    return constants.EXIT_FAILURE
553

    
554
  oob_command = "power-%s" % command
555

    
556
  if oob_command in _OOB_COMMAND_ASK:
557
    if not args:
558
      ToStderr("Please provide at least one node for this command")
559
      return constants.EXIT_FAILURE
560
    elif not opts.force and not ConfirmOperation(args, "nodes",
561
                                                 "power %s" % command):
562
      return constants.EXIT_FAILURE
563
    assert len(args) > 0
564

    
565
  opcodelist = []
566
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
567
    # TODO: This is a little ugly as we can't catch and revert
568
    for node in args:
569
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
570
                                                auto_promote=opts.auto_promote))
571

    
572
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
573
                                         command=oob_command,
574
                                         ignore_status=opts.ignore_status,
575
                                         timeout=opts.oob_timeout,
576
                                         power_delay=opts.power_delay))
577

    
578
  cli.SetGenericOpcodeOpts(opcodelist, opts)
579

    
580
  job_id = cli.SendJob(opcodelist)
581

    
582
  # We just want the OOB Opcode status
583
  # If it fails PollJob gives us the error message in it
584
  result = cli.PollJob(job_id)[-1]
585

    
586
  errs = 0
587
  data = []
588
  for node_result in result:
589
    (node_tuple, data_tuple) = node_result
590
    (_, node_name) = node_tuple
591
    (data_status, data_node) = data_tuple
592
    if data_status == constants.RS_NORMAL:
593
      if oob_command == constants.OOB_POWER_STATUS:
594
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
595
          text = "powered"
596
        else:
597
          text = "unpowered"
598
        data.append([node_name, text])
599
      else:
600
        # We don't expect data here, so we just say, it was successfully invoked
601
        data.append([node_name, "invoked"])
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 Health(opts, args):
619
  """Show health of a node using OOB.
620

621
  @param opts: the command line options selected by the user
622
  @type args: list
623
  @param args: should contain only one element, the name of
624
      the node to be removed
625
  @rtype: int
626
  @return: the desired exit code
627

628
  """
629
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
630
                            timeout=opts.oob_timeout)
631
  result = SubmitOpCode(op, opts=opts)
632

    
633
  if opts.no_headers:
634
    headers = None
635
  else:
636
    headers = {"node": "Node", "status": "Status"}
637

    
638
  errs = 0
639
  data = []
640
  for node_result in result:
641
    (node_tuple, data_tuple) = node_result
642
    (_, node_name) = node_tuple
643
    (data_status, data_node) = data_tuple
644
    if data_status == constants.RS_NORMAL:
645
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
646
      for item, status in data_node[1:]:
647
        data.append(["", "%s=%s" % (item, status)])
648
    else:
649
      errs += 1
650
      data.append([node_name, cli.FormatResultError(data_status, True)])
651

    
652
  data = GenerateTable(separator=opts.separator, headers=headers,
653
                       fields=["node", "status"], data=data)
654

    
655
  for line in data:
656
    ToStdout(line)
657

    
658
  if errs:
659
    return constants.EXIT_FAILURE
660
  else:
661
    return constants.EXIT_SUCCESS
662

    
663

    
664
def ListVolumes(opts, args):
665
  """List logical volumes on node(s).
666

667
  @param opts: the command line options selected by the user
668
  @type args: list
669
  @param args: should either be an empty list, in which case
670
      we list data for all nodes, or contain a list of nodes
671
      to display data only for those
672
  @rtype: int
673
  @return: the desired exit code
674

675
  """
676
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
677

    
678
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
679
  output = SubmitOpCode(op, opts=opts)
680

    
681
  if not opts.no_headers:
682
    headers = {"node": "Node", "phys": "PhysDev",
683
               "vg": "VG", "name": "Name",
684
               "size": "Size", "instance": "Instance"}
685
  else:
686
    headers = None
687

    
688
  unitfields = ["size"]
689

    
690
  numfields = ["size"]
691

    
692
  data = GenerateTable(separator=opts.separator, headers=headers,
693
                       fields=selected_fields, unitfields=unitfields,
694
                       numfields=numfields, data=output, units=opts.units)
695

    
696
  for line in data:
697
    ToStdout(line)
698

    
699
  return 0
700

    
701

    
702
def ListStorage(opts, args):
703
  """List physical volumes on node(s).
704

705
  @param opts: the command line options selected by the user
706
  @type args: list
707
  @param args: should either be an empty list, in which case
708
      we list data for all nodes, or contain a list of nodes
709
      to display data only for those
710
  @rtype: int
711
  @return: the desired exit code
712

713
  """
714
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
715
  if opts.user_storage_type is None:
716
    opts.user_storage_type = constants.ST_LVM_PV
717

    
718
  storage_type = ConvertStorageType(opts.user_storage_type)
719

    
720
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
721

    
722
  op = opcodes.OpNodeQueryStorage(nodes=args,
723
                                  storage_type=storage_type,
724
                                  output_fields=selected_fields)
725
  output = SubmitOpCode(op, opts=opts)
726

    
727
  if not opts.no_headers:
728
    headers = {
729
      constants.SF_NODE: "Node",
730
      constants.SF_TYPE: "Type",
731
      constants.SF_NAME: "Name",
732
      constants.SF_SIZE: "Size",
733
      constants.SF_USED: "Used",
734
      constants.SF_FREE: "Free",
735
      constants.SF_ALLOCATABLE: "Allocatable",
736
      }
737
  else:
738
    headers = None
739

    
740
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
741
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
742

    
743
  # change raw values to nicer strings
744
  for row in output:
745
    for idx, field in enumerate(selected_fields):
746
      val = row[idx]
747
      if field == constants.SF_ALLOCATABLE:
748
        if val:
749
          val = "Y"
750
        else:
751
          val = "N"
752
      row[idx] = str(val)
753

    
754
  data = GenerateTable(separator=opts.separator, headers=headers,
755
                       fields=selected_fields, unitfields=unitfields,
756
                       numfields=numfields, data=output, units=opts.units)
757

    
758
  for line in data:
759
    ToStdout(line)
760

    
761
  return 0
762

    
763

    
764
def ModifyStorage(opts, args):
765
  """Modify storage volume on a node.
766

767
  @param opts: the command line options selected by the user
768
  @type args: list
769
  @param args: should contain 3 items: node name, storage type and volume name
770
  @rtype: int
771
  @return: the desired exit code
772

773
  """
774
  (node_name, user_storage_type, volume_name) = args
775

    
776
  storage_type = ConvertStorageType(user_storage_type)
777

    
778
  changes = {}
779

    
780
  if opts.allocatable is not None:
781
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
782

    
783
  if changes:
784
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
785
                                     storage_type=storage_type,
786
                                     name=volume_name,
787
                                     changes=changes)
788
    SubmitOpCode(op, opts=opts)
789
  else:
790
    ToStderr("No changes to perform, exiting.")
791

    
792

    
793
def RepairStorage(opts, args):
794
  """Repairs a storage volume on a node.
795

796
  @param opts: the command line options selected by the user
797
  @type args: list
798
  @param args: should contain 3 items: node name, storage type and volume name
799
  @rtype: int
800
  @return: the desired exit code
801

802
  """
803
  (node_name, user_storage_type, volume_name) = args
804

    
805
  storage_type = ConvertStorageType(user_storage_type)
806

    
807
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
808
                                   storage_type=storage_type,
809
                                   name=volume_name,
810
                                   ignore_consistency=opts.ignore_consistency)
811
  SubmitOpCode(op, opts=opts)
812

    
813

    
814
def SetNodeParams(opts, args):
815
  """Modifies a node.
816

817
  @param opts: the command line options selected by the user
818
  @type args: list
819
  @param args: should contain only one element, the node name
820
  @rtype: int
821
  @return: the desired exit code
822

823
  """
824
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
825
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
826
                 opts.ndparams]
827
  if all_changes.count(None) == len(all_changes):
828
    ToStderr("Please give at least one of the parameters.")
829
    return 1
830

    
831
  op = opcodes.OpNodeSetParams(node_name=args[0],
832
                               master_candidate=opts.master_candidate,
833
                               offline=opts.offline,
834
                               drained=opts.drained,
835
                               master_capable=opts.master_capable,
836
                               vm_capable=opts.vm_capable,
837
                               secondary_ip=opts.secondary_ip,
838
                               force=opts.force,
839
                               ndparams=opts.ndparams,
840
                               auto_promote=opts.auto_promote,
841
                               powered=opts.node_powered)
842

    
843
  # even if here we process the result, we allow submit only
844
  result = SubmitOrSend(op, opts)
845

    
846
  if result:
847
    ToStdout("Modified node %s", args[0])
848
    for param, data in result:
849
      ToStdout(" - %-5s -> %s", param, data)
850
  return 0
851

    
852

    
853
commands = {
854
  "add": (
855
    AddNode, [ArgHost(min=1, max=1)],
856
    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
857
     NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
858
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT],
859
    "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
860
    " [--no-node-setup] [--verbose]"
861
    " <node_name>",
862
    "Add a node to the cluster"),
863
  "evacuate": (
864
    EvacuateNode, ARGS_ONE_NODE,
865
    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
866
     PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT],
867
    "[-f] {-I <iallocator> | -n <dst>} <node>",
868
    "Relocate the secondary instances from a node"
869
    " to other nodes"),
870
  "failover": (
871
    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
872
                                  IALLOCATOR_OPT, PRIORITY_OPT],
873
    "[-f] <node>",
874
    "Stops the primary instances on a node and start them on their"
875
    " secondary node (only for instances with drbd disk template)"),
876
  "migrate": (
877
    MigrateNode, ARGS_ONE_NODE,
878
    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
879
     IALLOCATOR_OPT, PRIORITY_OPT],
880
    "[-f] <node>",
881
    "Migrate all the primary instance on a node away from it"
882
    " (only for instances of type drbd)"),
883
  "info": (
884
    ShowNodeConfig, ARGS_MANY_NODES, [],
885
    "[<node_name>...]", "Show information about the node(s)"),
886
  "list": (
887
    ListNodes, ARGS_MANY_NODES,
888
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
889
     FORCE_FILTER_OPT],
890
    "[nodes...]",
891
    "Lists the nodes in the cluster. The available fields can be shown using"
892
    " the \"list-fields\" command (see the man page for details)."
893
    " The default field list is (in order): %s." %
894
    utils.CommaJoin(_LIST_DEF_FIELDS)),
895
  "list-fields": (
896
    ListNodeFields, [ArgUnknown()],
897
    [NOHDR_OPT, SEP_OPT],
898
    "[fields...]",
899
    "Lists all available fields for nodes"),
900
  "modify": (
901
    SetNodeParams, ARGS_ONE_NODE,
902
    [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
903
     CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
904
     AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
905
     NODE_POWERED_OPT],
906
    "<node_name>", "Alters the parameters of a node"),
907
  "powercycle": (
908
    PowercycleNode, ARGS_ONE_NODE,
909
    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
910
    "<node_name>", "Tries to forcefully powercycle a node"),
911
  "power": (
912
    PowerNode,
913
    [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
914
     ArgNode()],
915
    [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
916
     FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
917
    "on|off|cycle|status [nodes...]",
918
    "Change power state of node by calling out-of-band helper."),
919
  "remove": (
920
    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
921
    "<node_name>", "Removes a node from the cluster"),
922
  "volumes": (
923
    ListVolumes, [ArgNode()],
924
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
925
    "[<node_name>...]", "List logical volumes on node(s)"),
926
  "list-storage": (
927
    ListStorage, ARGS_MANY_NODES,
928
    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
929
     PRIORITY_OPT],
930
    "[<node_name>...]", "List physical volumes on node(s). The available"
931
    " fields are (see the man page for details): %s." %
932
    (utils.CommaJoin(_LIST_STOR_HEADERS))),
933
  "modify-storage": (
934
    ModifyStorage,
935
    [ArgNode(min=1, max=1),
936
     ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
937
     ArgFile(min=1, max=1)],
938
    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
939
    "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
940
  "repair-storage": (
941
    RepairStorage,
942
    [ArgNode(min=1, max=1),
943
     ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
944
     ArgFile(min=1, max=1)],
945
    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
946
    "<node_name> <storage_type> <name>",
947
    "Repairs a storage volume on a node"),
948
  "list-tags": (
949
    ListTags, ARGS_ONE_NODE, [],
950
    "<node_name>", "List the tags of the given node"),
951
  "add-tags": (
952
    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
953
    "<node_name> tag...", "Add tags to the given node"),
954
  "remove-tags": (
955
    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
956
    [TAG_SRC_OPT, PRIORITY_OPT],
957
    "<node_name> tag...", "Remove tags from the given node"),
958
  "health": (
959
    Health, ARGS_MANY_NODES,
960
    [NOHDR_OPT, SEP_OPT, SUBMIT_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
961
    "[<node_name>...]", "List health of node(s) using out-of-band"),
962
  }
963

    
964

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