Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ f1bebf4c

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

    
142
  assert nodes, "Empty node list"
143

    
144
  cmd = [constants.SETUP_SSH]
145

    
146
  # Pass --debug|--verbose to the external script if set on our invocation
147
  # --debug overrides --verbose
148
  if options.debug:
149
    cmd.append("--debug")
150
  elif options.verbose:
151
    cmd.append("--verbose")
152
  if not options.ssh_key_check:
153
    cmd.append("--no-ssh-key-check")
154
  if options.force_join:
155
    cmd.append("--force-join")
156

    
157
  cmd.extend(nodes)
158

    
159
  result = utils.RunCmd(cmd, interactive=True)
160

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

    
166

    
167
@UsesRPC
168
def AddNode(opts, args):
169
  """Add a node to the cluster.
170

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

177
  """
178
  cl = GetClient()
179
  node = netutils.GetHostname(name=args[0]).name
180
  readd = opts.readd
181

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

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

    
205
  # read the cluster name from the master
206
  output = cl.QueryConfigValues(["cluster_name"])
207
  cluster_name = output[0]
208

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

    
217
  if opts.node_setup:
218
    _RunSetupSSH(opts, [node])
219

    
220
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
221

    
222
  if opts.disk_state:
223
    disk_state = utils.FlatToDict(opts.disk_state)
224
  else:
225
    disk_state = {}
226

    
227
  hv_state = dict(opts.hv_state)
228

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

    
237

    
238
def ListNodes(opts, args):
239
  """List nodes and their properties.
240

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

247
  """
248
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
249

    
250
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
251
                              (",".join, False))
252

    
253
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
254
                     opts.separator, not opts.no_headers,
255
                     format_override=fmtoverride, verbose=opts.verbose,
256
                     force_filter=opts.force_filter)
257

    
258

    
259
def ListNodeFields(opts, args):
260
  """List node fields.
261

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

268
  """
269
  return GenericListFields(constants.QR_NODE, args, opts.separator,
270
                           not opts.no_headers)
271

    
272

    
273
def EvacuateNode(opts, args):
274
  """Relocate all secondary instance from a node.
275

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

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

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

    
300
  # Determine affected instances
301
  fields = []
302

    
303
  if not opts.secondary_only:
304
    fields.append("pinst_list")
305
  if not opts.primary_only:
306
    fields.append("sinst_list")
307

    
308
  cl = GetClient()
309

    
310
  result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
311
  instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
312

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

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

    
325
  # Evacuate node
326
  op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
327
                              remote_node=opts.dst_node,
328
                              iallocator=opts.iallocator,
329
                              early_release=opts.early_release)
330
  result = SubmitOrSend(op, opts, cl=cl)
331

    
332
  # Keep track of submitted jobs
333
  jex = JobExecutor(cl=cl, opts=opts)
334

    
335
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
336
    jex.AddJobId(None, status, job_id)
337

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

    
347
  return rcode
348

    
349

    
350
def FailoverNode(opts, args):
351
  """Failover all primary instance on a node.
352

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

359
  """
360
  cl = GetClient()
361
  force = opts.force
362
  selected_fields = ["name", "pinst_list"]
363

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

    
370
  if not pinst:
371
    ToStderr("No primary instances on node %s, exiting.", node)
372
    return 0
373

    
374
  pinst = utils.NiceSort(pinst)
375

    
376
  retcode = 0
377

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

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

    
397

    
398
def MigrateNode(opts, args):
399
  """Migrate all primary instance on a node.
400

401
  """
402
  cl = GetClient()
403
  force = opts.force
404
  selected_fields = ["name", "pinst_list"]
405

    
406
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
407
  ((node, pinst), ) = result
408

    
409
  if not pinst:
410
    ToStdout("No primary instances on node %s, exiting." % node)
411
    return 0
412

    
413
  pinst = utils.NiceSort(pinst)
414

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

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

    
430
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
431
                             iallocator=opts.iallocator,
432
                             target_node=opts.dst_node,
433
                             allow_runtime_changes=opts.allow_runtime_chgs,
434
                             ignore_ipolicy=opts.ignore_ipolicy)
435

    
436
  result = SubmitOrSend(op, opts, cl=cl)
437

    
438
  # Keep track of submitted jobs
439
  jex = JobExecutor(cl=cl, opts=opts)
440

    
441
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
442
    jex.AddJobId(None, status, job_id)
443

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

    
453
  return rcode
454

    
455

    
456
def ShowNodeConfig(opts, args):
457
  """Show node information.
458

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

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

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

    
507
  return 0
508

    
509

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

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

520
  """
521
  op = opcodes.OpNodeRemove(node_name=args[0])
522
  SubmitOpCode(op, opts=opts)
523
  return 0
524

    
525

    
526
def PowercycleNode(opts, args):
527
  """Remove a node from the cluster.
528

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

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

    
542
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
543
  result = SubmitOrSend(op, opts)
544
  if result:
545
    ToStderr(result)
546
  return 0
547

    
548

    
549
def PowerNode(opts, args):
550
  """Change/ask power state of a node.
551

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

559
  """
560
  command = args.pop(0)
561

    
562
  if opts.no_headers:
563
    headers = None
564
  else:
565
    headers = {"node": "Node", "status": "Status"}
566

    
567
  if command not in _LIST_POWER_COMMANDS:
568
    ToStderr("power subcommand %s not supported." % command)
569
    return constants.EXIT_FAILURE
570

    
571
  oob_command = "power-%s" % command
572

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

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

    
589
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
590
                                         command=oob_command,
591
                                         ignore_status=opts.ignore_status,
592
                                         timeout=opts.oob_timeout,
593
                                         power_delay=opts.power_delay))
594

    
595
  cli.SetGenericOpcodeOpts(opcodelist, opts)
596

    
597
  job_id = cli.SendJob(opcodelist)
598

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

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

    
623
  data = GenerateTable(separator=opts.separator, headers=headers,
624
                       fields=["node", "status"], data=data)
625

    
626
  for line in data:
627
    ToStdout(line)
628

    
629
  if errs:
630
    return constants.EXIT_FAILURE
631
  else:
632
    return constants.EXIT_SUCCESS
633

    
634

    
635
def Health(opts, args):
636
  """Show health of a node using OOB.
637

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

645
  """
646
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
647
                            timeout=opts.oob_timeout)
648
  result = SubmitOpCode(op, opts=opts)
649

    
650
  if opts.no_headers:
651
    headers = None
652
  else:
653
    headers = {"node": "Node", "status": "Status"}
654

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

    
669
  data = GenerateTable(separator=opts.separator, headers=headers,
670
                       fields=["node", "status"], data=data)
671

    
672
  for line in data:
673
    ToStdout(line)
674

    
675
  if errs:
676
    return constants.EXIT_FAILURE
677
  else:
678
    return constants.EXIT_SUCCESS
679

    
680

    
681
def ListVolumes(opts, args):
682
  """List logical volumes on node(s).
683

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

692
  """
693
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
694

    
695
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
696
  output = SubmitOpCode(op, opts=opts)
697

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

    
705
  unitfields = ["size"]
706

    
707
  numfields = ["size"]
708

    
709
  data = GenerateTable(separator=opts.separator, headers=headers,
710
                       fields=selected_fields, unitfields=unitfields,
711
                       numfields=numfields, data=output, units=opts.units)
712

    
713
  for line in data:
714
    ToStdout(line)
715

    
716
  return 0
717

    
718

    
719
def ListStorage(opts, args):
720
  """List physical volumes on node(s).
721

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

730
  """
731
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
732
  if opts.user_storage_type is None:
733
    opts.user_storage_type = constants.ST_LVM_PV
734

    
735
  storage_type = ConvertStorageType(opts.user_storage_type)
736

    
737
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
738

    
739
  op = opcodes.OpNodeQueryStorage(nodes=args,
740
                                  storage_type=storage_type,
741
                                  output_fields=selected_fields)
742
  output = SubmitOpCode(op, opts=opts)
743

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

    
757
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
758
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
759

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

    
771
  data = GenerateTable(separator=opts.separator, headers=headers,
772
                       fields=selected_fields, unitfields=unitfields,
773
                       numfields=numfields, data=output, units=opts.units)
774

    
775
  for line in data:
776
    ToStdout(line)
777

    
778
  return 0
779

    
780

    
781
def ModifyStorage(opts, args):
782
  """Modify storage volume on a node.
783

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

790
  """
791
  (node_name, user_storage_type, volume_name) = args
792

    
793
  storage_type = ConvertStorageType(user_storage_type)
794

    
795
  changes = {}
796

    
797
  if opts.allocatable is not None:
798
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
799

    
800
  if changes:
801
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
802
                                     storage_type=storage_type,
803
                                     name=volume_name,
804
                                     changes=changes)
805
    SubmitOrSend(op, opts)
806
  else:
807
    ToStderr("No changes to perform, exiting.")
808

    
809

    
810
def RepairStorage(opts, args):
811
  """Repairs a storage volume on a node.
812

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

819
  """
820
  (node_name, user_storage_type, volume_name) = args
821

    
822
  storage_type = ConvertStorageType(user_storage_type)
823

    
824
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
825
                                   storage_type=storage_type,
826
                                   name=volume_name,
827
                                   ignore_consistency=opts.ignore_consistency)
828
  SubmitOrSend(op, opts)
829

    
830

    
831
def SetNodeParams(opts, args):
832
  """Modifies a node.
833

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

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

    
849
  if opts.disk_state:
850
    disk_state = utils.FlatToDict(opts.disk_state)
851
  else:
852
    disk_state = {}
853

    
854
  hv_state = dict(opts.hv_state)
855

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

    
870
  # even if here we process the result, we allow submit only
871
  result = SubmitOrSend(op, opts)
872

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

    
879

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

    
993
#: dictionary with aliases for commands
994
aliases = {
995
  "show": "info",
996
  }
997

    
998

    
999
def Main():
1000
  return GenericMain(commands, aliases=aliases,
1001
                     override={"tag_type": constants.TAG_NODE},
1002
                     env_override=_ENV_OVERRIDE)