Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 69f0340a

History | View | Annotate | Download (30.2 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=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
_ENV_OVERRIDE = frozenset(["list"])
110

    
111

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

    
117
IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
118
                               action="store_true", dest="ignore_status",
119
                               help=("Ignore the Node(s) offline status"
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
  if options.force_join:
152
    cmd.append("--force-join")
153

    
154
  cmd.extend(nodes)
155

    
156
  result = utils.RunCmd(cmd, interactive=True)
157

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

    
163

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

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

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

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

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

    
202
  # read the cluster name from the master
203
  output = cl.QueryConfigValues(["cluster_name"])
204
  cluster_name = output[0]
205

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

    
214
  if opts.node_setup:
215
    _RunSetupSSH(opts, [node])
216

    
217
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
218

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

    
225

    
226
def ListNodes(opts, args):
227
  """List nodes and their properties.
228

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

235
  """
236
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
237

    
238
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
239
                              (",".join, False))
240

    
241
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
242
                     opts.separator, not opts.no_headers,
243
                     format_override=fmtoverride, verbose=opts.verbose,
244
                     force_filter=opts.force_filter)
245

    
246

    
247
def ListNodeFields(opts, args):
248
  """List node fields.
249

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

256
  """
257
  return GenericListFields(constants.QR_NODE, args, opts.separator,
258
                           not opts.no_headers)
259

    
260

    
261
def EvacuateNode(opts, args):
262
  """Relocate all secondary instance from a node.
263

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

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

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

    
288
  # Determine affected instances
289
  fields = []
290

    
291
  if not opts.secondary_only:
292
    fields.append("pinst_list")
293
  if not opts.primary_only:
294
    fields.append("sinst_list")
295

    
296
  cl = GetClient()
297

    
298
  result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
299
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
300

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

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

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

    
320
  # Keep track of submitted jobs
321
  jex = JobExecutor(cl=cl, opts=opts)
322

    
323
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
324
    jex.AddJobId(None, status, job_id)
325

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

    
335
  return rcode
336

    
337

    
338
def FailoverNode(opts, args):
339
  """Failover all primary instance on a node.
340

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

347
  """
348
  cl = GetClient()
349
  force = opts.force
350
  selected_fields = ["name", "pinst_list"]
351

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

    
358
  if not pinst:
359
    ToStderr("No primary instances on node %s, exiting.", node)
360
    return 0
361

    
362
  pinst = utils.NiceSort(pinst)
363

    
364
  retcode = 0
365

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

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

    
385

    
386
def MigrateNode(opts, args):
387
  """Migrate all primary instance on a node.
388

389
  """
390
  cl = GetClient()
391
  force = opts.force
392
  selected_fields = ["name", "pinst_list"]
393

    
394
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
395
  ((node, pinst), ) = result
396

    
397
  if not pinst:
398
    ToStdout("No primary instances on node %s, exiting." % node)
399
    return 0
400

    
401
  pinst = utils.NiceSort(pinst)
402

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

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

    
418
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
419
                             iallocator=opts.iallocator,
420
                             target_node=opts.dst_node)
421

    
422
  result = SubmitOpCode(op, cl=cl, opts=opts)
423

    
424
  # Keep track of submitted jobs
425
  jex = JobExecutor(cl=cl, opts=opts)
426

    
427
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
428
    jex.AddJobId(None, status, job_id)
429

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

    
439
  return rcode
440

    
441

    
442
def ShowNodeConfig(opts, args):
443
  """Show node information.
444

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

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

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

    
493
  return 0
494

    
495

    
496
def RemoveNode(opts, args):
497
  """Remove a node from the cluster.
498

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

506
  """
507
  op = opcodes.OpNodeRemove(node_name=args[0])
508
  SubmitOpCode(op, opts=opts)
509
  return 0
510

    
511

    
512
def PowercycleNode(opts, args):
513
  """Remove a node from the cluster.
514

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

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

    
528
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
529
  result = SubmitOpCode(op, opts=opts)
530
  if result:
531
    ToStderr(result)
532
  return 0
533

    
534

    
535
def PowerNode(opts, args):
536
  """Change/ask power state of a node.
537

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

545
  """
546
  command = args.pop(0)
547

    
548
  if opts.no_headers:
549
    headers = None
550
  else:
551
    headers = {"node": "Node", "status": "Status"}
552

    
553
  if command not in _LIST_POWER_COMMANDS:
554
    ToStderr("power subcommand %s not supported." % command)
555
    return constants.EXIT_FAILURE
556

    
557
  oob_command = "power-%s" % command
558

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

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

    
575
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
576
                                         command=oob_command,
577
                                         ignore_status=opts.ignore_status,
578
                                         timeout=opts.oob_timeout,
579
                                         power_delay=opts.power_delay))
580

    
581
  cli.SetGenericOpcodeOpts(opcodelist, opts)
582

    
583
  job_id = cli.SendJob(opcodelist)
584

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

    
589
  errs = 0
590
  data = []
591
  for node_result in result:
592
    (node_tuple, data_tuple) = node_result
593
    (_, node_name) = node_tuple
594
    (data_status, data_node) = data_tuple
595
    if data_status == constants.RS_NORMAL:
596
      if oob_command == constants.OOB_POWER_STATUS:
597
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
598
          text = "powered"
599
        else:
600
          text = "unpowered"
601
        data.append([node_name, text])
602
      else:
603
        # We don't expect data here, so we just say, it was successfully invoked
604
        data.append([node_name, "invoked"])
605
    else:
606
      errs += 1
607
      data.append([node_name, cli.FormatResultError(data_status, True)])
608

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

    
612
  for line in data:
613
    ToStdout(line)
614

    
615
  if errs:
616
    return constants.EXIT_FAILURE
617
  else:
618
    return constants.EXIT_SUCCESS
619

    
620

    
621
def Health(opts, args):
622
  """Show health of a node using OOB.
623

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

631
  """
632
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
633
                            timeout=opts.oob_timeout)
634
  result = SubmitOpCode(op, opts=opts)
635

    
636
  if opts.no_headers:
637
    headers = None
638
  else:
639
    headers = {"node": "Node", "status": "Status"}
640

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

    
655
  data = GenerateTable(separator=opts.separator, headers=headers,
656
                       fields=["node", "status"], data=data)
657

    
658
  for line in data:
659
    ToStdout(line)
660

    
661
  if errs:
662
    return constants.EXIT_FAILURE
663
  else:
664
    return constants.EXIT_SUCCESS
665

    
666

    
667
def ListVolumes(opts, args):
668
  """List logical volumes on node(s).
669

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

678
  """
679
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
680

    
681
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
682
  output = SubmitOpCode(op, opts=opts)
683

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

    
691
  unitfields = ["size"]
692

    
693
  numfields = ["size"]
694

    
695
  data = GenerateTable(separator=opts.separator, headers=headers,
696
                       fields=selected_fields, unitfields=unitfields,
697
                       numfields=numfields, data=output, units=opts.units)
698

    
699
  for line in data:
700
    ToStdout(line)
701

    
702
  return 0
703

    
704

    
705
def ListStorage(opts, args):
706
  """List physical volumes on node(s).
707

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

716
  """
717
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
718
  if opts.user_storage_type is None:
719
    opts.user_storage_type = constants.ST_LVM_PV
720

    
721
  storage_type = ConvertStorageType(opts.user_storage_type)
722

    
723
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
724

    
725
  op = opcodes.OpNodeQueryStorage(nodes=args,
726
                                  storage_type=storage_type,
727
                                  output_fields=selected_fields)
728
  output = SubmitOpCode(op, opts=opts)
729

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

    
743
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
744
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
745

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

    
757
  data = GenerateTable(separator=opts.separator, headers=headers,
758
                       fields=selected_fields, unitfields=unitfields,
759
                       numfields=numfields, data=output, units=opts.units)
760

    
761
  for line in data:
762
    ToStdout(line)
763

    
764
  return 0
765

    
766

    
767
def ModifyStorage(opts, args):
768
  """Modify storage volume on a node.
769

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

776
  """
777
  (node_name, user_storage_type, volume_name) = args
778

    
779
  storage_type = ConvertStorageType(user_storage_type)
780

    
781
  changes = {}
782

    
783
  if opts.allocatable is not None:
784
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
785

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

    
795

    
796
def RepairStorage(opts, args):
797
  """Repairs a storage volume on a node.
798

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

805
  """
806
  (node_name, user_storage_type, volume_name) = args
807

    
808
  storage_type = ConvertStorageType(user_storage_type)
809

    
810
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
811
                                   storage_type=storage_type,
812
                                   name=volume_name,
813
                                   ignore_consistency=opts.ignore_consistency)
814
  SubmitOpCode(op, opts=opts)
815

    
816

    
817
def SetNodeParams(opts, args):
818
  """Modifies a node.
819

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

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

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

    
846
  # even if here we process the result, we allow submit only
847
  result = SubmitOrSend(op, opts)
848

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

    
855

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

    
967

    
968
def Main():
969
  return GenericMain(commands, override={"tag_type": constants.TAG_NODE},
970
                     env_override=_ENV_OVERRIDE)