4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
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.
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.
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
21 """Node related commands"""
23 # pylint: disable-msg=W0401,W0613,W0614,C0103
24 # W0401: Wildcard import ganeti.cli
25 # W0613: Unused argument, since all functions follow the same API
26 # W0614: Unused import %s from wildcard import (since we need cli)
27 # C0103: Invalid name gnt-node
29 from ganeti.cli import *
30 from ganeti import cli
31 from ganeti import bootstrap
32 from ganeti import opcodes
33 from ganeti import utils
34 from ganeti import constants
35 from ganeti import errors
36 from ganeti import netutils
37 from cStringIO import StringIO
40 #: default list of field for L{ListNodes}
42 "name", "dtotal", "dfree",
43 "mtotal", "mnode", "mfree",
44 "pinst_cnt", "sinst_cnt",
48 #: Default field list for L{ListVolumes}
49 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
52 #: default list of field for L{ListStorage}
53 _LIST_STOR_DEF_FIELDS = [
60 constants.SF_ALLOCATABLE,
64 #: default list of power commands
65 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
68 #: headers (and full field list) for L{ListStorage}
69 _LIST_STOR_HEADERS = {
70 constants.SF_NODE: "Node",
71 constants.SF_TYPE: "Type",
72 constants.SF_NAME: "Name",
73 constants.SF_SIZE: "Size",
74 constants.SF_USED: "Used",
75 constants.SF_FREE: "Free",
76 constants.SF_ALLOCATABLE: "Allocatable",
80 #: User-facing storage unit types
81 _USER_STORAGE_TYPE = {
82 constants.ST_FILE: "file",
83 constants.ST_LVM_PV: "lvm-pv",
84 constants.ST_LVM_VG: "lvm-vg",
88 cli_option("-t", "--storage-type",
89 dest="user_storage_type",
90 choices=_USER_STORAGE_TYPE.keys(),
92 metavar="STORAGE_TYPE",
93 help=("Storage type (%s)" %
94 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
96 _REPAIRABLE_STORAGE_TYPES = \
97 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
98 if constants.SO_FIX_CONSISTENCY in so]
100 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
103 _OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
104 constants.OOB_POWER_CYCLE])
107 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
108 action="store_false", dest="node_setup",
109 help=("Do not make initial SSH setup on remote"
110 " node (needs to be done manually)"))
112 IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
113 action="store_true", dest="ignore_status",
114 help=("Ignore the Node(s) offline status"
115 " (potentially DANGEROUS)"))
118 def ConvertStorageType(user_storage_type):
119 """Converts a user storage type to its internal name.
123 return _USER_STORAGE_TYPE[user_storage_type]
125 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
129 def _RunSetupSSH(options, nodes):
130 """Wrapper around utils.RunCmd to call setup-ssh
132 @param options: The command line options
133 @param nodes: The nodes to setup
136 cmd = [constants.SETUP_SSH]
138 # Pass --debug|--verbose to the external script if set on our invocation
139 # --debug overrides --verbose
141 cmd.append("--debug")
142 elif options.verbose:
143 cmd.append("--verbose")
144 if not options.ssh_key_check:
145 cmd.append("--no-ssh-key-check")
146 if options.force_join:
147 cmd.append("--force-join")
151 result = utils.RunCmd(cmd, interactive=True)
154 errmsg = ("Command '%s' failed with exit code %s; output %r" %
155 (result.cmd, result.exit_code, result.output))
156 raise errors.OpExecError(errmsg)
160 def AddNode(opts, args):
161 """Add a node to the cluster.
163 @param opts: the command line options selected by the user
165 @param args: should contain only one element, the new node name
167 @return: the desired exit code
171 node = netutils.GetHostname(name=args[0]).name
175 output = cl.QueryNodes(names=[node], fields=['name', 'sip'],
177 node_exists, sip = output[0]
178 except (errors.OpPrereqError, errors.OpExecError):
184 ToStderr("Node %s not in the cluster"
185 " - please retry without '--readd'", node)
189 ToStderr("Node %s already in the cluster (as %s)"
190 " - please retry with '--readd'", node, node_exists)
192 sip = opts.secondary_ip
194 # read the cluster name from the master
195 output = cl.QueryConfigValues(['cluster_name'])
196 cluster_name = output[0]
198 if not readd and opts.node_setup:
199 ToStderr("-- WARNING -- \n"
200 "Performing this operation is going to replace the ssh daemon"
202 "on the target machine (%s) with the ones of the"
204 "and grant full intra-cluster ssh root access to/from it\n", node)
207 _RunSetupSSH(opts, [node])
209 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
211 op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
212 readd=opts.readd, group=opts.nodegroup,
213 vm_capable=opts.vm_capable, ndparams=opts.ndparams,
214 master_capable=opts.master_capable)
215 SubmitOpCode(op, opts=opts)
218 def ListNodes(opts, args):
219 """List nodes and their properties.
221 @param opts: the command line options selected by the user
223 @param args: nodes to list, or empty for all
225 @return: the desired exit code
228 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
230 fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
233 return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
234 opts.separator, not opts.no_headers,
235 format_override=fmtoverride, verbose=opts.verbose,
236 force_filter=opts.force_filter)
239 def ListNodeFields(opts, args):
242 @param opts: the command line options selected by the user
244 @param args: fields to list, or empty for all
246 @return: the desired exit code
249 return GenericListFields(constants.QR_NODE, args, opts.separator,
253 def EvacuateNode(opts, args):
254 """Relocate all secondary instance from a node.
256 @param opts: the command line options selected by the user
258 @param args: should be an empty list
260 @return: the desired exit code
266 dst_node = opts.dst_node
267 iallocator = opts.iallocator
269 op = opcodes.OpNodeEvacStrategy(nodes=args,
270 iallocator=iallocator,
271 remote_node=dst_node)
273 result = SubmitOpCode(op, cl=cl, opts=opts)
275 # no instances to migrate
276 ToStderr("No secondary instances on node(s) %s, exiting.",
277 utils.CommaJoin(args))
278 return constants.EXIT_SUCCESS
280 if not force and not AskUser("Relocate instance(s) %s from node(s) %s?" %
281 (",".join("'%s'" % name[0] for name in result),
282 utils.CommaJoin(args))):
283 return constants.EXIT_CONFIRMATION
285 jex = JobExecutor(cl=cl, opts=opts)
289 ToStdout("Will relocate instance %s to node %s", iname, node)
290 op = opcodes.OpInstanceReplaceDisks(instance_name=iname,
291 remote_node=node, disks=[],
292 mode=constants.REPLACE_DISK_CHG,
293 early_release=opts.early_release)
294 jex.QueueJob(iname, op)
295 results = jex.GetResults()
296 bad_cnt = len([row for row in results if not row[0]])
298 ToStdout("All %d instance(s) failed over successfully.", len(results))
299 rcode = constants.EXIT_SUCCESS
301 ToStdout("There were errors during the failover:\n"
302 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
303 rcode = constants.EXIT_FAILURE
307 def FailoverNode(opts, args):
308 """Failover all primary instance on a node.
310 @param opts: the command line options selected by the user
312 @param args: should be an empty list
314 @return: the desired exit code
319 selected_fields = ["name", "pinst_list"]
321 # these fields are static data anyway, so it doesn't matter, but
322 # locking=True should be safer
323 result = cl.QueryNodes(names=args, fields=selected_fields,
325 node, pinst = result[0]
328 ToStderr("No primary instances on node %s, exiting.", node)
331 pinst = utils.NiceSort(pinst)
335 if not force and not AskUser("Fail over instance(s) %s?" %
336 (",".join("'%s'" % name for name in pinst))):
339 jex = JobExecutor(cl=cl, opts=opts)
341 op = opcodes.OpInstanceFailover(instance_name=iname,
342 ignore_consistency=opts.ignore_consistency,
343 iallocator=opts.iallocator)
344 jex.QueueJob(iname, op)
345 results = jex.GetResults()
346 bad_cnt = len([row for row in results if not row[0]])
348 ToStdout("All %d instance(s) failed over successfully.", len(results))
350 ToStdout("There were errors during the failover:\n"
351 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
355 def MigrateNode(opts, args):
356 """Migrate all primary instance on a node.
361 selected_fields = ["name", "pinst_list"]
363 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
364 node, pinst = result[0]
367 ToStdout("No primary instances on node %s, exiting." % node)
370 pinst = utils.NiceSort(pinst)
372 if not force and not AskUser("Migrate instance(s) %s?" %
373 (",".join("'%s'" % name for name in pinst))):
376 # this should be removed once --non-live is deprecated
377 if not opts.live and opts.migration_mode is not None:
378 raise errors.OpPrereqError("Only one of the --non-live and "
379 "--migration-mode options can be passed",
381 if not opts.live: # --non-live passed
382 mode = constants.HT_MIGRATION_NONLIVE
384 mode = opts.migration_mode
385 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
386 iallocator=opts.iallocator)
387 SubmitOpCode(op, cl=cl, opts=opts)
390 def ShowNodeConfig(opts, args):
391 """Show node information.
393 @param opts: the command line options selected by the user
395 @param args: should either be an empty list, in which case
396 we show information about all nodes, or should contain
397 a list of nodes to be queried for information
399 @return: the desired exit code
403 result = cl.QueryNodes(fields=["name", "pip", "sip",
404 "pinst_list", "sinst_list",
405 "master_candidate", "drained", "offline",
406 "master_capable", "vm_capable", "powered",
407 "ndparams", "custom_ndparams"],
408 names=args, use_locking=False)
410 for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
411 master_capable, vm_capable, powered, ndparams,
412 ndparams_custom) in result:
413 ToStdout("Node name: %s", name)
414 ToStdout(" primary ip: %s", primary_ip)
415 ToStdout(" secondary ip: %s", secondary_ip)
416 ToStdout(" master candidate: %s", is_mc)
417 ToStdout(" drained: %s", drained)
418 ToStdout(" offline: %s", offline)
419 if powered is not None:
420 ToStdout(" powered: %s", powered)
421 ToStdout(" master_capable: %s", master_capable)
422 ToStdout(" vm_capable: %s", vm_capable)
425 ToStdout(" primary for instances:")
426 for iname in utils.NiceSort(pinst):
427 ToStdout(" - %s", iname)
429 ToStdout(" primary for no instances")
431 ToStdout(" secondary for instances:")
432 for iname in utils.NiceSort(sinst):
433 ToStdout(" - %s", iname)
435 ToStdout(" secondary for no instances")
436 ToStdout(" node parameters:")
438 FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
439 ToStdout(buf.getvalue().rstrip("\n"))
444 def RemoveNode(opts, args):
445 """Remove a node from the cluster.
447 @param opts: the command line options selected by the user
449 @param args: should contain only one element, the name of
450 the node to be removed
452 @return: the desired exit code
455 op = opcodes.OpNodeRemove(node_name=args[0])
456 SubmitOpCode(op, opts=opts)
460 def PowercycleNode(opts, args):
461 """Remove a node from the cluster.
463 @param opts: the command line options selected by the user
465 @param args: should contain only one element, the name of
466 the node to be removed
468 @return: the desired exit code
472 if (not opts.confirm and
473 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
476 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
477 result = SubmitOpCode(op, opts=opts)
483 def PowerNode(opts, args):
484 """Change/ask power state of a node.
486 @param opts: the command line options selected by the user
488 @param args: should contain only one element, the name of
489 the node to be removed
491 @return: the desired exit code
494 command = args.pop(0)
499 headers = {"node": "Node", "status": "Status"}
501 if command not in _LIST_POWER_COMMANDS:
502 ToStderr("power subcommand %s not supported." % command)
503 return constants.EXIT_FAILURE
505 oob_command = "power-%s" % command
507 if oob_command in _OOB_COMMAND_ASK:
509 ToStderr("Please provide at least one node for this command")
510 return constants.EXIT_FAILURE
511 elif not opts.force and not ConfirmOperation(args, "nodes",
512 "power %s" % command):
513 return constants.EXIT_FAILURE
517 if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
518 # TODO: This is a little ugly as we can't catch and revert
520 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
521 auto_promote=opts.auto_promote))
523 opcodelist.append(opcodes.OpOobCommand(node_names=args,
525 ignore_status=opts.ignore_status,
526 timeout=opts.oob_timeout,
527 power_delay=opts.power_delay))
529 cli.SetGenericOpcodeOpts(opcodelist, opts)
531 job_id = cli.SendJob(opcodelist)
533 # We just want the OOB Opcode status
534 # If it fails PollJob gives us the error message in it
535 result = cli.PollJob(job_id)[-1]
539 for node_result in result:
540 (node_tuple, data_tuple) = node_result
541 (_, node_name) = node_tuple
542 (data_status, data_node) = data_tuple
543 if data_status == constants.RS_NORMAL:
544 if oob_command == constants.OOB_POWER_STATUS:
545 if data_node[constants.OOB_POWER_STATUS_POWERED]:
549 data.append([node_name, text])
551 # We don't expect data here, so we just say, it was successfully invoked
552 data.append([node_name, "invoked"])
555 data.append([node_name, cli.FormatResultError(data_status, True)])
557 data = GenerateTable(separator=opts.separator, headers=headers,
558 fields=["node", "status"], data=data)
564 return constants.EXIT_FAILURE
566 return constants.EXIT_SUCCESS
569 def Health(opts, args):
570 """Show health of a node using OOB.
572 @param opts: the command line options selected by the user
574 @param args: should contain only one element, the name of
575 the node to be removed
577 @return: the desired exit code
580 op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
581 timeout=opts.oob_timeout)
582 result = SubmitOpCode(op, opts=opts)
587 headers = {"node": "Node", "status": "Status"}
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 data.append([node_name, "%s=%s" % tuple(data_node[0])])
597 for item, status in data_node[1:]:
598 data.append(["", "%s=%s" % (item, status)])
601 data.append([node_name, cli.FormatResultError(data_status, True)])
603 data = GenerateTable(separator=opts.separator, headers=headers,
604 fields=["node", "status"], data=data)
610 return constants.EXIT_FAILURE
612 return constants.EXIT_SUCCESS
615 def ListVolumes(opts, args):
616 """List logical volumes on node(s).
618 @param opts: the command line options selected by the user
620 @param args: should either be an empty list, in which case
621 we list data for all nodes, or contain a list of nodes
622 to display data only for those
624 @return: the desired exit code
627 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
629 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
630 output = SubmitOpCode(op, opts=opts)
632 if not opts.no_headers:
633 headers = {"node": "Node", "phys": "PhysDev",
634 "vg": "VG", "name": "Name",
635 "size": "Size", "instance": "Instance"}
639 unitfields = ["size"]
643 data = GenerateTable(separator=opts.separator, headers=headers,
644 fields=selected_fields, unitfields=unitfields,
645 numfields=numfields, data=output, units=opts.units)
653 def ListStorage(opts, args):
654 """List physical volumes on node(s).
656 @param opts: the command line options selected by the user
658 @param args: should either be an empty list, in which case
659 we list data for all nodes, or contain a list of nodes
660 to display data only for those
662 @return: the desired exit code
665 # TODO: Default to ST_FILE if LVM is disabled on the cluster
666 if opts.user_storage_type is None:
667 opts.user_storage_type = constants.ST_LVM_PV
669 storage_type = ConvertStorageType(opts.user_storage_type)
671 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
673 op = opcodes.OpNodeQueryStorage(nodes=args,
674 storage_type=storage_type,
675 output_fields=selected_fields)
676 output = SubmitOpCode(op, opts=opts)
678 if not opts.no_headers:
680 constants.SF_NODE: "Node",
681 constants.SF_TYPE: "Type",
682 constants.SF_NAME: "Name",
683 constants.SF_SIZE: "Size",
684 constants.SF_USED: "Used",
685 constants.SF_FREE: "Free",
686 constants.SF_ALLOCATABLE: "Allocatable",
691 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
692 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
694 # change raw values to nicer strings
696 for idx, field in enumerate(selected_fields):
698 if field == constants.SF_ALLOCATABLE:
705 data = GenerateTable(separator=opts.separator, headers=headers,
706 fields=selected_fields, unitfields=unitfields,
707 numfields=numfields, data=output, units=opts.units)
715 def ModifyStorage(opts, args):
716 """Modify storage volume on a node.
718 @param opts: the command line options selected by the user
720 @param args: should contain 3 items: node name, storage type and volume name
722 @return: the desired exit code
725 (node_name, user_storage_type, volume_name) = args
727 storage_type = ConvertStorageType(user_storage_type)
731 if opts.allocatable is not None:
732 changes[constants.SF_ALLOCATABLE] = opts.allocatable
735 op = opcodes.OpNodeModifyStorage(node_name=node_name,
736 storage_type=storage_type,
739 SubmitOpCode(op, opts=opts)
741 ToStderr("No changes to perform, exiting.")
744 def RepairStorage(opts, args):
745 """Repairs a storage volume on a node.
747 @param opts: the command line options selected by the user
749 @param args: should contain 3 items: node name, storage type and volume name
751 @return: the desired exit code
754 (node_name, user_storage_type, volume_name) = args
756 storage_type = ConvertStorageType(user_storage_type)
758 op = opcodes.OpRepairNodeStorage(node_name=node_name,
759 storage_type=storage_type,
761 ignore_consistency=opts.ignore_consistency)
762 SubmitOpCode(op, opts=opts)
765 def SetNodeParams(opts, args):
768 @param opts: the command line options selected by the user
770 @param args: should contain only one element, the node name
772 @return: the desired exit code
775 all_changes = [opts.master_candidate, opts.drained, opts.offline,
776 opts.master_capable, opts.vm_capable, opts.secondary_ip,
778 if all_changes.count(None) == len(all_changes):
779 ToStderr("Please give at least one of the parameters.")
782 op = opcodes.OpNodeSetParams(node_name=args[0],
783 master_candidate=opts.master_candidate,
784 offline=opts.offline,
785 drained=opts.drained,
786 master_capable=opts.master_capable,
787 vm_capable=opts.vm_capable,
788 secondary_ip=opts.secondary_ip,
790 ndparams=opts.ndparams,
791 auto_promote=opts.auto_promote,
792 powered=opts.node_powered)
794 # even if here we process the result, we allow submit only
795 result = SubmitOrSend(op, opts)
798 ToStdout("Modified node %s", args[0])
799 for param, data in result:
800 ToStdout(" - %-5s -> %s", param, data)
806 AddNode, [ArgHost(min=1, max=1)],
807 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
808 NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
809 CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT],
810 "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
811 " [--no-node-setup] [--verbose]"
813 "Add a node to the cluster"),
815 EvacuateNode, [ArgNode(min=1)],
816 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
818 "[-f] {-I <iallocator> | -n <dst>} <node>",
819 "Relocate the secondary instances from a node"
820 " to other nodes (only for instances with drbd disk template)"),
822 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
823 IALLOCATOR_OPT, PRIORITY_OPT],
825 "Stops the primary instances on a node and start them on their"
826 " secondary node (only for instances with drbd disk template)"),
828 MigrateNode, ARGS_ONE_NODE,
829 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT,
830 IALLOCATOR_OPT, PRIORITY_OPT],
832 "Migrate all the primary instance on a node away from it"
833 " (only for instances of type drbd)"),
835 ShowNodeConfig, ARGS_MANY_NODES, [],
836 "[<node_name>...]", "Show information about the node(s)"),
838 ListNodes, ARGS_MANY_NODES,
839 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
842 "Lists the nodes in the cluster. The available fields can be shown using"
843 " the \"list-fields\" command (see the man page for details)."
844 " The default field list is (in order): %s." %
845 utils.CommaJoin(_LIST_DEF_FIELDS)),
847 ListNodeFields, [ArgUnknown()],
848 [NOHDR_OPT, SEP_OPT],
850 "Lists all available fields for nodes"),
852 SetNodeParams, ARGS_ONE_NODE,
853 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
854 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
855 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
857 "<node_name>", "Alters the parameters of a node"),
859 PowercycleNode, ARGS_ONE_NODE,
860 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
861 "<node_name>", "Tries to forcefully powercycle a node"),
864 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
866 [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
867 FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
868 "on|off|cycle|status [nodes...]",
869 "Change power state of node by calling out-of-band helper."),
871 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
872 "<node_name>", "Removes a node from the cluster"),
874 ListVolumes, [ArgNode()],
875 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
876 "[<node_name>...]", "List logical volumes on node(s)"),
878 ListStorage, ARGS_MANY_NODES,
879 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
881 "[<node_name>...]", "List physical volumes on node(s). The available"
882 " fields are (see the man page for details): %s." %
883 (utils.CommaJoin(_LIST_STOR_HEADERS))),
886 [ArgNode(min=1, max=1),
887 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
888 ArgFile(min=1, max=1)],
889 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
890 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
893 [ArgNode(min=1, max=1),
894 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
895 ArgFile(min=1, max=1)],
896 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
897 "<node_name> <storage_type> <name>",
898 "Repairs a storage volume on a node"),
900 ListTags, ARGS_ONE_NODE, [],
901 "<node_name>", "List the tags of the given node"),
903 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
904 "<node_name> tag...", "Add tags to the given node"),
906 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
907 [TAG_SRC_OPT, PRIORITY_OPT],
908 "<node_name> tag...", "Remove tags from the given node"),
910 Health, ARGS_MANY_NODES,
911 [NOHDR_OPT, SEP_OPT, SUBMIT_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
912 "[<node_name>...]", "List health of node(s) using out-of-band"),
917 return GenericMain(commands, override={"tag_type": constants.TAG_NODE})