Statistics
| Branch: | Tag: | Revision:

root / lib / client / gnt_node.py @ fed67843

History | View | Annotate | Download (28.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
OOB_TIMEOUT_OPT = cli_option("--oob-timeout", dest="oob_timeout", type="int",
118
                         default=constants.OOB_TIMEOUT,
119
                         help="Maximum time to wait for out-of-band helper")
120

    
121
def ConvertStorageType(user_storage_type):
122
  """Converts a user storage type to its internal name.
123

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

    
131

    
132
def _RunSetupSSH(options, nodes):
133
  """Wrapper around utils.RunCmd to call setup-ssh
134

135
  @param options: The command line options
136
  @param nodes: The nodes to setup
137

138
  """
139
  cmd = [constants.SETUP_SSH]
140

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

    
150
  cmd.extend(nodes)
151

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

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

    
159

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

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

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

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

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

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

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

    
207
  if opts.node_setup:
208
    _RunSetupSSH(opts, [node])
209

    
210
  bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
211

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

    
218

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

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

228
  """
229
  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
230

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

    
234
  return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
235
                     opts.separator, not opts.no_headers,
236
                     format_override=fmtoverride)
237

    
238

    
239
def ListNodeFields(opts, args):
240
  """List node fields.
241

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

248
  """
249
  return GenericListFields(constants.QR_NODE, args, opts.separator,
250
                           not opts.no_headers)
251

    
252

    
253
def EvacuateNode(opts, args):
254
  """Relocate all secondary instance from a node.
255

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

262
  """
263
  cl = GetClient()
264
  force = opts.force
265

    
266
  dst_node = opts.dst_node
267
  iallocator = opts.iallocator
268

    
269
  op = opcodes.OpNodeEvacStrategy(nodes=args,
270
                                  iallocator=iallocator,
271
                                  remote_node=dst_node)
272

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

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

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

    
306

    
307
def FailoverNode(opts, args):
308
  """Failover all primary instance on a node.
309

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

316
  """
317
  cl = GetClient()
318
  force = opts.force
319
  selected_fields = ["name", "pinst_list"]
320

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

    
327
  if not pinst:
328
    ToStderr("No primary instances on node %s, exiting.", node)
329
    return 0
330

    
331
  pinst = utils.NiceSort(pinst)
332

    
333
  retcode = 0
334

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

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

    
353

    
354
def MigrateNode(opts, args):
355
  """Migrate all primary instance on a node.
356

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

    
362
  result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
363
  node, pinst = result[0]
364

    
365
  if not pinst:
366
    ToStdout("No primary instances on node %s, exiting." % node)
367
    return 0
368

    
369
  pinst = utils.NiceSort(pinst)
370

    
371
  if not force and not AskUser("Migrate instance(s) %s?" %
372
                               (",".join("'%s'" % name for name in pinst))):
373
    return 2
374

    
375
  # this should be removed once --non-live is deprecated
376
  if not opts.live and opts.migration_mode is not None:
377
    raise errors.OpPrereqError("Only one of the --non-live and "
378
                               "--migration-mode options can be passed",
379
                               errors.ECODE_INVAL)
380
  if not opts.live: # --non-live passed
381
    mode = constants.HT_MIGRATION_NONLIVE
382
  else:
383
    mode = opts.migration_mode
384
  op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode)
385
  SubmitOpCode(op, cl=cl, opts=opts)
386

    
387

    
388
def ShowNodeConfig(opts, args):
389
  """Show node information.
390

391
  @param opts: the command line options selected by the user
392
  @type args: list
393
  @param args: should either be an empty list, in which case
394
      we show information about all nodes, or should contain
395
      a list of nodes to be queried for information
396
  @rtype: int
397
  @return: the desired exit code
398

399
  """
400
  cl = GetClient()
401
  result = cl.QueryNodes(fields=["name", "pip", "sip",
402
                                 "pinst_list", "sinst_list",
403
                                 "master_candidate", "drained", "offline",
404
                                 "master_capable", "vm_capable", "powered",
405
                                 "ndparams", "custom_ndparams"],
406
                         names=args, use_locking=False)
407

    
408
  for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
409
       master_capable, vm_capable, powered, ndparams,
410
       ndparams_custom) in result:
411
    ToStdout("Node name: %s", name)
412
    ToStdout("  primary ip: %s", primary_ip)
413
    ToStdout("  secondary ip: %s", secondary_ip)
414
    ToStdout("  master candidate: %s", is_mc)
415
    ToStdout("  drained: %s", drained)
416
    ToStdout("  offline: %s", offline)
417
    if powered is not None:
418
      ToStdout("  powered: %s", powered)
419
    ToStdout("  master_capable: %s", master_capable)
420
    ToStdout("  vm_capable: %s", vm_capable)
421
    if vm_capable:
422
      if pinst:
423
        ToStdout("  primary for instances:")
424
        for iname in utils.NiceSort(pinst):
425
          ToStdout("    - %s", iname)
426
      else:
427
        ToStdout("  primary for no instances")
428
      if sinst:
429
        ToStdout("  secondary for instances:")
430
        for iname in utils.NiceSort(sinst):
431
          ToStdout("    - %s", iname)
432
      else:
433
        ToStdout("  secondary for no instances")
434
    ToStdout("  node parameters:")
435
    buf = StringIO()
436
    FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
437
    ToStdout(buf.getvalue().rstrip("\n"))
438

    
439
  return 0
440

    
441

    
442
def RemoveNode(opts, args):
443
  """Remove a node from the cluster.
444

445
  @param opts: the command line options selected by the user
446
  @type args: list
447
  @param args: should contain only one element, the name of
448
      the node to be removed
449
  @rtype: int
450
  @return: the desired exit code
451

452
  """
453
  op = opcodes.OpNodeRemove(node_name=args[0])
454
  SubmitOpCode(op, opts=opts)
455
  return 0
456

    
457

    
458
def PowercycleNode(opts, args):
459
  """Remove a node from the cluster.
460

461
  @param opts: the command line options selected by the user
462
  @type args: list
463
  @param args: should contain only one element, the name of
464
      the node to be removed
465
  @rtype: int
466
  @return: the desired exit code
467

468
  """
469
  node = args[0]
470
  if (not opts.confirm and
471
      not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
472
    return 2
473

    
474
  op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
475
  result = SubmitOpCode(op, opts=opts)
476
  if result:
477
    ToStderr(result)
478
  return 0
479

    
480

    
481
def PowerNode(opts, args):
482
  """Change/ask power state of a node.
483

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

491
  """
492
  command = args.pop(0)
493

    
494
  if opts.no_headers:
495
    headers = None
496
  else:
497
    headers = {"node": "Node", "status": "Status"}
498

    
499
  if command not in _LIST_POWER_COMMANDS:
500
    ToStderr("power subcommand %s not supported." % command)
501
    return constants.EXIT_FAILURE
502

    
503
  oob_command = "power-%s" % command
504

    
505
  if oob_command in _OOB_COMMAND_ASK:
506
    if not args:
507
      ToStderr("Please provide at least one node for this command")
508
      return constants.EXIT_FAILURE
509
    elif not opts.force and not ConfirmOperation(args, "nodes",
510
                                                 "power %s" % command):
511
      return constants.EXIT_FAILURE
512
    assert len(args) > 0
513

    
514
  opcodelist = []
515
  if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
516
    # TODO: This is a little ugly as we can't catch and revert
517
    for node in args:
518
      opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
519
                                                auto_promote=opts.auto_promote))
520

    
521
  opcodelist.append(opcodes.OpOobCommand(node_names=args,
522
                                         command=oob_command,
523
                                         ignore_status=opts.ignore_status,
524
                                         timeout=opts.oob_timeout))
525

    
526
  cli.SetGenericOpcodeOpts(opcodelist, opts)
527

    
528
  job_id = cli.SendJob(opcodelist)
529

    
530
  # We just want the OOB Opcode status
531
  # If it fails PollJob gives us the error message in it
532
  result = cli.PollJob(job_id)[-1]
533

    
534
  errs = 0
535
  data = []
536
  for node_result in result:
537
    (node_tuple, data_tuple) = node_result
538
    (_, node_name) = node_tuple
539
    (data_status, data_node) = data_tuple
540
    if data_status == constants.RS_NORMAL:
541
      if oob_command == constants.OOB_POWER_STATUS:
542
        if data_node[constants.OOB_POWER_STATUS_POWERED]:
543
          text = "powered"
544
        else:
545
          text = "unpowered"
546
        data.append([node_name, text])
547
      else:
548
        # We don't expect data here, so we just say, it was successfully invoked
549
        data.append([node_name, "invoked"])
550
    else:
551
      errs += 1
552
      data.append([node_name, cli.FormatResultError(data_status)])
553

    
554
  data = GenerateTable(separator=opts.separator, headers=headers,
555
                       fields=["node", "status"], data=data)
556

    
557
  for line in data:
558
    ToStdout(line)
559

    
560
  if errs:
561
    return constants.EXIT_FAILURE
562
  else:
563
    return constants.EXIT_SUCCESS
564

    
565

    
566
def Health(opts, args):
567
  """Show health of a node using OOB.
568

569
  @param opts: the command line options selected by the user
570
  @type args: list
571
  @param args: should contain only one element, the name of
572
      the node to be removed
573
  @rtype: int
574
  @return: the desired exit code
575

576
  """
577
  op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
578
                            timeout=opts.oob_timeout)
579
  result = SubmitOpCode(op, opts=opts)
580

    
581
  if opts.no_headers:
582
    headers = None
583
  else:
584
    headers = {"node": "Node", "status": "Status"}
585

    
586
  errs = 0
587
  data = []
588
  for node_result in result:
589
    (node_tuple, data_tuple) = node_result
590
    (_, node_name) = node_tuple
591
    (data_status, data_node) = data_tuple
592
    if data_status == constants.RS_NORMAL:
593
      data.append([node_name, "%s=%s" % tuple(data_node[0])])
594
      for item, status in data_node[1:]:
595
        data.append(["", "%s=%s" % (item, status)])
596
    else:
597
      errs += 1
598
      data.append([node_name, cli.FormatResultError(data_status)])
599

    
600
  data = GenerateTable(separator=opts.separator, headers=headers,
601
                       fields=["node", "status"], data=data)
602

    
603
  for line in data:
604
    ToStdout(line)
605

    
606
  if errs:
607
    return constants.EXIT_FAILURE
608
  else:
609
    return constants.EXIT_SUCCESS
610

    
611

    
612
def ListVolumes(opts, args):
613
  """List logical volumes on node(s).
614

615
  @param opts: the command line options selected by the user
616
  @type args: list
617
  @param args: should either be an empty list, in which case
618
      we list data for all nodes, or contain a list of nodes
619
      to display data only for those
620
  @rtype: int
621
  @return: the desired exit code
622

623
  """
624
  selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
625

    
626
  op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
627
  output = SubmitOpCode(op, opts=opts)
628

    
629
  if not opts.no_headers:
630
    headers = {"node": "Node", "phys": "PhysDev",
631
               "vg": "VG", "name": "Name",
632
               "size": "Size", "instance": "Instance"}
633
  else:
634
    headers = None
635

    
636
  unitfields = ["size"]
637

    
638
  numfields = ["size"]
639

    
640
  data = GenerateTable(separator=opts.separator, headers=headers,
641
                       fields=selected_fields, unitfields=unitfields,
642
                       numfields=numfields, data=output, units=opts.units)
643

    
644
  for line in data:
645
    ToStdout(line)
646

    
647
  return 0
648

    
649

    
650
def ListStorage(opts, args):
651
  """List physical volumes on node(s).
652

653
  @param opts: the command line options selected by the user
654
  @type args: list
655
  @param args: should either be an empty list, in which case
656
      we list data for all nodes, or contain a list of nodes
657
      to display data only for those
658
  @rtype: int
659
  @return: the desired exit code
660

661
  """
662
  # TODO: Default to ST_FILE if LVM is disabled on the cluster
663
  if opts.user_storage_type is None:
664
    opts.user_storage_type = constants.ST_LVM_PV
665

    
666
  storage_type = ConvertStorageType(opts.user_storage_type)
667

    
668
  selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
669

    
670
  op = opcodes.OpNodeQueryStorage(nodes=args,
671
                                  storage_type=storage_type,
672
                                  output_fields=selected_fields)
673
  output = SubmitOpCode(op, opts=opts)
674

    
675
  if not opts.no_headers:
676
    headers = {
677
      constants.SF_NODE: "Node",
678
      constants.SF_TYPE: "Type",
679
      constants.SF_NAME: "Name",
680
      constants.SF_SIZE: "Size",
681
      constants.SF_USED: "Used",
682
      constants.SF_FREE: "Free",
683
      constants.SF_ALLOCATABLE: "Allocatable",
684
      }
685
  else:
686
    headers = None
687

    
688
  unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
689
  numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
690

    
691
  # change raw values to nicer strings
692
  for row in output:
693
    for idx, field in enumerate(selected_fields):
694
      val = row[idx]
695
      if field == constants.SF_ALLOCATABLE:
696
        if val:
697
          val = "Y"
698
        else:
699
          val = "N"
700
      row[idx] = str(val)
701

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

    
706
  for line in data:
707
    ToStdout(line)
708

    
709
  return 0
710

    
711

    
712
def ModifyStorage(opts, args):
713
  """Modify storage volume on a node.
714

715
  @param opts: the command line options selected by the user
716
  @type args: list
717
  @param args: should contain 3 items: node name, storage type and volume name
718
  @rtype: int
719
  @return: the desired exit code
720

721
  """
722
  (node_name, user_storage_type, volume_name) = args
723

    
724
  storage_type = ConvertStorageType(user_storage_type)
725

    
726
  changes = {}
727

    
728
  if opts.allocatable is not None:
729
    changes[constants.SF_ALLOCATABLE] = opts.allocatable
730

    
731
  if changes:
732
    op = opcodes.OpNodeModifyStorage(node_name=node_name,
733
                                     storage_type=storage_type,
734
                                     name=volume_name,
735
                                     changes=changes)
736
    SubmitOpCode(op, opts=opts)
737
  else:
738
    ToStderr("No changes to perform, exiting.")
739

    
740

    
741
def RepairStorage(opts, args):
742
  """Repairs a storage volume on a node.
743

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

750
  """
751
  (node_name, user_storage_type, volume_name) = args
752

    
753
  storage_type = ConvertStorageType(user_storage_type)
754

    
755
  op = opcodes.OpRepairNodeStorage(node_name=node_name,
756
                                   storage_type=storage_type,
757
                                   name=volume_name,
758
                                   ignore_consistency=opts.ignore_consistency)
759
  SubmitOpCode(op, opts=opts)
760

    
761

    
762
def SetNodeParams(opts, args):
763
  """Modifies a node.
764

765
  @param opts: the command line options selected by the user
766
  @type args: list
767
  @param args: should contain only one element, the node name
768
  @rtype: int
769
  @return: the desired exit code
770

771
  """
772
  all_changes = [opts.master_candidate, opts.drained, opts.offline,
773
                 opts.master_capable, opts.vm_capable, opts.secondary_ip,
774
                 opts.ndparams]
775
  if all_changes.count(None) == len(all_changes):
776
    ToStderr("Please give at least one of the parameters.")
777
    return 1
778

    
779
  op = opcodes.OpNodeSetParams(node_name=args[0],
780
                               master_candidate=opts.master_candidate,
781
                               offline=opts.offline,
782
                               drained=opts.drained,
783
                               master_capable=opts.master_capable,
784
                               vm_capable=opts.vm_capable,
785
                               secondary_ip=opts.secondary_ip,
786
                               force=opts.force,
787
                               ndparams=opts.ndparams,
788
                               auto_promote=opts.auto_promote,
789
                               powered=opts.node_powered)
790

    
791
  # even if here we process the result, we allow submit only
792
  result = SubmitOrSend(op, opts)
793

    
794
  if result:
795
    ToStdout("Modified node %s", args[0])
796
    for param, data in result:
797
      ToStdout(" - %-5s -> %s", param, data)
798
  return 0
799

    
800

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

    
908

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