Fix explanation of gnt-node evacuate --primaries-only
[ganeti-local] / lib / client / gnt_node.py
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 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
110                               action="store_false", dest="node_setup",
111                               help=("Do not make initial SSH setup on remote"
112                                     " node (needs to be done manually)"))
113
114 IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
115                                action="store_true", dest="ignore_status",
116                                help=("Ignore the Node(s) offline status"
117                                      " (potentially DANGEROUS)"))
118
119
120 def ConvertStorageType(user_storage_type):
121   """Converts a user storage type to its internal name.
122
123   """
124   try:
125     return _USER_STORAGE_TYPE[user_storage_type]
126   except KeyError:
127     raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
128                                errors.ECODE_INVAL)
129
130
131 def _RunSetupSSH(options, nodes):
132   """Wrapper around utils.RunCmd to call setup-ssh
133
134   @param options: The command line options
135   @param nodes: The nodes to setup
136
137   """
138   cmd = [constants.SETUP_SSH]
139
140   # Pass --debug|--verbose to the external script if set on our invocation
141   # --debug overrides --verbose
142   if options.debug:
143     cmd.append("--debug")
144   elif options.verbose:
145     cmd.append("--verbose")
146   if not options.ssh_key_check:
147     cmd.append("--no-ssh-key-check")
148   if options.force_join:
149     cmd.append("--force-join")
150
151   cmd.extend(nodes)
152
153   result = utils.RunCmd(cmd, interactive=True)
154
155   if result.failed:
156     errmsg = ("Command '%s' failed with exit code %s; output %r" %
157               (result.cmd, result.exit_code, result.output))
158     raise errors.OpExecError(errmsg)
159
160
161 @UsesRPC
162 def AddNode(opts, args):
163   """Add a node to the cluster.
164
165   @param opts: the command line options selected by the user
166   @type args: list
167   @param args: should contain only one element, the new node name
168   @rtype: int
169   @return: the desired exit code
170
171   """
172   cl = GetClient()
173   node = netutils.GetHostname(name=args[0]).name
174   readd = opts.readd
175
176   try:
177     output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
178                            use_locking=False)
179     node_exists, sip, is_master = output[0]
180   except (errors.OpPrereqError, errors.OpExecError):
181     node_exists = ""
182     sip = None
183
184   if readd:
185     if not node_exists:
186       ToStderr("Node %s not in the cluster"
187                " - please retry without '--readd'", node)
188       return 1
189     if is_master:
190       ToStderr("Node %s is the master, cannot readd", node)
191       return 1
192   else:
193     if node_exists:
194       ToStderr("Node %s already in the cluster (as %s)"
195                " - please retry with '--readd'", node, node_exists)
196       return 1
197     sip = opts.secondary_ip
198
199   # read the cluster name from the master
200   output = cl.QueryConfigValues(["cluster_name"])
201   cluster_name = output[0]
202
203   if not readd and opts.node_setup:
204     ToStderr("-- WARNING -- \n"
205              "Performing this operation is going to replace the ssh daemon"
206              " keypair\n"
207              "on the target machine (%s) with the ones of the"
208              " current one\n"
209              "and grant full intra-cluster ssh root access to/from it\n", node)
210
211   if opts.node_setup:
212     _RunSetupSSH(opts, [node])
213
214   bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
215
216   op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
217                          readd=opts.readd, group=opts.nodegroup,
218                          vm_capable=opts.vm_capable, ndparams=opts.ndparams,
219                          master_capable=opts.master_capable)
220   SubmitOpCode(op, opts=opts)
221
222
223 def ListNodes(opts, args):
224   """List nodes and their properties.
225
226   @param opts: the command line options selected by the user
227   @type args: list
228   @param args: nodes to list, or empty for all
229   @rtype: int
230   @return: the desired exit code
231
232   """
233   selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
234
235   fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
236                               (",".join, False))
237
238   return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
239                      opts.separator, not opts.no_headers,
240                      format_override=fmtoverride, verbose=opts.verbose,
241                      force_filter=opts.force_filter)
242
243
244 def ListNodeFields(opts, args):
245   """List node fields.
246
247   @param opts: the command line options selected by the user
248   @type args: list
249   @param args: fields to list, or empty for all
250   @rtype: int
251   @return: the desired exit code
252
253   """
254   return GenericListFields(constants.QR_NODE, args, opts.separator,
255                            not opts.no_headers)
256
257
258 def EvacuateNode(opts, args):
259   """Relocate all secondary instance from a node.
260
261   @param opts: the command line options selected by the user
262   @type args: list
263   @param args: should be an empty list
264   @rtype: int
265   @return: the desired exit code
266
267   """
268   if opts.dst_node is not None:
269     ToStderr("New secondary node given (disabling iallocator), hence evacuating"
270              " secondary instances only.")
271     opts.secondary_only = True
272     opts.primary_only = False
273
274   if opts.secondary_only and opts.primary_only:
275     raise errors.OpPrereqError("Only one of the --primary-only and"
276                                " --secondary-only options can be passed",
277                                errors.ECODE_INVAL)
278   elif opts.primary_only:
279     mode = constants.IALLOCATOR_NEVAC_PRI
280   elif opts.secondary_only:
281     mode = constants.IALLOCATOR_NEVAC_SEC
282   else:
283     mode = constants.IALLOCATOR_NEVAC_ALL
284
285   # Determine affected instances
286   fields = []
287
288   if not opts.secondary_only:
289     fields.append("pinst_list")
290   if not opts.primary_only:
291     fields.append("sinst_list")
292
293   cl = GetClient()
294
295   result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
296   instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
297
298   if not instances:
299     # No instances to evacuate
300     ToStderr("No instances to evacuate on node(s) %s, exiting.",
301              utils.CommaJoin(args))
302     return constants.EXIT_SUCCESS
303
304   if not (opts.force or
305           AskUser("Relocate instance(s) %s from node(s) %s?" %
306                   (utils.CommaJoin(utils.NiceSort(instances)),
307                    utils.CommaJoin(args)))):
308     return constants.EXIT_CONFIRMATION
309
310   # Evacuate node
311   op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
312                               remote_node=opts.dst_node,
313                               iallocator=opts.iallocator,
314                               early_release=opts.early_release)
315   result = SubmitOpCode(op, cl=cl, opts=opts)
316
317   # Keep track of submitted jobs
318   jex = JobExecutor(cl=cl, opts=opts)
319
320   for (status, job_id) in result[constants.JOB_IDS_KEY]:
321     jex.AddJobId(None, status, job_id)
322
323   results = jex.GetResults()
324   bad_cnt = len([row for row in results if not row[0]])
325   if bad_cnt == 0:
326     ToStdout("All instances evacuated successfully.")
327     rcode = constants.EXIT_SUCCESS
328   else:
329     ToStdout("There were %s errors during the evacuation.", bad_cnt)
330     rcode = constants.EXIT_FAILURE
331
332   return rcode
333
334
335 def FailoverNode(opts, args):
336   """Failover all primary instance on a node.
337
338   @param opts: the command line options selected by the user
339   @type args: list
340   @param args: should be an empty list
341   @rtype: int
342   @return: the desired exit code
343
344   """
345   cl = GetClient()
346   force = opts.force
347   selected_fields = ["name", "pinst_list"]
348
349   # these fields are static data anyway, so it doesn't matter, but
350   # locking=True should be safer
351   result = cl.QueryNodes(names=args, fields=selected_fields,
352                          use_locking=False)
353   node, pinst = result[0]
354
355   if not pinst:
356     ToStderr("No primary instances on node %s, exiting.", node)
357     return 0
358
359   pinst = utils.NiceSort(pinst)
360
361   retcode = 0
362
363   if not force and not AskUser("Fail over instance(s) %s?" %
364                                (",".join("'%s'" % name for name in pinst))):
365     return 2
366
367   jex = JobExecutor(cl=cl, opts=opts)
368   for iname in pinst:
369     op = opcodes.OpInstanceFailover(instance_name=iname,
370                                     ignore_consistency=opts.ignore_consistency,
371                                     iallocator=opts.iallocator)
372     jex.QueueJob(iname, op)
373   results = jex.GetResults()
374   bad_cnt = len([row for row in results if not row[0]])
375   if bad_cnt == 0:
376     ToStdout("All %d instance(s) failed over successfully.", len(results))
377   else:
378     ToStdout("There were errors during the failover:\n"
379              "%d error(s) out of %d instance(s).", bad_cnt, len(results))
380   return retcode
381
382
383 def MigrateNode(opts, args):
384   """Migrate all primary instance on a node.
385
386   """
387   cl = GetClient()
388   force = opts.force
389   selected_fields = ["name", "pinst_list"]
390
391   result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
392   ((node, pinst), ) = result
393
394   if not pinst:
395     ToStdout("No primary instances on node %s, exiting." % node)
396     return 0
397
398   pinst = utils.NiceSort(pinst)
399
400   if not (force or
401           AskUser("Migrate instance(s) %s?" %
402                   utils.CommaJoin(utils.NiceSort(pinst)))):
403     return constants.EXIT_CONFIRMATION
404
405   # this should be removed once --non-live is deprecated
406   if not opts.live and opts.migration_mode is not None:
407     raise errors.OpPrereqError("Only one of the --non-live and "
408                                "--migration-mode options can be passed",
409                                errors.ECODE_INVAL)
410   if not opts.live: # --non-live passed
411     mode = constants.HT_MIGRATION_NONLIVE
412   else:
413     mode = opts.migration_mode
414
415   op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
416                              iallocator=opts.iallocator,
417                              target_node=opts.dst_node)
418
419   result = SubmitOpCode(op, cl=cl, opts=opts)
420
421   # Keep track of submitted jobs
422   jex = JobExecutor(cl=cl, opts=opts)
423
424   for (status, job_id) in result[constants.JOB_IDS_KEY]:
425     jex.AddJobId(None, status, job_id)
426
427   results = jex.GetResults()
428   bad_cnt = len([row for row in results if not row[0]])
429   if bad_cnt == 0:
430     ToStdout("All instances migrated successfully.")
431     rcode = constants.EXIT_SUCCESS
432   else:
433     ToStdout("There were %s errors during the node migration.", bad_cnt)
434     rcode = constants.EXIT_FAILURE
435
436   return rcode
437
438
439 def ShowNodeConfig(opts, args):
440   """Show node information.
441
442   @param opts: the command line options selected by the user
443   @type args: list
444   @param args: should either be an empty list, in which case
445       we show information about all nodes, or should contain
446       a list of nodes to be queried for information
447   @rtype: int
448   @return: the desired exit code
449
450   """
451   cl = GetClient()
452   result = cl.QueryNodes(fields=["name", "pip", "sip",
453                                  "pinst_list", "sinst_list",
454                                  "master_candidate", "drained", "offline",
455                                  "master_capable", "vm_capable", "powered",
456                                  "ndparams", "custom_ndparams"],
457                          names=args, use_locking=False)
458
459   for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
460        master_capable, vm_capable, powered, ndparams,
461        ndparams_custom) in result:
462     ToStdout("Node name: %s", name)
463     ToStdout("  primary ip: %s", primary_ip)
464     ToStdout("  secondary ip: %s", secondary_ip)
465     ToStdout("  master candidate: %s", is_mc)
466     ToStdout("  drained: %s", drained)
467     ToStdout("  offline: %s", offline)
468     if powered is not None:
469       ToStdout("  powered: %s", powered)
470     ToStdout("  master_capable: %s", master_capable)
471     ToStdout("  vm_capable: %s", vm_capable)
472     if vm_capable:
473       if pinst:
474         ToStdout("  primary for instances:")
475         for iname in utils.NiceSort(pinst):
476           ToStdout("    - %s", iname)
477       else:
478         ToStdout("  primary for no instances")
479       if sinst:
480         ToStdout("  secondary for instances:")
481         for iname in utils.NiceSort(sinst):
482           ToStdout("    - %s", iname)
483       else:
484         ToStdout("  secondary for no instances")
485     ToStdout("  node parameters:")
486     buf = StringIO()
487     FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
488     ToStdout(buf.getvalue().rstrip("\n"))
489
490   return 0
491
492
493 def RemoveNode(opts, args):
494   """Remove a node from the cluster.
495
496   @param opts: the command line options selected by the user
497   @type args: list
498   @param args: should contain only one element, the name of
499       the node to be removed
500   @rtype: int
501   @return: the desired exit code
502
503   """
504   op = opcodes.OpNodeRemove(node_name=args[0])
505   SubmitOpCode(op, opts=opts)
506   return 0
507
508
509 def PowercycleNode(opts, args):
510   """Remove a node from the cluster.
511
512   @param opts: the command line options selected by the user
513   @type args: list
514   @param args: should contain only one element, the name of
515       the node to be removed
516   @rtype: int
517   @return: the desired exit code
518
519   """
520   node = args[0]
521   if (not opts.confirm and
522       not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
523     return 2
524
525   op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
526   result = SubmitOpCode(op, opts=opts)
527   if result:
528     ToStderr(result)
529   return 0
530
531
532 def PowerNode(opts, args):
533   """Change/ask power state of a node.
534
535   @param opts: the command line options selected by the user
536   @type args: list
537   @param args: should contain only one element, the name of
538       the node to be removed
539   @rtype: int
540   @return: the desired exit code
541
542   """
543   command = args.pop(0)
544
545   if opts.no_headers:
546     headers = None
547   else:
548     headers = {"node": "Node", "status": "Status"}
549
550   if command not in _LIST_POWER_COMMANDS:
551     ToStderr("power subcommand %s not supported." % command)
552     return constants.EXIT_FAILURE
553
554   oob_command = "power-%s" % command
555
556   if oob_command in _OOB_COMMAND_ASK:
557     if not args:
558       ToStderr("Please provide at least one node for this command")
559       return constants.EXIT_FAILURE
560     elif not opts.force and not ConfirmOperation(args, "nodes",
561                                                  "power %s" % command):
562       return constants.EXIT_FAILURE
563     assert len(args) > 0
564
565   opcodelist = []
566   if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
567     # TODO: This is a little ugly as we can't catch and revert
568     for node in args:
569       opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
570                                                 auto_promote=opts.auto_promote))
571
572   opcodelist.append(opcodes.OpOobCommand(node_names=args,
573                                          command=oob_command,
574                                          ignore_status=opts.ignore_status,
575                                          timeout=opts.oob_timeout,
576                                          power_delay=opts.power_delay))
577
578   cli.SetGenericOpcodeOpts(opcodelist, opts)
579
580   job_id = cli.SendJob(opcodelist)
581
582   # We just want the OOB Opcode status
583   # If it fails PollJob gives us the error message in it
584   result = cli.PollJob(job_id)[-1]
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       if oob_command == constants.OOB_POWER_STATUS:
594         if data_node[constants.OOB_POWER_STATUS_POWERED]:
595           text = "powered"
596         else:
597           text = "unpowered"
598         data.append([node_name, text])
599       else:
600         # We don't expect data here, so we just say, it was successfully invoked
601         data.append([node_name, "invoked"])
602     else:
603       errs += 1
604       data.append([node_name, cli.FormatResultError(data_status, True)])
605
606   data = GenerateTable(separator=opts.separator, headers=headers,
607                        fields=["node", "status"], data=data)
608
609   for line in data:
610     ToStdout(line)
611
612   if errs:
613     return constants.EXIT_FAILURE
614   else:
615     return constants.EXIT_SUCCESS
616
617
618 def Health(opts, args):
619   """Show health of a node using OOB.
620
621   @param opts: the command line options selected by the user
622   @type args: list
623   @param args: should contain only one element, the name of
624       the node to be removed
625   @rtype: int
626   @return: the desired exit code
627
628   """
629   op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
630                             timeout=opts.oob_timeout)
631   result = SubmitOpCode(op, opts=opts)
632
633   if opts.no_headers:
634     headers = None
635   else:
636     headers = {"node": "Node", "status": "Status"}
637
638   errs = 0
639   data = []
640   for node_result in result:
641     (node_tuple, data_tuple) = node_result
642     (_, node_name) = node_tuple
643     (data_status, data_node) = data_tuple
644     if data_status == constants.RS_NORMAL:
645       data.append([node_name, "%s=%s" % tuple(data_node[0])])
646       for item, status in data_node[1:]:
647         data.append(["", "%s=%s" % (item, status)])
648     else:
649       errs += 1
650       data.append([node_name, cli.FormatResultError(data_status, True)])
651
652   data = GenerateTable(separator=opts.separator, headers=headers,
653                        fields=["node", "status"], data=data)
654
655   for line in data:
656     ToStdout(line)
657
658   if errs:
659     return constants.EXIT_FAILURE
660   else:
661     return constants.EXIT_SUCCESS
662
663
664 def ListVolumes(opts, args):
665   """List logical volumes on node(s).
666
667   @param opts: the command line options selected by the user
668   @type args: list
669   @param args: should either be an empty list, in which case
670       we list data for all nodes, or contain a list of nodes
671       to display data only for those
672   @rtype: int
673   @return: the desired exit code
674
675   """
676   selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
677
678   op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
679   output = SubmitOpCode(op, opts=opts)
680
681   if not opts.no_headers:
682     headers = {"node": "Node", "phys": "PhysDev",
683                "vg": "VG", "name": "Name",
684                "size": "Size", "instance": "Instance"}
685   else:
686     headers = None
687
688   unitfields = ["size"]
689
690   numfields = ["size"]
691
692   data = GenerateTable(separator=opts.separator, headers=headers,
693                        fields=selected_fields, unitfields=unitfields,
694                        numfields=numfields, data=output, units=opts.units)
695
696   for line in data:
697     ToStdout(line)
698
699   return 0
700
701
702 def ListStorage(opts, args):
703   """List physical volumes on node(s).
704
705   @param opts: the command line options selected by the user
706   @type args: list
707   @param args: should either be an empty list, in which case
708       we list data for all nodes, or contain a list of nodes
709       to display data only for those
710   @rtype: int
711   @return: the desired exit code
712
713   """
714   # TODO: Default to ST_FILE if LVM is disabled on the cluster
715   if opts.user_storage_type is None:
716     opts.user_storage_type = constants.ST_LVM_PV
717
718   storage_type = ConvertStorageType(opts.user_storage_type)
719
720   selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
721
722   op = opcodes.OpNodeQueryStorage(nodes=args,
723                                   storage_type=storage_type,
724                                   output_fields=selected_fields)
725   output = SubmitOpCode(op, opts=opts)
726
727   if not opts.no_headers:
728     headers = {
729       constants.SF_NODE: "Node",
730       constants.SF_TYPE: "Type",
731       constants.SF_NAME: "Name",
732       constants.SF_SIZE: "Size",
733       constants.SF_USED: "Used",
734       constants.SF_FREE: "Free",
735       constants.SF_ALLOCATABLE: "Allocatable",
736       }
737   else:
738     headers = None
739
740   unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
741   numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
742
743   # change raw values to nicer strings
744   for row in output:
745     for idx, field in enumerate(selected_fields):
746       val = row[idx]
747       if field == constants.SF_ALLOCATABLE:
748         if val:
749           val = "Y"
750         else:
751           val = "N"
752       row[idx] = str(val)
753
754   data = GenerateTable(separator=opts.separator, headers=headers,
755                        fields=selected_fields, unitfields=unitfields,
756                        numfields=numfields, data=output, units=opts.units)
757
758   for line in data:
759     ToStdout(line)
760
761   return 0
762
763
764 def ModifyStorage(opts, args):
765   """Modify storage volume on a node.
766
767   @param opts: the command line options selected by the user
768   @type args: list
769   @param args: should contain 3 items: node name, storage type and volume name
770   @rtype: int
771   @return: the desired exit code
772
773   """
774   (node_name, user_storage_type, volume_name) = args
775
776   storage_type = ConvertStorageType(user_storage_type)
777
778   changes = {}
779
780   if opts.allocatable is not None:
781     changes[constants.SF_ALLOCATABLE] = opts.allocatable
782
783   if changes:
784     op = opcodes.OpNodeModifyStorage(node_name=node_name,
785                                      storage_type=storage_type,
786                                      name=volume_name,
787                                      changes=changes)
788     SubmitOpCode(op, opts=opts)
789   else:
790     ToStderr("No changes to perform, exiting.")
791
792
793 def RepairStorage(opts, args):
794   """Repairs a storage volume on a node.
795
796   @param opts: the command line options selected by the user
797   @type args: list
798   @param args: should contain 3 items: node name, storage type and volume name
799   @rtype: int
800   @return: the desired exit code
801
802   """
803   (node_name, user_storage_type, volume_name) = args
804
805   storage_type = ConvertStorageType(user_storage_type)
806
807   op = opcodes.OpRepairNodeStorage(node_name=node_name,
808                                    storage_type=storage_type,
809                                    name=volume_name,
810                                    ignore_consistency=opts.ignore_consistency)
811   SubmitOpCode(op, opts=opts)
812
813
814 def SetNodeParams(opts, args):
815   """Modifies a node.
816
817   @param opts: the command line options selected by the user
818   @type args: list
819   @param args: should contain only one element, the node name
820   @rtype: int
821   @return: the desired exit code
822
823   """
824   all_changes = [opts.master_candidate, opts.drained, opts.offline,
825                  opts.master_capable, opts.vm_capable, opts.secondary_ip,
826                  opts.ndparams]
827   if all_changes.count(None) == len(all_changes):
828     ToStderr("Please give at least one of the parameters.")
829     return 1
830
831   op = opcodes.OpNodeSetParams(node_name=args[0],
832                                master_candidate=opts.master_candidate,
833                                offline=opts.offline,
834                                drained=opts.drained,
835                                master_capable=opts.master_capable,
836                                vm_capable=opts.vm_capable,
837                                secondary_ip=opts.secondary_ip,
838                                force=opts.force,
839                                ndparams=opts.ndparams,
840                                auto_promote=opts.auto_promote,
841                                powered=opts.node_powered)
842
843   # even if here we process the result, we allow submit only
844   result = SubmitOrSend(op, opts)
845
846   if result:
847     ToStdout("Modified node %s", args[0])
848     for param, data in result:
849       ToStdout(" - %-5s -> %s", param, data)
850   return 0
851
852
853 commands = {
854   "add": (
855     AddNode, [ArgHost(min=1, max=1)],
856     [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
857      NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
858      CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT],
859     "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
860     " [--no-node-setup] [--verbose]"
861     " <node_name>",
862     "Add a node to the cluster"),
863   "evacuate": (
864     EvacuateNode, ARGS_ONE_NODE,
865     [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
866      PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT],
867     "[-f] {-I <iallocator> | -n <dst>} <node>",
868     "Relocate the primary and/or secondary instances from a node"),
869   "failover": (
870     FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
871                                   IALLOCATOR_OPT, PRIORITY_OPT],
872     "[-f] <node>",
873     "Stops the primary instances on a node and start them on their"
874     " secondary node (only for instances with drbd disk template)"),
875   "migrate": (
876     MigrateNode, ARGS_ONE_NODE,
877     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
878      IALLOCATOR_OPT, PRIORITY_OPT],
879     "[-f] <node>",
880     "Migrate all the primary instance on a node away from it"
881     " (only for instances of type drbd)"),
882   "info": (
883     ShowNodeConfig, ARGS_MANY_NODES, [],
884     "[<node_name>...]", "Show information about the node(s)"),
885   "list": (
886     ListNodes, ARGS_MANY_NODES,
887     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
888      FORCE_FILTER_OPT],
889     "[nodes...]",
890     "Lists the nodes in the cluster. The available fields can be shown using"
891     " the \"list-fields\" command (see the man page for details)."
892     " The default field list is (in order): %s." %
893     utils.CommaJoin(_LIST_DEF_FIELDS)),
894   "list-fields": (
895     ListNodeFields, [ArgUnknown()],
896     [NOHDR_OPT, SEP_OPT],
897     "[fields...]",
898     "Lists all available fields for nodes"),
899   "modify": (
900     SetNodeParams, ARGS_ONE_NODE,
901     [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
902      CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
903      AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
904      NODE_POWERED_OPT],
905     "<node_name>", "Alters the parameters of a node"),
906   "powercycle": (
907     PowercycleNode, ARGS_ONE_NODE,
908     [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
909     "<node_name>", "Tries to forcefully powercycle a node"),
910   "power": (
911     PowerNode,
912     [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
913      ArgNode()],
914     [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
915      FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
916     "on|off|cycle|status [nodes...]",
917     "Change power state of node by calling out-of-band helper."),
918   "remove": (
919     RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
920     "<node_name>", "Removes a node from the cluster"),
921   "volumes": (
922     ListVolumes, [ArgNode()],
923     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
924     "[<node_name>...]", "List logical volumes on node(s)"),
925   "list-storage": (
926     ListStorage, ARGS_MANY_NODES,
927     [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
928      PRIORITY_OPT],
929     "[<node_name>...]", "List physical volumes on node(s). The available"
930     " fields are (see the man page for details): %s." %
931     (utils.CommaJoin(_LIST_STOR_HEADERS))),
932   "modify-storage": (
933     ModifyStorage,
934     [ArgNode(min=1, max=1),
935      ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
936      ArgFile(min=1, max=1)],
937     [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
938     "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
939   "repair-storage": (
940     RepairStorage,
941     [ArgNode(min=1, max=1),
942      ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
943      ArgFile(min=1, max=1)],
944     [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
945     "<node_name> <storage_type> <name>",
946     "Repairs a storage volume on a node"),
947   "list-tags": (
948     ListTags, ARGS_ONE_NODE, [],
949     "<node_name>", "List the tags of the given node"),
950   "add-tags": (
951     AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
952     "<node_name> tag...", "Add tags to the given node"),
953   "remove-tags": (
954     RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
955     [TAG_SRC_OPT, PRIORITY_OPT],
956     "<node_name> tag...", "Remove tags from the given node"),
957   "health": (
958     Health, ARGS_MANY_NODES,
959     [NOHDR_OPT, SEP_OPT, SUBMIT_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
960     "[<node_name>...]", "List health of node(s) using out-of-band"),
961   }
962
963
964 def Main():
965   return GenericMain(commands, override={"tag_type": constants.TAG_NODE})