Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 96897af7

History | View | Annotate | Download (31.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
                             allow_runtime_changes=opts.allow_runtime_chgs,
431
                             ignore_ipolicy=opts.ignore_ipolicy)
432

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

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

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

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

    
450
  return rcode
451

    
452

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

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

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

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

    
504
  return 0
505

    
506

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

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

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

    
522

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

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

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

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

    
545

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

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

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

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

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

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

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

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

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

    
592
  cli.SetGenericOpcodeOpts(opcodelist, opts)
593

    
594
  job_id = cli.SendJob(opcodelist)
595

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

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

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

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

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

    
631

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

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

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

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

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

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

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

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

    
677

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

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

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

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

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

    
702
  unitfields = ["size"]
703

    
704
  numfields = ["size"]
705

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

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

    
713
  return 0
714

    
715

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

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

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

    
732
  storage_type = ConvertStorageType(opts.user_storage_type)
733

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

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

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

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

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

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

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

    
775
  return 0
776

    
777

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

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

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

    
790
  storage_type = ConvertStorageType(user_storage_type)
791

    
792
  changes = {}
793

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

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

    
806

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

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

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

    
819
  storage_type = ConvertStorageType(user_storage_type)
820

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

    
827

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

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

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

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

    
851
  hv_state = dict(opts.hv_state)
852

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

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

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

    
876

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

    
990
#: dictionary with aliases for commands
991
aliases = {
992
  "show": "info",
993
  }
994

    
995

    
996
def Main():
997
  return GenericMain(commands, aliases=aliases,
998
                     override={"tag_type": constants.TAG_NODE},
999
                     env_override=_ENV_OVERRIDE)