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