Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ 029fe503

History | View | Annotate | Download (29.5 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-msg=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
from ganeti.cli import *
30
from ganeti import cli
31
from ganeti import bootstrap
32
from ganeti import opcodes
33
from ganeti import utils
34
from ganeti import constants
35
from ganeti import errors
36
from ganeti import netutils
37
from cStringIO import StringIO
38

    
39

    
40
#: default list of field for L{ListNodes}
41
_LIST_DEF_FIELDS = [
42
  "name", "dtotal", "dfree",
43
  "mtotal", "mnode", "mfree",
44
  "pinst_cnt", "sinst_cnt",
45
  ]
46

    
47

    
48
#: Default field list for L{ListVolumes}
49
_LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
50

    
51

    
52
#: default list of field for L{ListStorage}
53
_LIST_STOR_DEF_FIELDS = [
54
  constants.SF_NODE,
55
  constants.SF_TYPE,
56
  constants.SF_NAME,
57
  constants.SF_SIZE,
58
  constants.SF_USED,
59
  constants.SF_FREE,
60
  constants.SF_ALLOCATABLE,
61
  ]
62

    
63

    
64
#: default list of power commands
65
_LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
66

    
67

    
68
#: headers (and full field list) for L{ListStorage}
69
_LIST_STOR_HEADERS = {
70
  constants.SF_NODE: "Node",
71
  constants.SF_TYPE: "Type",
72
  constants.SF_NAME: "Name",
73
  constants.SF_SIZE: "Size",
74
  constants.SF_USED: "Used",
75
  constants.SF_FREE: "Free",
76
  constants.SF_ALLOCATABLE: "Allocatable",
77
  }
78

    
79

    
80
#: User-facing storage unit types
81
_USER_STORAGE_TYPE = {
82
  constants.ST_FILE: "file",
83
  constants.ST_LVM_PV: "lvm-pv",
84
  constants.ST_LVM_VG: "lvm-vg",
85
  }
86

    
87
_STORAGE_TYPE_OPT = \
88
  cli_option("-t", "--storage-type",
89
             dest="user_storage_type",
90
             choices=_USER_STORAGE_TYPE.keys(),
91
             default=None,
92
             metavar="STORAGE_TYPE",
93
             help=("Storage type (%s)" %
94
                   utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
95

    
96
_REPAIRABLE_STORAGE_TYPES = \
97
  [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
98
   if constants.SO_FIX_CONSISTENCY in so]
99

    
100
_MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
101

    
102

    
103
_OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
104
                              constants.OOB_POWER_CYCLE])
105

    
106

    
107
NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
108
                              action="store_false", dest="node_setup",
109
                              help=("Do not make initial SSH setup on remote"
110
                                    " node (needs to be done manually)"))
111

    
112
IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
113
                               action="store_true", dest="ignore_status",
114
                               help=("Ignore the Node(s) offline status"
115
                                     " (potentially DANGEROUS)"))
116

    
117

    
118
def ConvertStorageType(user_storage_type):
119
  """Converts a user storage type to its internal name.
120

121
  """
122
  try:
123
    return _USER_STORAGE_TYPE[user_storage_type]
124
  except KeyError:
125
    raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
126
                               errors.ECODE_INVAL)
127

    
128

    
129
def _RunSetupSSH(options, nodes):
130
  """Wrapper around utils.RunCmd to call setup-ssh
131

132
  @param options: The command line options
133
  @param nodes: The nodes to setup
134

135
  """
136
  cmd = [constants.SETUP_SSH]
137

    
138
  # Pass --debug|--verbose to the external script if set on our invocation
139
  # --debug overrides --verbose
140
  if options.debug:
141
    cmd.append("--debug")
142
  elif options.verbose:
143
    cmd.append("--verbose")
144
  if not options.ssh_key_check:
145
    cmd.append("--no-ssh-key-check")
146
  if options.force_join:
147
    cmd.append("--force-join")
148

    
149
  cmd.extend(nodes)
150

    
151
  result = utils.RunCmd(cmd, interactive=True)
152

    
153
  if result.failed:
154
    errmsg = ("Command '%s' failed with exit code %s; output %r" %
155
              (result.cmd, result.exit_code, result.output))
156
    raise errors.OpExecError(errmsg)
157

    
158

    
159
@UsesRPC
160
def AddNode(opts, args):
161
  """Add a node to the cluster.
162

163
  @param opts: the command line options selected by the user
164
  @type args: list
165
  @param args: should contain only one element, the new node name
166
  @rtype: int
167
  @return: the desired exit code
168

169
  """
170
  cl = GetClient()
171
  node = netutils.GetHostname(name=args[0]).name
172
  readd = opts.readd
173

    
174
  try:
175
    output = cl.QueryNodes(names=[node], fields=['name', 'sip', 'master'],
176
                           use_locking=False)
177
    node_exists, sip, is_master = output[0]
178
  except (errors.OpPrereqError, errors.OpExecError):
179
    node_exists = ""
180
    sip = None
181

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

    
197
  # read the cluster name from the master
198
  output = cl.QueryConfigValues(['cluster_name'])
199
  cluster_name = output[0]
200

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

    
209
  if opts.node_setup:
210
    _RunSetupSSH(opts, [node])
211

    
212
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
213

    
214
  op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
215
                         readd=opts.readd, group=opts.nodegroup,
216
                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
217
                         master_capable=opts.master_capable)
218
  SubmitOpCode(op, opts=opts)
219

    
220

    
221
def ListNodes(opts, args):
222
  """List nodes and their properties.
223

224
  @param opts: the command line options selected by the user
225
  @type args: list
226
  @param args: nodes to list, or empty for all
227
  @rtype: int
228
  @return: the desired exit code
229

230
  """
231
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
232

    
233
  fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
234
                              (",".join, False))
235

    
236
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
237
                     opts.separator, not opts.no_headers,
238
                     format_override=fmtoverride, verbose=opts.verbose,
239
                     force_filter=opts.force_filter)
240

    
241

    
242
def ListNodeFields(opts, args):
243
  """List node fields.
244

245
  @param opts: the command line options selected by the user
246
  @type args: list
247
  @param args: fields to list, or empty for all
248
  @rtype: int
249
  @return: the desired exit code
250

251
  """
252
  return GenericListFields(constants.QR_NODE, args, opts.separator,
253
                           not opts.no_headers)
254

    
255

    
256
def EvacuateNode(opts, args):
257
  """Relocate all secondary instance from a node.
258

259
  @param opts: the command line options selected by the user
260
  @type args: list
261
  @param args: should be an empty list
262
  @rtype: int
263
  @return: the desired exit code
264

265
  """
266
  cl = GetClient()
267
  force = opts.force
268

    
269
  dst_node = opts.dst_node
270
  iallocator = opts.iallocator
271

    
272
  op = opcodes.OpNodeEvacStrategy(nodes=args,
273
                                  iallocator=iallocator,
274
                                  remote_node=dst_node)
275

    
276
  result = SubmitOpCode(op, cl=cl, opts=opts)
277
  if not result:
278
    # no instances to migrate
279
    ToStderr("No secondary instances on node(s) %s, exiting.",
280
             utils.CommaJoin(args))
281
    return constants.EXIT_SUCCESS
282

    
283
  if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
284
                               (",".join("'%s'" % name[0] for name in result),
285
                               utils.CommaJoin(args))):
286
    return constants.EXIT_CONFIRMATION
287

    
288
  jex = JobExecutor(cl=cl, opts=opts)
289
  for row in result:
290
    iname = row[0]
291
    node = row[1]
292
    ToStdout("Will relocate instance %s to node %s", iname, node)
293
    op = opcodes.OpInstanceReplaceDisks(instance_name=iname,
294
                                        remote_node=node, disks=[],
295
                                        mode=constants.REPLACE_DISK_CHG,
296
                                        early_release=opts.early_release)
297
    jex.QueueJob(iname, op)
298
  results = jex.GetResults()
299
  bad_cnt = len([row for row in results if not row[0]])
300
  if bad_cnt == 0:
301
    ToStdout("All %d instance(s) failed over successfully.", len(results))
302
    rcode = constants.EXIT_SUCCESS
303
  else:
304
    ToStdout("There were errors during the failover:\n"
305
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
306
    rcode = constants.EXIT_FAILURE
307
  return rcode
308

    
309

    
310
def FailoverNode(opts, args):
311
  """Failover all primary instance on a node.
312

313
  @param opts: the command line options selected by the user
314
  @type args: list
315
  @param args: should be an empty list
316
  @rtype: int
317
  @return: the desired exit code
318

319
  """
320
  cl = GetClient()
321
  force = opts.force
322
  selected_fields = ["name", "pinst_list"]
323

    
324
  # these fields are static data anyway, so it doesn't matter, but
325
  # locking=True should be safer
326
  result = cl.QueryNodes(names=args, fields=selected_fields,
327
                         use_locking=False)
328
  node, pinst = result[0]
329

    
330
  if not pinst:
331
    ToStderr("No primary instances on node %s, exiting.", node)
332
    return 0
333

    
334
  pinst = utils.NiceSort(pinst)
335

    
336
  retcode = 0
337

    
338
  if not force and not AskUser("Fail over instance(s) %s?" %
339
                               (",".join("'%s'" % name for name in pinst))):
340
    return 2
341

    
342
  jex = JobExecutor(cl=cl, opts=opts)
343
  for iname in pinst:
344
    op = opcodes.OpInstanceFailover(instance_name=iname,
345
                                    ignore_consistency=opts.ignore_consistency,
346
                                    iallocator=opts.iallocator)
347
    jex.QueueJob(iname, op)
348
  results = jex.GetResults()
349
  bad_cnt = len([row for row in results if not row[0]])
350
  if bad_cnt == 0:
351
    ToStdout("All %d instance(s) failed over successfully.", len(results))
352
  else:
353
    ToStdout("There were errors during the failover:\n"
354
             "%d error(s) out of %d instance(s).", bad_cnt, len(results))
355
  return retcode
356

    
357

    
358
def MigrateNode(opts, args):
359
  """Migrate all primary instance on a node.
360

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

    
366
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
367
  ((node, pinst), ) = result
368

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

    
373
  pinst = utils.NiceSort(pinst)
374

    
375
  if not (force or
376
          AskUser("Migrate instance(s) %s?" %
377
                  utils.CommaJoin(utils.NiceSort(pinst)))):
378
    return constants.EXIT_CONFIRMATION
379

    
380
  # this should be removed once --non-live is deprecated
381
  if not opts.live and opts.migration_mode is not None:
382
    raise errors.OpPrereqError("Only one of the --non-live and "
383
                               "--migration-mode options can be passed",
384
                               errors.ECODE_INVAL)
385
  if not opts.live: # --non-live passed
386
    mode = constants.HT_MIGRATION_NONLIVE
387
  else:
388
    mode = opts.migration_mode
389

    
390
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
391
                             iallocator=opts.iallocator,
392
                             target_node=opts.dst_node)
393

    
394
  result = SubmitOpCode(op, cl=cl, opts=opts)
395

    
396
  # Keep track of submitted jobs
397
  jex = JobExecutor(cl=cl, opts=opts)
398

    
399
  for (status, job_id) in result[constants.JOB_IDS_KEY]:
400
    jex.AddJobId(None, status, job_id)
401

    
402
  results = jex.GetResults()
403
  bad_cnt = len([row for row in results if not row[0]])
404
  if bad_cnt == 0:
405
    ToStdout("All instances migrated successfully.")
406
    rcode = constants.EXIT_SUCCESS
407
  else:
408
    ToStdout("There were %s errors during the node migration.", bad_cnt)
409
    rcode = constants.EXIT_FAILURE
410

    
411
  return rcode
412

    
413

    
414
def ShowNodeConfig(opts, args):
415
  """Show node information.
416

417
  @param opts: the command line options selected by the user
418
  @type args: list
419
  @param args: should either be an empty list, in which case
420
      we show information about all nodes, or should contain
421
      a list of nodes to be queried for information
422
  @rtype: int
423
  @return: the desired exit code
424

425
  """
426
  cl = GetClient()
427
  result = cl.QueryNodes(fields=["name", "pip", "sip",
428
                                 "pinst_list", "sinst_list",
429
                                 "master_candidate", "drained", "offline",
430
                                 "master_capable", "vm_capable", "powered",
431
                                 "ndparams", "custom_ndparams"],
432
                         names=args, use_locking=False)
433

    
434
  for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
435
       master_capable, vm_capable, powered, ndparams,
436
       ndparams_custom) in result:
437
    ToStdout("Node name: %s", name)
438
    ToStdout("  primary ip: %s", primary_ip)
439
    ToStdout("  secondary ip: %s", secondary_ip)
440
    ToStdout("  master candidate: %s", is_mc)
441
    ToStdout("  drained: %s", drained)
442
    ToStdout("  offline: %s", offline)
443
    if powered is not None:
444
      ToStdout("  powered: %s", powered)
445
    ToStdout("  master_capable: %s", master_capable)
446
    ToStdout("  vm_capable: %s", vm_capable)
447
    if vm_capable:
448
      if pinst:
449
        ToStdout("  primary for instances:")
450
        for iname in utils.NiceSort(pinst):
451
          ToStdout("    - %s", iname)
452
      else:
453
        ToStdout("  primary for no instances")
454
      if sinst:
455
        ToStdout("  secondary for instances:")
456
        for iname in utils.NiceSort(sinst):
457
          ToStdout("    - %s", iname)
458
      else:
459
        ToStdout("  secondary for no instances")
460
    ToStdout("  node parameters:")
461
    buf = StringIO()
462
    FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
463
    ToStdout(buf.getvalue().rstrip("\n"))
464

    
465
  return 0
466

    
467

    
468
def RemoveNode(opts, args):
469
  """Remove a node from the cluster.
470

471
  @param opts: the command line options selected by the user
472
  @type args: list
473
  @param args: should contain only one element, the name of
474
      the node to be removed
475
  @rtype: int
476
  @return: the desired exit code
477

478
  """
479
  op = opcodes.OpNodeRemove(node_name=args[0])
480
  SubmitOpCode(op, opts=opts)
481
  return 0
482

    
483

    
484
def PowercycleNode(opts, args):
485
  """Remove a node from the cluster.
486

487
  @param opts: the command line options selected by the user
488
  @type args: list
489
  @param args: should contain only one element, the name of
490
      the node to be removed
491
  @rtype: int
492
  @return: the desired exit code
493

494
  """
495
  node = args[0]
496
  if (not opts.confirm and
497
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
498
    return 2
499

    
500
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
501
  result = SubmitOpCode(op, opts=opts)
502
  if result:
503
    ToStderr(result)
504
  return 0
505

    
506

    
507
def PowerNode(opts, args):
508
  """Change/ask power state of a node.
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
  command = args.pop(0)
519

    
520
  if opts.no_headers:
521
    headers = None
522
  else:
523
    headers = {"node": "Node", "status": "Status"}
524

    
525
  if command not in _LIST_POWER_COMMANDS:
526
    ToStderr("power subcommand %s not supported." % command)
527
    return constants.EXIT_FAILURE
528

    
529
  oob_command = "power-%s" % command
530

    
531
  if oob_command in _OOB_COMMAND_ASK:
532
    if not args:
533
      ToStderr("Please provide at least one node for this command")
534
      return constants.EXIT_FAILURE
535
    elif not opts.force and not ConfirmOperation(args, "nodes",
536
                                                 "power %s" % command):
537
      return constants.EXIT_FAILURE
538
    assert len(args) > 0
539

    
540
  opcodelist = []
541
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
542
    # TODO: This is a little ugly as we can't catch and revert
543
    for node in args:
544
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
545
                                                auto_promote=opts.auto_promote))
546

    
547
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
548
                                         command=oob_command,
549
                                         ignore_status=opts.ignore_status,
550
                                         timeout=opts.oob_timeout,
551
                                         power_delay=opts.power_delay))
552

    
553
  cli.SetGenericOpcodeOpts(opcodelist, opts)
554

    
555
  job_id = cli.SendJob(opcodelist)
556

    
557
  # We just want the OOB Opcode status
558
  # If it fails PollJob gives us the error message in it
559
  result = cli.PollJob(job_id)[-1]
560

    
561
  errs = 0
562
  data = []
563
  for node_result in result:
564
    (node_tuple, data_tuple) = node_result
565
    (_, node_name) = node_tuple
566
    (data_status, data_node) = data_tuple
567
    if data_status == constants.RS_NORMAL:
568
      if oob_command == constants.OOB_POWER_STATUS:
569
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
570
          text = "powered"
571
        else:
572
          text = "unpowered"
573
        data.append([node_name, text])
574
      else:
575
        # We don't expect data here, so we just say, it was successfully invoked
576
        data.append([node_name, "invoked"])
577
    else:
578
      errs += 1
579
      data.append([node_name, cli.FormatResultError(data_status, True)])
580

    
581
  data = GenerateTable(separator=opts.separator, headers=headers,
582
                       fields=["node", "status"], data=data)
583

    
584
  for line in data:
585
    ToStdout(line)
586

    
587
  if errs:
588
    return constants.EXIT_FAILURE
589
  else:
590
    return constants.EXIT_SUCCESS
591

    
592

    
593
def Health(opts, args):
594
  """Show health of a node using OOB.
595

596
  @param opts: the command line options selected by the user
597
  @type args: list
598
  @param args: should contain only one element, the name of
599
      the node to be removed
600
  @rtype: int
601
  @return: the desired exit code
602

603
  """
604
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
605
                            timeout=opts.oob_timeout)
606
  result = SubmitOpCode(op, opts=opts)
607

    
608
  if opts.no_headers:
609
    headers = None
610
  else:
611
    headers = {"node": "Node", "status": "Status"}
612

    
613
  errs = 0
614
  data = []
615
  for node_result in result:
616
    (node_tuple, data_tuple) = node_result
617
    (_, node_name) = node_tuple
618
    (data_status, data_node) = data_tuple
619
    if data_status == constants.RS_NORMAL:
620
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
621
      for item, status in data_node[1:]:
622
        data.append(["", "%s=%s" % (item, status)])
623
    else:
624
      errs += 1
625
      data.append([node_name, cli.FormatResultError(data_status, True)])
626

    
627
  data = GenerateTable(separator=opts.separator, headers=headers,
628
                       fields=["node", "status"], data=data)
629

    
630
  for line in data:
631
    ToStdout(line)
632

    
633
  if errs:
634
    return constants.EXIT_FAILURE
635
  else:
636
    return constants.EXIT_SUCCESS
637

    
638

    
639
def ListVolumes(opts, args):
640
  """List logical volumes on node(s).
641

642
  @param opts: the command line options selected by the user
643
  @type args: list
644
  @param args: should either be an empty list, in which case
645
      we list data for all nodes, or contain a list of nodes
646
      to display data only for those
647
  @rtype: int
648
  @return: the desired exit code
649

650
  """
651
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
652

    
653
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
654
  output = SubmitOpCode(op, opts=opts)
655

    
656
  if not opts.no_headers:
657
    headers = {"node": "Node", "phys": "PhysDev",
658
               "vg": "VG", "name": "Name",
659
               "size": "Size", "instance": "Instance"}
660
  else:
661
    headers = None
662

    
663
  unitfields = ["size"]
664

    
665
  numfields = ["size"]
666

    
667
  data = GenerateTable(separator=opts.separator, headers=headers,
668
                       fields=selected_fields, unitfields=unitfields,
669
                       numfields=numfields, data=output, units=opts.units)
670

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

    
674
  return 0
675

    
676

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

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

688
  """
689
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
690
  if opts.user_storage_type is None:
691
    opts.user_storage_type = constants.ST_LVM_PV
692

    
693
  storage_type = ConvertStorageType(opts.user_storage_type)
694

    
695
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
696

    
697
  op = opcodes.OpNodeQueryStorage(nodes=args,
698
                                  storage_type=storage_type,
699
                                  output_fields=selected_fields)
700
  output = SubmitOpCode(op, opts=opts)
701

    
702
  if not opts.no_headers:
703
    headers = {
704
      constants.SF_NODE: "Node",
705
      constants.SF_TYPE: "Type",
706
      constants.SF_NAME: "Name",
707
      constants.SF_SIZE: "Size",
708
      constants.SF_USED: "Used",
709
      constants.SF_FREE: "Free",
710
      constants.SF_ALLOCATABLE: "Allocatable",
711
      }
712
  else:
713
    headers = None
714

    
715
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
716
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
717

    
718
  # change raw values to nicer strings
719
  for row in output:
720
    for idx, field in enumerate(selected_fields):
721
      val = row[idx]
722
      if field == constants.SF_ALLOCATABLE:
723
        if val:
724
          val = "Y"
725
        else:
726
          val = "N"
727
      row[idx] = str(val)
728

    
729
  data = GenerateTable(separator=opts.separator, headers=headers,
730
                       fields=selected_fields, unitfields=unitfields,
731
                       numfields=numfields, data=output, units=opts.units)
732

    
733
  for line in data:
734
    ToStdout(line)
735

    
736
  return 0
737

    
738

    
739
def ModifyStorage(opts, args):
740
  """Modify storage volume on a node.
741

742
  @param opts: the command line options selected by the user
743
  @type args: list
744
  @param args: should contain 3 items: node name, storage type and volume name
745
  @rtype: int
746
  @return: the desired exit code
747

748
  """
749
  (node_name, user_storage_type, volume_name) = args
750

    
751
  storage_type = ConvertStorageType(user_storage_type)
752

    
753
  changes = {}
754

    
755
  if opts.allocatable is not None:
756
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
757

    
758
  if changes:
759
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
760
                                     storage_type=storage_type,
761
                                     name=volume_name,
762
                                     changes=changes)
763
    SubmitOpCode(op, opts=opts)
764
  else:
765
    ToStderr("No changes to perform, exiting.")
766

    
767

    
768
def RepairStorage(opts, args):
769
  """Repairs a storage volume on a node.
770

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

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

    
780
  storage_type = ConvertStorageType(user_storage_type)
781

    
782
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
783
                                   storage_type=storage_type,
784
                                   name=volume_name,
785
                                   ignore_consistency=opts.ignore_consistency)
786
  SubmitOpCode(op, opts=opts)
787

    
788

    
789
def SetNodeParams(opts, args):
790
  """Modifies a node.
791

792
  @param opts: the command line options selected by the user
793
  @type args: list
794
  @param args: should contain only one element, the node name
795
  @rtype: int
796
  @return: the desired exit code
797

798
  """
799
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
800
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
801
                 opts.ndparams]
802
  if all_changes.count(None) == len(all_changes):
803
    ToStderr("Please give at least one of the parameters.")
804
    return 1
805

    
806
  op = opcodes.OpNodeSetParams(node_name=args[0],
807
                               master_candidate=opts.master_candidate,
808
                               offline=opts.offline,
809
                               drained=opts.drained,
810
                               master_capable=opts.master_capable,
811
                               vm_capable=opts.vm_capable,
812
                               secondary_ip=opts.secondary_ip,
813
                               force=opts.force,
814
                               ndparams=opts.ndparams,
815
                               auto_promote=opts.auto_promote,
816
                               powered=opts.node_powered)
817

    
818
  # even if here we process the result, we allow submit only
819
  result = SubmitOrSend(op, opts)
820

    
821
  if result:
822
    ToStdout("Modified node %s", args[0])
823
    for param, data in result:
824
      ToStdout(" - %-5s -> %s", param, data)
825
  return 0
826

    
827

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

    
939

    
940
def Main():
941
  return GenericMain(commands, override={"tag_type": constants.TAG_NODE})