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