Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 57dc299a

History | View | Annotate | Download (30.8 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
  if opts.disk_state:
220
    disk_state = utils.FlatToDict(opts.disk_state)
221
  else:
222
    disk_state = {}
223

    
224
  hv_state = dict(opts.hv_state)
225

    
226
  op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
227
                         readd=opts.readd, group=opts.nodegroup,
228
                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
229
                         master_capable=opts.master_capable,
230
                         disk_state=disk_state,
231
                         hv_state=hv_state)
232
  SubmitOpCode(op, opts=opts)
233

    
234

    
235
def ListNodes(opts, args):
236
  """List nodes and their properties.
237

238
  @param opts: the command line options selected by the user
239
  @type args: list
240
  @param args: nodes to list, or empty for all
241
  @rtype: int
242
  @return: the desired exit code
243

244
  """
245
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
246

    
247
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
248
                              (",".join, False))
249

    
250
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
251
                     opts.separator, not opts.no_headers,
252
                     format_override=fmtoverride, verbose=opts.verbose,
253
                     force_filter=opts.force_filter)
254

    
255

    
256
def ListNodeFields(opts, args):
257
  """List node fields.
258

259
  @param opts: the command line options selected by the user
260
  @type args: list
261
  @param args: fields to list, or empty for all
262
  @rtype: int
263
  @return: the desired exit code
264

265
  """
266
  return GenericListFields(constants.QR_NODE, args, opts.separator,
267
                           not opts.no_headers)
268

    
269

    
270
def EvacuateNode(opts, args):
271
  """Relocate all secondary instance from a node.
272

273
  @param opts: the command line options selected by the user
274
  @type args: list
275
  @param args: should be an empty list
276
  @rtype: int
277
  @return: the desired exit code
278

279
  """
280
  if opts.dst_node is not None:
281
    ToStderr("New secondary node given (disabling iallocator), hence evacuating"
282
             " secondary instances only.")
283
    opts.secondary_only = True
284
    opts.primary_only = False
285

    
286
  if opts.secondary_only and opts.primary_only:
287
    raise errors.OpPrereqError("Only one of the --primary-only and"
288
                               " --secondary-only options can be passed",
289
                               errors.ECODE_INVAL)
290
  elif opts.primary_only:
291
    mode = constants.NODE_EVAC_PRI
292
  elif opts.secondary_only:
293
    mode = constants.NODE_EVAC_SEC
294
  else:
295
    mode = constants.NODE_EVAC_ALL
296

    
297
  # Determine affected instances
298
  fields = []
299

    
300
  if not opts.secondary_only:
301
    fields.append("pinst_list")
302
  if not opts.primary_only:
303
    fields.append("sinst_list")
304

    
305
  cl = GetClient()
306

    
307
  result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
308
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
309

    
310
  if not instances:
311
    # No instances to evacuate
312
    ToStderr("No instances to evacuate on node(s) %s, exiting.",
313
             utils.CommaJoin(args))
314
    return constants.EXIT_SUCCESS
315

    
316
  if not (opts.force or
317
          AskUser("Relocate instance(s) %s from node(s) %s?" %
318
                  (utils.CommaJoin(utils.NiceSort(instances)),
319
                   utils.CommaJoin(args)))):
320
    return constants.EXIT_CONFIRMATION
321

    
322
  # Evacuate node
323
  op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
324
                              remote_node=opts.dst_node,
325
                              iallocator=opts.iallocator,
326
                              early_release=opts.early_release)
327
  result = SubmitOpCode(op, cl=cl, opts=opts)
328

    
329
  # Keep track of submitted jobs
330
  jex = JobExecutor(cl=cl, opts=opts)
331

    
332
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
333
    jex.AddJobId(None, status, job_id)
334

    
335
  results = jex.GetResults()
336
  bad_cnt = len([row for row in results if not row[0]])
337
  if bad_cnt == 0:
338
    ToStdout("All instances evacuated successfully.")
339
    rcode = constants.EXIT_SUCCESS
340
  else:
341
    ToStdout("There were %s errors during the evacuation.", bad_cnt)
342
    rcode = constants.EXIT_FAILURE
343

    
344
  return rcode
345

    
346

    
347
def FailoverNode(opts, args):
348
  """Failover all primary instance on a node.
349

350
  @param opts: the command line options selected by the user
351
  @type args: list
352
  @param args: should be an empty list
353
  @rtype: int
354
  @return: the desired exit code
355

356
  """
357
  cl = GetClient()
358
  force = opts.force
359
  selected_fields = ["name", "pinst_list"]
360

    
361
  # these fields are static data anyway, so it doesn't matter, but
362
  # locking=True should be safer
363
  result = cl.QueryNodes(names=args, fields=selected_fields,
364
                         use_locking=False)
365
  node, pinst = result[0]
366

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

    
371
  pinst = utils.NiceSort(pinst)
372

    
373
  retcode = 0
374

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

    
379
  jex = JobExecutor(cl=cl, opts=opts)
380
  for iname in pinst:
381
    op = opcodes.OpInstanceFailover(instance_name=iname,
382
                                    ignore_consistency=opts.ignore_consistency,
383
                                    iallocator=opts.iallocator)
384
    jex.QueueJob(iname, op)
385
  results = jex.GetResults()
386
  bad_cnt = len([row for row in results if not row[0]])
387
  if bad_cnt == 0:
388
    ToStdout("All %d instance(s) failed over successfully.", len(results))
389
  else:
390
    ToStdout("There were errors during the failover:\n"
391
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
392
  return retcode
393

    
394

    
395
def MigrateNode(opts, args):
396
  """Migrate all primary instance on a node.
397

398
  """
399
  cl = GetClient()
400
  force = opts.force
401
  selected_fields = ["name", "pinst_list"]
402

    
403
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
404
  ((node, pinst), ) = result
405

    
406
  if not pinst:
407
    ToStdout("No primary instances on node %s, exiting." % node)
408
    return 0
409

    
410
  pinst = utils.NiceSort(pinst)
411

    
412
  if not (force or
413
          AskUser("Migrate instance(s) %s?" %
414
                  utils.CommaJoin(utils.NiceSort(pinst)))):
415
    return constants.EXIT_CONFIRMATION
416

    
417
  # this should be removed once --non-live is deprecated
418
  if not opts.live and opts.migration_mode is not None:
419
    raise errors.OpPrereqError("Only one of the --non-live and "
420
                               "--migration-mode options can be passed",
421
                               errors.ECODE_INVAL)
422
  if not opts.live: # --non-live passed
423
    mode = constants.HT_MIGRATION_NONLIVE
424
  else:
425
    mode = opts.migration_mode
426

    
427
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
428
                             iallocator=opts.iallocator,
429
                             target_node=opts.dst_node,
430
                             ignore_ipolicy=opts.ignore_ipolicy)
431

    
432
  result = SubmitOpCode(op, cl=cl, opts=opts)
433

    
434
  # Keep track of submitted jobs
435
  jex = JobExecutor(cl=cl, opts=opts)
436

    
437
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
438
    jex.AddJobId(None, status, job_id)
439

    
440
  results = jex.GetResults()
441
  bad_cnt = len([row for row in results if not row[0]])
442
  if bad_cnt == 0:
443
    ToStdout("All instances migrated successfully.")
444
    rcode = constants.EXIT_SUCCESS
445
  else:
446
    ToStdout("There were %s errors during the node migration.", bad_cnt)
447
    rcode = constants.EXIT_FAILURE
448

    
449
  return rcode
450

    
451

    
452
def ShowNodeConfig(opts, args):
453
  """Show node information.
454

455
  @param opts: the command line options selected by the user
456
  @type args: list
457
  @param args: should either be an empty list, in which case
458
      we show information about all nodes, or should contain
459
      a list of nodes to be queried for information
460
  @rtype: int
461
  @return: the desired exit code
462

463
  """
464
  cl = GetClient()
465
  result = cl.QueryNodes(fields=["name", "pip", "sip",
466
                                 "pinst_list", "sinst_list",
467
                                 "master_candidate", "drained", "offline",
468
                                 "master_capable", "vm_capable", "powered",
469
                                 "ndparams", "custom_ndparams"],
470
                         names=args, use_locking=False)
471

    
472
  for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
473
       master_capable, vm_capable, powered, ndparams,
474
       ndparams_custom) in result:
475
    ToStdout("Node name: %s", name)
476
    ToStdout("  primary ip: %s", primary_ip)
477
    ToStdout("  secondary ip: %s", secondary_ip)
478
    ToStdout("  master candidate: %s", is_mc)
479
    ToStdout("  drained: %s", drained)
480
    ToStdout("  offline: %s", offline)
481
    if powered is not None:
482
      ToStdout("  powered: %s", powered)
483
    ToStdout("  master_capable: %s", master_capable)
484
    ToStdout("  vm_capable: %s", vm_capable)
485
    if vm_capable:
486
      if pinst:
487
        ToStdout("  primary for instances:")
488
        for iname in utils.NiceSort(pinst):
489
          ToStdout("    - %s", iname)
490
      else:
491
        ToStdout("  primary for no instances")
492
      if sinst:
493
        ToStdout("  secondary for instances:")
494
        for iname in utils.NiceSort(sinst):
495
          ToStdout("    - %s", iname)
496
      else:
497
        ToStdout("  secondary for no instances")
498
    ToStdout("  node parameters:")
499
    buf = StringIO()
500
    FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
501
    ToStdout(buf.getvalue().rstrip("\n"))
502

    
503
  return 0
504

    
505

    
506
def RemoveNode(opts, args):
507
  """Remove a node from the cluster.
508

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

516
  """
517
  op = opcodes.OpNodeRemove(node_name=args[0])
518
  SubmitOpCode(op, opts=opts)
519
  return 0
520

    
521

    
522
def PowercycleNode(opts, args):
523
  """Remove a node from the cluster.
524

525
  @param opts: the command line options selected by the user
526
  @type args: list
527
  @param args: should contain only one element, the name of
528
      the node to be removed
529
  @rtype: int
530
  @return: the desired exit code
531

532
  """
533
  node = args[0]
534
  if (not opts.confirm and
535
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
536
    return 2
537

    
538
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
539
  result = SubmitOpCode(op, opts=opts)
540
  if result:
541
    ToStderr(result)
542
  return 0
543

    
544

    
545
def PowerNode(opts, args):
546
  """Change/ask power state of a node.
547

548
  @param opts: the command line options selected by the user
549
  @type args: list
550
  @param args: should contain only one element, the name of
551
      the node to be removed
552
  @rtype: int
553
  @return: the desired exit code
554

555
  """
556
  command = args.pop(0)
557

    
558
  if opts.no_headers:
559
    headers = None
560
  else:
561
    headers = {"node": "Node", "status": "Status"}
562

    
563
  if command not in _LIST_POWER_COMMANDS:
564
    ToStderr("power subcommand %s not supported." % command)
565
    return constants.EXIT_FAILURE
566

    
567
  oob_command = "power-%s" % command
568

    
569
  if oob_command in _OOB_COMMAND_ASK:
570
    if not args:
571
      ToStderr("Please provide at least one node for this command")
572
      return constants.EXIT_FAILURE
573
    elif not opts.force and not ConfirmOperation(args, "nodes",
574
                                                 "power %s" % command):
575
      return constants.EXIT_FAILURE
576
    assert len(args) > 0
577

    
578
  opcodelist = []
579
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
580
    # TODO: This is a little ugly as we can't catch and revert
581
    for node in args:
582
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
583
                                                auto_promote=opts.auto_promote))
584

    
585
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
586
                                         command=oob_command,
587
                                         ignore_status=opts.ignore_status,
588
                                         timeout=opts.oob_timeout,
589
                                         power_delay=opts.power_delay))
590

    
591
  cli.SetGenericOpcodeOpts(opcodelist, opts)
592

    
593
  job_id = cli.SendJob(opcodelist)
594

    
595
  # We just want the OOB Opcode status
596
  # If it fails PollJob gives us the error message in it
597
  result = cli.PollJob(job_id)[-1]
598

    
599
  errs = 0
600
  data = []
601
  for node_result in result:
602
    (node_tuple, data_tuple) = node_result
603
    (_, node_name) = node_tuple
604
    (data_status, data_node) = data_tuple
605
    if data_status == constants.RS_NORMAL:
606
      if oob_command == constants.OOB_POWER_STATUS:
607
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
608
          text = "powered"
609
        else:
610
          text = "unpowered"
611
        data.append([node_name, text])
612
      else:
613
        # We don't expect data here, so we just say, it was successfully invoked
614
        data.append([node_name, "invoked"])
615
    else:
616
      errs += 1
617
      data.append([node_name, cli.FormatResultError(data_status, True)])
618

    
619
  data = GenerateTable(separator=opts.separator, headers=headers,
620
                       fields=["node", "status"], data=data)
621

    
622
  for line in data:
623
    ToStdout(line)
624

    
625
  if errs:
626
    return constants.EXIT_FAILURE
627
  else:
628
    return constants.EXIT_SUCCESS
629

    
630

    
631
def Health(opts, args):
632
  """Show health of a node using OOB.
633

634
  @param opts: the command line options selected by the user
635
  @type args: list
636
  @param args: should contain only one element, the name of
637
      the node to be removed
638
  @rtype: int
639
  @return: the desired exit code
640

641
  """
642
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
643
                            timeout=opts.oob_timeout)
644
  result = SubmitOpCode(op, opts=opts)
645

    
646
  if opts.no_headers:
647
    headers = None
648
  else:
649
    headers = {"node": "Node", "status": "Status"}
650

    
651
  errs = 0
652
  data = []
653
  for node_result in result:
654
    (node_tuple, data_tuple) = node_result
655
    (_, node_name) = node_tuple
656
    (data_status, data_node) = data_tuple
657
    if data_status == constants.RS_NORMAL:
658
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
659
      for item, status in data_node[1:]:
660
        data.append(["", "%s=%s" % (item, status)])
661
    else:
662
      errs += 1
663
      data.append([node_name, cli.FormatResultError(data_status, True)])
664

    
665
  data = GenerateTable(separator=opts.separator, headers=headers,
666
                       fields=["node", "status"], data=data)
667

    
668
  for line in data:
669
    ToStdout(line)
670

    
671
  if errs:
672
    return constants.EXIT_FAILURE
673
  else:
674
    return constants.EXIT_SUCCESS
675

    
676

    
677
def ListVolumes(opts, args):
678
  """List logical volumes on node(s).
679

680
  @param opts: the command line options selected by the user
681
  @type args: list
682
  @param args: should either be an empty list, in which case
683
      we list data for all nodes, or contain a list of nodes
684
      to display data only for those
685
  @rtype: int
686
  @return: the desired exit code
687

688
  """
689
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
690

    
691
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
692
  output = SubmitOpCode(op, opts=opts)
693

    
694
  if not opts.no_headers:
695
    headers = {"node": "Node", "phys": "PhysDev",
696
               "vg": "VG", "name": "Name",
697
               "size": "Size", "instance": "Instance"}
698
  else:
699
    headers = None
700

    
701
  unitfields = ["size"]
702

    
703
  numfields = ["size"]
704

    
705
  data = GenerateTable(separator=opts.separator, headers=headers,
706
                       fields=selected_fields, unitfields=unitfields,
707
                       numfields=numfields, data=output, units=opts.units)
708

    
709
  for line in data:
710
    ToStdout(line)
711

    
712
  return 0
713

    
714

    
715
def ListStorage(opts, args):
716
  """List physical volumes on node(s).
717

718
  @param opts: the command line options selected by the user
719
  @type args: list
720
  @param args: should either be an empty list, in which case
721
      we list data for all nodes, or contain a list of nodes
722
      to display data only for those
723
  @rtype: int
724
  @return: the desired exit code
725

726
  """
727
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
728
  if opts.user_storage_type is None:
729
    opts.user_storage_type = constants.ST_LVM_PV
730

    
731
  storage_type = ConvertStorageType(opts.user_storage_type)
732

    
733
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
734

    
735
  op = opcodes.OpNodeQueryStorage(nodes=args,
736
                                  storage_type=storage_type,
737
                                  output_fields=selected_fields)
738
  output = SubmitOpCode(op, opts=opts)
739

    
740
  if not opts.no_headers:
741
    headers = {
742
      constants.SF_NODE: "Node",
743
      constants.SF_TYPE: "Type",
744
      constants.SF_NAME: "Name",
745
      constants.SF_SIZE: "Size",
746
      constants.SF_USED: "Used",
747
      constants.SF_FREE: "Free",
748
      constants.SF_ALLOCATABLE: "Allocatable",
749
      }
750
  else:
751
    headers = None
752

    
753
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
754
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
755

    
756
  # change raw values to nicer strings
757
  for row in output:
758
    for idx, field in enumerate(selected_fields):
759
      val = row[idx]
760
      if field == constants.SF_ALLOCATABLE:
761
        if val:
762
          val = "Y"
763
        else:
764
          val = "N"
765
      row[idx] = str(val)
766

    
767
  data = GenerateTable(separator=opts.separator, headers=headers,
768
                       fields=selected_fields, unitfields=unitfields,
769
                       numfields=numfields, data=output, units=opts.units)
770

    
771
  for line in data:
772
    ToStdout(line)
773

    
774
  return 0
775

    
776

    
777
def ModifyStorage(opts, args):
778
  """Modify storage volume on a node.
779

780
  @param opts: the command line options selected by the user
781
  @type args: list
782
  @param args: should contain 3 items: node name, storage type and volume name
783
  @rtype: int
784
  @return: the desired exit code
785

786
  """
787
  (node_name, user_storage_type, volume_name) = args
788

    
789
  storage_type = ConvertStorageType(user_storage_type)
790

    
791
  changes = {}
792

    
793
  if opts.allocatable is not None:
794
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
795

    
796
  if changes:
797
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
798
                                     storage_type=storage_type,
799
                                     name=volume_name,
800
                                     changes=changes)
801
    SubmitOpCode(op, opts=opts)
802
  else:
803
    ToStderr("No changes to perform, exiting.")
804

    
805

    
806
def RepairStorage(opts, args):
807
  """Repairs a storage volume on a node.
808

809
  @param opts: the command line options selected by the user
810
  @type args: list
811
  @param args: should contain 3 items: node name, storage type and volume name
812
  @rtype: int
813
  @return: the desired exit code
814

815
  """
816
  (node_name, user_storage_type, volume_name) = args
817

    
818
  storage_type = ConvertStorageType(user_storage_type)
819

    
820
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
821
                                   storage_type=storage_type,
822
                                   name=volume_name,
823
                                   ignore_consistency=opts.ignore_consistency)
824
  SubmitOpCode(op, opts=opts)
825

    
826

    
827
def SetNodeParams(opts, args):
828
  """Modifies a node.
829

830
  @param opts: the command line options selected by the user
831
  @type args: list
832
  @param args: should contain only one element, the node name
833
  @rtype: int
834
  @return: the desired exit code
835

836
  """
837
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
838
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
839
                 opts.ndparams]
840
  if (all_changes.count(None) == len(all_changes) and
841
      not (opts.hv_state or opts.disk_state)):
842
    ToStderr("Please give at least one of the parameters.")
843
    return 1
844

    
845
  if opts.disk_state:
846
    disk_state = utils.FlatToDict(opts.disk_state)
847
  else:
848
    disk_state = {}
849

    
850
  hv_state = dict(opts.hv_state)
851

    
852
  op = opcodes.OpNodeSetParams(node_name=args[0],
853
                               master_candidate=opts.master_candidate,
854
                               offline=opts.offline,
855
                               drained=opts.drained,
856
                               master_capable=opts.master_capable,
857
                               vm_capable=opts.vm_capable,
858
                               secondary_ip=opts.secondary_ip,
859
                               force=opts.force,
860
                               ndparams=opts.ndparams,
861
                               auto_promote=opts.auto_promote,
862
                               powered=opts.node_powered,
863
                               hv_state=hv_state,
864
                               disk_state=disk_state)
865

    
866
  # even if here we process the result, we allow submit only
867
  result = SubmitOrSend(op, opts)
868

    
869
  if result:
870
    ToStdout("Modified node %s", args[0])
871
    for param, data in result:
872
      ToStdout(" - %-5s -> %s", param, data)
873
  return 0
874

    
875

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

    
988

    
989
def Main():
990
  return GenericMain(commands, override={"tag_type": constants.TAG_NODE},
991
                     env_override=_ENV_OVERRIDE)