Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 085e0d9f

History | View | Annotate | Download (30.7 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

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

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

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

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

    
448
  return rcode
449

    
450

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

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

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

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

    
502
  return 0
503

    
504

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

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

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

    
520

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

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

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

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

    
543

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

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

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

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

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

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

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

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

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

    
590
  cli.SetGenericOpcodeOpts(opcodelist, opts)
591

    
592
  job_id = cli.SendJob(opcodelist)
593

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

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

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

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

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

    
629

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

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

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

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

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

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

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

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

    
675

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

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

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

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

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

    
700
  unitfields = ["size"]
701

    
702
  numfields = ["size"]
703

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

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

    
711
  return 0
712

    
713

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

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

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

    
730
  storage_type = ConvertStorageType(opts.user_storage_type)
731

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

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

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

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

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

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

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

    
773
  return 0
774

    
775

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

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

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

    
788
  storage_type = ConvertStorageType(user_storage_type)
789

    
790
  changes = {}
791

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

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

    
804

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

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

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

    
817
  storage_type = ConvertStorageType(user_storage_type)
818

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

    
825

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

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

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

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

    
849
  hv_state = dict(opts.hv_state)
850

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

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

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

    
874

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

    
987

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