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