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=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
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
42 #: default list of field for L{ListNodes}
44 "name", "dtotal", "dfree",
45 "mtotal", "mnode", "mfree",
46 "pinst_cnt", "sinst_cnt",
50 #: Default field list for L{ListVolumes}
51 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
54 #: default list of field for L{ListStorage}
55 _LIST_STOR_DEF_FIELDS = [
62 constants.SF_ALLOCATABLE,
66 #: default list of power commands
67 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
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",
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",
90 cli_option("-t", "--storage-type",
91 dest="user_storage_type",
92 choices=_USER_STORAGE_TYPE.keys(),
94 metavar="STORAGE_TYPE",
95 help=("Storage type (%s)" %
96 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
98 _REPAIRABLE_STORAGE_TYPES = \
99 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
100 if constants.SO_FIX_CONSISTENCY in so]
102 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
105 _OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
106 constants.OOB_POWER_CYCLE])
109 _ENV_OVERRIDE = frozenset(["list"])
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)"))
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)"))
123 def ConvertStorageType(user_storage_type):
124 """Converts a user storage type to its internal name.
128 return _USER_STORAGE_TYPE[user_storage_type]
130 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
134 def _RunSetupSSH(options, nodes):
135 """Wrapper around utils.RunCmd to call setup-ssh
137 @param options: The command line options
138 @param nodes: The nodes to setup
141 cmd = [constants.SETUP_SSH]
143 # Pass --debug|--verbose to the external script if set on our invocation
144 # --debug overrides --verbose
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")
156 result = utils.RunCmd(cmd, interactive=True)
159 errmsg = ("Command '%s' failed with exit code %s; output %r" %
160 (result.cmd, result.exit_code, result.output))
161 raise errors.OpExecError(errmsg)
165 def AddNode(opts, args):
166 """Add a node to the cluster.
168 @param opts: the command line options selected by the user
170 @param args: should contain only one element, the new node name
172 @return: the desired exit code
176 node = netutils.GetHostname(name=args[0]).name
180 output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
182 node_exists, sip, is_master = output[0]
183 except (errors.OpPrereqError, errors.OpExecError):
189 ToStderr("Node %s not in the cluster"
190 " - please retry without '--readd'", node)
193 ToStderr("Node %s is the master, cannot readd", node)
197 ToStderr("Node %s already in the cluster (as %s)"
198 " - please retry with '--readd'", node, node_exists)
200 sip = opts.secondary_ip
202 # read the cluster name from the master
203 output = cl.QueryConfigValues(["cluster_name"])
204 cluster_name = output[0]
206 if not readd and opts.node_setup:
207 ToStderr("-- WARNING -- \n"
208 "Performing this operation is going to replace the ssh daemon"
210 "on the target machine (%s) with the ones of the"
212 "and grant full intra-cluster ssh root access to/from it\n", node)
215 _RunSetupSSH(opts, [node])
217 bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
219 op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
220 readd=opts.readd, group=opts.nodegroup,
221 vm_capable=opts.vm_capable, ndparams=opts.ndparams,
222 master_capable=opts.master_capable)
223 SubmitOpCode(op, opts=opts)
226 def ListNodes(opts, args):
227 """List nodes and their properties.
229 @param opts: the command line options selected by the user
231 @param args: nodes to list, or empty for all
233 @return: the desired exit code
236 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
238 fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
241 return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
242 opts.separator, not opts.no_headers,
243 format_override=fmtoverride, verbose=opts.verbose,
244 force_filter=opts.force_filter)
247 def ListNodeFields(opts, args):
250 @param opts: the command line options selected by the user
252 @param args: fields to list, or empty for all
254 @return: the desired exit code
257 return GenericListFields(constants.QR_NODE, args, opts.separator,
261 def EvacuateNode(opts, args):
262 """Relocate all secondary instance from a node.
264 @param opts: the command line options selected by the user
266 @param args: should be an empty list
268 @return: the desired exit code
271 if opts.dst_node is not None:
272 ToStderr("New secondary node given (disabling iallocator), hence evacuating"
273 " secondary instances only.")
274 opts.secondary_only = True
275 opts.primary_only = False
277 if opts.secondary_only and opts.primary_only:
278 raise errors.OpPrereqError("Only one of the --primary-only and"
279 " --secondary-only options can be passed",
281 elif opts.primary_only:
282 mode = constants.IALLOCATOR_NEVAC_PRI
283 elif opts.secondary_only:
284 mode = constants.IALLOCATOR_NEVAC_SEC
286 mode = constants.IALLOCATOR_NEVAC_ALL
288 # Determine affected instances
291 if not opts.secondary_only:
292 fields.append("pinst_list")
293 if not opts.primary_only:
294 fields.append("sinst_list")
298 result = cl.QueryNodes(names=args, fields=fields, use_locking=False)
299 instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
302 # No instances to evacuate
303 ToStderr("No instances to evacuate on node(s) %s, exiting.",
304 utils.CommaJoin(args))
305 return constants.EXIT_SUCCESS
307 if not (opts.force or
308 AskUser("Relocate instance(s) %s from node(s) %s?" %
309 (utils.CommaJoin(utils.NiceSort(instances)),
310 utils.CommaJoin(args)))):
311 return constants.EXIT_CONFIRMATION
314 op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
315 remote_node=opts.dst_node,
316 iallocator=opts.iallocator,
317 early_release=opts.early_release)
318 result = SubmitOpCode(op, cl=cl, opts=opts)
320 # Keep track of submitted jobs
321 jex = JobExecutor(cl=cl, opts=opts)
323 for (status, job_id) in result[constants.JOB_IDS_KEY]:
324 jex.AddJobId(None, status, job_id)
326 results = jex.GetResults()
327 bad_cnt = len([row for row in results if not row[0]])
329 ToStdout("All instances evacuated successfully.")
330 rcode = constants.EXIT_SUCCESS
332 ToStdout("There were %s errors during the evacuation.", bad_cnt)
333 rcode = constants.EXIT_FAILURE
338 def FailoverNode(opts, args):
339 """Failover all primary instance on a node.
341 @param opts: the command line options selected by the user
343 @param args: should be an empty list
345 @return: the desired exit code
350 selected_fields = ["name", "pinst_list"]
352 # these fields are static data anyway, so it doesn't matter, but
353 # locking=True should be safer
354 result = cl.QueryNodes(names=args, fields=selected_fields,
356 node, pinst = result[0]
359 ToStderr("No primary instances on node %s, exiting.", node)
362 pinst = utils.NiceSort(pinst)
366 if not force and not AskUser("Fail over instance(s) %s?" %
367 (",".join("'%s'" % name for name in pinst))):
370 jex = JobExecutor(cl=cl, opts=opts)
372 op = opcodes.OpInstanceFailover(instance_name=iname,
373 ignore_consistency=opts.ignore_consistency,
374 iallocator=opts.iallocator)
375 jex.QueueJob(iname, op)
376 results = jex.GetResults()
377 bad_cnt = len([row for row in results if not row[0]])
379 ToStdout("All %d instance(s) failed over successfully.", len(results))
381 ToStdout("There were errors during the failover:\n"
382 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
386 def MigrateNode(opts, args):
387 """Migrate all primary instance on a node.
392 selected_fields = ["name", "pinst_list"]
394 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
395 ((node, pinst), ) = result
398 ToStdout("No primary instances on node %s, exiting." % node)
401 pinst = utils.NiceSort(pinst)
404 AskUser("Migrate instance(s) %s?" %
405 utils.CommaJoin(utils.NiceSort(pinst)))):
406 return constants.EXIT_CONFIRMATION
408 # this should be removed once --non-live is deprecated
409 if not opts.live and opts.migration_mode is not None:
410 raise errors.OpPrereqError("Only one of the --non-live and "
411 "--migration-mode options can be passed",
413 if not opts.live: # --non-live passed
414 mode = constants.HT_MIGRATION_NONLIVE
416 mode = opts.migration_mode
418 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
419 iallocator=opts.iallocator,
420 target_node=opts.dst_node)
422 result = SubmitOpCode(op, cl=cl, opts=opts)
424 # Keep track of submitted jobs
425 jex = JobExecutor(cl=cl, opts=opts)
427 for (status, job_id) in result[constants.JOB_IDS_KEY]:
428 jex.AddJobId(None, status, job_id)
430 results = jex.GetResults()
431 bad_cnt = len([row for row in results if not row[0]])
433 ToStdout("All instances migrated successfully.")
434 rcode = constants.EXIT_SUCCESS
436 ToStdout("There were %s errors during the node migration.", bad_cnt)
437 rcode = constants.EXIT_FAILURE
442 def ShowNodeConfig(opts, args):
443 """Show node information.
445 @param opts: the command line options selected by the user
447 @param args: should either be an empty list, in which case
448 we show information about all nodes, or should contain
449 a list of nodes to be queried for information
451 @return: the desired exit code
455 result = cl.QueryNodes(fields=["name", "pip", "sip",
456 "pinst_list", "sinst_list",
457 "master_candidate", "drained", "offline",
458 "master_capable", "vm_capable", "powered",
459 "ndparams", "custom_ndparams"],
460 names=args, use_locking=False)
462 for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
463 master_capable, vm_capable, powered, ndparams,
464 ndparams_custom) in result:
465 ToStdout("Node name: %s", name)
466 ToStdout(" primary ip: %s", primary_ip)
467 ToStdout(" secondary ip: %s", secondary_ip)
468 ToStdout(" master candidate: %s", is_mc)
469 ToStdout(" drained: %s", drained)
470 ToStdout(" offline: %s", offline)
471 if powered is not None:
472 ToStdout(" powered: %s", powered)
473 ToStdout(" master_capable: %s", master_capable)
474 ToStdout(" vm_capable: %s", vm_capable)
477 ToStdout(" primary for instances:")
478 for iname in utils.NiceSort(pinst):
479 ToStdout(" - %s", iname)
481 ToStdout(" primary for no instances")
483 ToStdout(" secondary for instances:")
484 for iname in utils.NiceSort(sinst):
485 ToStdout(" - %s", iname)
487 ToStdout(" secondary for no instances")
488 ToStdout(" node parameters:")
490 FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
491 ToStdout(buf.getvalue().rstrip("\n"))
496 def RemoveNode(opts, args):
497 """Remove a node from the cluster.
499 @param opts: the command line options selected by the user
501 @param args: should contain only one element, the name of
502 the node to be removed
504 @return: the desired exit code
507 op = opcodes.OpNodeRemove(node_name=args[0])
508 SubmitOpCode(op, opts=opts)
512 def PowercycleNode(opts, args):
513 """Remove a node from the cluster.
515 @param opts: the command line options selected by the user
517 @param args: should contain only one element, the name of
518 the node to be removed
520 @return: the desired exit code
524 if (not opts.confirm and
525 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
528 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
529 result = SubmitOpCode(op, opts=opts)
535 def PowerNode(opts, args):
536 """Change/ask power state of a node.
538 @param opts: the command line options selected by the user
540 @param args: should contain only one element, the name of
541 the node to be removed
543 @return: the desired exit code
546 command = args.pop(0)
551 headers = {"node": "Node", "status": "Status"}
553 if command not in _LIST_POWER_COMMANDS:
554 ToStderr("power subcommand %s not supported." % command)
555 return constants.EXIT_FAILURE
557 oob_command = "power-%s" % command
559 if oob_command in _OOB_COMMAND_ASK:
561 ToStderr("Please provide at least one node for this command")
562 return constants.EXIT_FAILURE
563 elif not opts.force and not ConfirmOperation(args, "nodes",
564 "power %s" % command):
565 return constants.EXIT_FAILURE
569 if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
570 # TODO: This is a little ugly as we can't catch and revert
572 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
573 auto_promote=opts.auto_promote))
575 opcodelist.append(opcodes.OpOobCommand(node_names=args,
577 ignore_status=opts.ignore_status,
578 timeout=opts.oob_timeout,
579 power_delay=opts.power_delay))
581 cli.SetGenericOpcodeOpts(opcodelist, opts)
583 job_id = cli.SendJob(opcodelist)
585 # We just want the OOB Opcode status
586 # If it fails PollJob gives us the error message in it
587 result = cli.PollJob(job_id)[-1]
591 for node_result in result:
592 (node_tuple, data_tuple) = node_result
593 (_, node_name) = node_tuple
594 (data_status, data_node) = data_tuple
595 if data_status == constants.RS_NORMAL:
596 if oob_command == constants.OOB_POWER_STATUS:
597 if data_node[constants.OOB_POWER_STATUS_POWERED]:
601 data.append([node_name, text])
603 # We don't expect data here, so we just say, it was successfully invoked
604 data.append([node_name, "invoked"])
607 data.append([node_name, cli.FormatResultError(data_status, True)])
609 data = GenerateTable(separator=opts.separator, headers=headers,
610 fields=["node", "status"], data=data)
616 return constants.EXIT_FAILURE
618 return constants.EXIT_SUCCESS
621 def Health(opts, args):
622 """Show health of a node using OOB.
624 @param opts: the command line options selected by the user
626 @param args: should contain only one element, the name of
627 the node to be removed
629 @return: the desired exit code
632 op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
633 timeout=opts.oob_timeout)
634 result = SubmitOpCode(op, opts=opts)
639 headers = {"node": "Node", "status": "Status"}
643 for node_result in result:
644 (node_tuple, data_tuple) = node_result
645 (_, node_name) = node_tuple
646 (data_status, data_node) = data_tuple
647 if data_status == constants.RS_NORMAL:
648 data.append([node_name, "%s=%s" % tuple(data_node[0])])
649 for item, status in data_node[1:]:
650 data.append(["", "%s=%s" % (item, status)])
653 data.append([node_name, cli.FormatResultError(data_status, True)])
655 data = GenerateTable(separator=opts.separator, headers=headers,
656 fields=["node", "status"], data=data)
662 return constants.EXIT_FAILURE
664 return constants.EXIT_SUCCESS
667 def ListVolumes(opts, args):
668 """List logical volumes on node(s).
670 @param opts: the command line options selected by the user
672 @param args: should either be an empty list, in which case
673 we list data for all nodes, or contain a list of nodes
674 to display data only for those
676 @return: the desired exit code
679 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
681 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
682 output = SubmitOpCode(op, opts=opts)
684 if not opts.no_headers:
685 headers = {"node": "Node", "phys": "PhysDev",
686 "vg": "VG", "name": "Name",
687 "size": "Size", "instance": "Instance"}
691 unitfields = ["size"]
695 data = GenerateTable(separator=opts.separator, headers=headers,
696 fields=selected_fields, unitfields=unitfields,
697 numfields=numfields, data=output, units=opts.units)
705 def ListStorage(opts, args):
706 """List physical volumes on node(s).
708 @param opts: the command line options selected by the user
710 @param args: should either be an empty list, in which case
711 we list data for all nodes, or contain a list of nodes
712 to display data only for those
714 @return: the desired exit code
717 # TODO: Default to ST_FILE if LVM is disabled on the cluster
718 if opts.user_storage_type is None:
719 opts.user_storage_type = constants.ST_LVM_PV
721 storage_type = ConvertStorageType(opts.user_storage_type)
723 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
725 op = opcodes.OpNodeQueryStorage(nodes=args,
726 storage_type=storage_type,
727 output_fields=selected_fields)
728 output = SubmitOpCode(op, opts=opts)
730 if not opts.no_headers:
732 constants.SF_NODE: "Node",
733 constants.SF_TYPE: "Type",
734 constants.SF_NAME: "Name",
735 constants.SF_SIZE: "Size",
736 constants.SF_USED: "Used",
737 constants.SF_FREE: "Free",
738 constants.SF_ALLOCATABLE: "Allocatable",
743 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
744 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
746 # change raw values to nicer strings
748 for idx, field in enumerate(selected_fields):
750 if field == constants.SF_ALLOCATABLE:
757 data = GenerateTable(separator=opts.separator, headers=headers,
758 fields=selected_fields, unitfields=unitfields,
759 numfields=numfields, data=output, units=opts.units)
767 def ModifyStorage(opts, args):
768 """Modify storage volume on a node.
770 @param opts: the command line options selected by the user
772 @param args: should contain 3 items: node name, storage type and volume name
774 @return: the desired exit code
777 (node_name, user_storage_type, volume_name) = args
779 storage_type = ConvertStorageType(user_storage_type)
783 if opts.allocatable is not None:
784 changes[constants.SF_ALLOCATABLE] = opts.allocatable
787 op = opcodes.OpNodeModifyStorage(node_name=node_name,
788 storage_type=storage_type,
791 SubmitOpCode(op, opts=opts)
793 ToStderr("No changes to perform, exiting.")
796 def RepairStorage(opts, args):
797 """Repairs a storage volume on a node.
799 @param opts: the command line options selected by the user
801 @param args: should contain 3 items: node name, storage type and volume name
803 @return: the desired exit code
806 (node_name, user_storage_type, volume_name) = args
808 storage_type = ConvertStorageType(user_storage_type)
810 op = opcodes.OpRepairNodeStorage(node_name=node_name,
811 storage_type=storage_type,
813 ignore_consistency=opts.ignore_consistency)
814 SubmitOpCode(op, opts=opts)
817 def SetNodeParams(opts, args):
820 @param opts: the command line options selected by the user
822 @param args: should contain only one element, the node name
824 @return: the desired exit code
827 all_changes = [opts.master_candidate, opts.drained, opts.offline,
828 opts.master_capable, opts.vm_capable, opts.secondary_ip,
830 if all_changes.count(None) == len(all_changes):
831 ToStderr("Please give at least one of the parameters.")
834 op = opcodes.OpNodeSetParams(node_name=args[0],
835 master_candidate=opts.master_candidate,
836 offline=opts.offline,
837 drained=opts.drained,
838 master_capable=opts.master_capable,
839 vm_capable=opts.vm_capable,
840 secondary_ip=opts.secondary_ip,
842 ndparams=opts.ndparams,
843 auto_promote=opts.auto_promote,
844 powered=opts.node_powered)
846 # even if here we process the result, we allow submit only
847 result = SubmitOrSend(op, opts)
850 ToStdout("Modified node %s", args[0])
851 for param, data in result:
852 ToStdout(" - %-5s -> %s", param, data)
858 AddNode, [ArgHost(min=1, max=1)],
859 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
860 NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
861 CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT],
862 "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
863 " [--no-node-setup] [--verbose]"
865 "Add a node to the cluster"),
867 EvacuateNode, ARGS_ONE_NODE,
868 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
869 PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT],
870 "[-f] {-I <iallocator> | -n <dst>} <node>",
871 "Relocate the secondary instances from a node"
874 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
875 IALLOCATOR_OPT, PRIORITY_OPT],
877 "Stops the primary instances on a node and start them on their"
878 " secondary node (only for instances with drbd disk template)"),
880 MigrateNode, ARGS_ONE_NODE,
881 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
882 IALLOCATOR_OPT, PRIORITY_OPT],
884 "Migrate all the primary instance on a node away from it"
885 " (only for instances of type drbd)"),
887 ShowNodeConfig, ARGS_MANY_NODES, [],
888 "[<node_name>...]", "Show information about the node(s)"),
890 ListNodes, ARGS_MANY_NODES,
891 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
894 "Lists the nodes in the cluster. The available fields can be shown using"
895 " the \"list-fields\" command (see the man page for details)."
896 " The default field list is (in order): %s." %
897 utils.CommaJoin(_LIST_DEF_FIELDS)),
899 ListNodeFields, [ArgUnknown()],
900 [NOHDR_OPT, SEP_OPT],
902 "Lists all available fields for nodes"),
904 SetNodeParams, ARGS_ONE_NODE,
905 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
906 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
907 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
909 "<node_name>", "Alters the parameters of a node"),
911 PowercycleNode, ARGS_ONE_NODE,
912 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
913 "<node_name>", "Tries to forcefully powercycle a node"),
916 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
918 [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
919 FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
920 "on|off|cycle|status [nodes...]",
921 "Change power state of node by calling out-of-band helper."),
923 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
924 "<node_name>", "Removes a node from the cluster"),
926 ListVolumes, [ArgNode()],
927 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
928 "[<node_name>...]", "List logical volumes on node(s)"),
930 ListStorage, ARGS_MANY_NODES,
931 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
933 "[<node_name>...]", "List physical volumes on node(s). The available"
934 " fields are (see the man page for details): %s." %
935 (utils.CommaJoin(_LIST_STOR_HEADERS))),
938 [ArgNode(min=1, max=1),
939 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
940 ArgFile(min=1, max=1)],
941 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
942 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
945 [ArgNode(min=1, max=1),
946 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
947 ArgFile(min=1, max=1)],
948 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
949 "<node_name> <storage_type> <name>",
950 "Repairs a storage volume on a node"),
952 ListTags, ARGS_ONE_NODE, [],
953 "<node_name>", "List the tags of the given node"),
955 AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
956 "<node_name> tag...", "Add tags to the given node"),
958 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
959 [TAG_SRC_OPT, PRIORITY_OPT],
960 "<node_name> tag...", "Remove tags from the given node"),
962 Health, ARGS_MANY_NODES,
963 [NOHDR_OPT, SEP_OPT, SUBMIT_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
964 "[<node_name>...]", "List health of node(s) using out-of-band"),
969 return GenericMain(commands, override={"tag_type": constants.TAG_NODE},
970 env_override=_ENV_OVERRIDE)