4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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
32 from ganeti.cli import *
33 from ganeti import cli
34 from ganeti import bootstrap
35 from ganeti import opcodes
36 from ganeti import utils
37 from ganeti import constants
38 from ganeti import errors
39 from ganeti import netutils
40 from ganeti import pathutils
41 from ganeti import ssh
42 from ganeti import compat
44 from ganeti import confd
45 from ganeti.confd import client as confd_client
47 #: default list of field for L{ListNodes}
49 "name", "dtotal", "dfree",
50 "mtotal", "mnode", "mfree",
51 "pinst_cnt", "sinst_cnt",
55 #: Default field list for L{ListVolumes}
56 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
59 #: default list of field for L{ListStorage}
60 _LIST_STOR_DEF_FIELDS = [
67 constants.SF_ALLOCATABLE,
71 #: default list of power commands
72 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
75 #: headers (and full field list) for L{ListStorage}
76 _LIST_STOR_HEADERS = {
77 constants.SF_NODE: "Node",
78 constants.SF_TYPE: "Type",
79 constants.SF_NAME: "Name",
80 constants.SF_SIZE: "Size",
81 constants.SF_USED: "Used",
82 constants.SF_FREE: "Free",
83 constants.SF_ALLOCATABLE: "Allocatable",
87 #: User-facing storage unit types
88 _USER_STORAGE_TYPE = {
89 constants.ST_FILE: "file",
90 constants.ST_LVM_PV: "lvm-pv",
91 constants.ST_LVM_VG: "lvm-vg",
95 cli_option("-t", "--storage-type",
96 dest="user_storage_type",
97 choices=_USER_STORAGE_TYPE.keys(),
99 metavar="STORAGE_TYPE",
100 help=("Storage type (%s)" %
101 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
103 _REPAIRABLE_STORAGE_TYPES = \
104 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
105 if constants.SO_FIX_CONSISTENCY in so]
107 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
109 _OOB_COMMAND_ASK = compat.UniqueFrozenset([
110 constants.OOB_POWER_OFF,
111 constants.OOB_POWER_CYCLE,
114 _ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
116 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
117 action="store_false", dest="node_setup",
118 help=("Do not make initial SSH setup on remote"
119 " node (needs to be done manually)"))
121 IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
122 action="store_true", dest="ignore_status",
123 help=("Ignore the Node(s) offline status"
124 " (potentially DANGEROUS)"))
127 def ConvertStorageType(user_storage_type):
128 """Converts a user storage type to its internal name.
132 return _USER_STORAGE_TYPE[user_storage_type]
134 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
138 def _TryReadFile(path):
139 """Tries to read a file.
141 If the file is not found, C{None} is returned.
144 @param path: Filename
145 @rtype: None or string
146 @todo: Consider adding a generic ENOENT wrapper
150 return utils.ReadFile(path)
151 except EnvironmentError, err:
152 if err.errno == errno.ENOENT:
158 def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
159 """Reads SSH keys according to C{keyfiles}.
162 @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values
163 tuples (private and public key file)
165 @return: List of three-values tuples (L{constants.SSHK_ALL}, private and
166 public key as strings)
171 for (kind, (private_file, public_file)) in keyfiles.items():
172 private_key = _TryReadFile(private_file)
173 public_key = _TryReadFile(public_file)
175 if public_key and private_key:
176 result.append((kind, private_key, public_key))
177 elif public_key or private_key:
178 _tostderr_fn("Couldn't find a complete set of keys for kind '%s'; files"
179 " '%s' and '%s'", kind, private_file, public_file)
184 def _SetupSSH(options, cluster_name, node):
185 """Configures a destination node's SSH daemon.
187 @param options: Command line options
189 @param cluster_name: Cluster name
191 @param node: Destination node name
194 if options.force_join:
195 ToStderr("The \"--force-join\" option is no longer supported and will be"
198 host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
200 (_, root_keyfiles) = \
201 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
203 root_keys = _ReadSshKeys(root_keyfiles)
206 utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
209 constants.SSHS_CLUSTER_NAME: cluster_name,
210 constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
211 constants.SSHS_SSH_HOST_KEY: host_keys,
212 constants.SSHS_SSH_ROOT_KEY: root_keys,
215 bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
216 options.debug, options.verbose, False,
217 options.ssh_key_check, options.ssh_key_check, data)
221 def AddNode(opts, args):
222 """Add a node to the cluster.
224 @param opts: the command line options selected by the user
226 @param args: should contain only one element, the new node name
228 @return: the desired exit code
232 node = netutils.GetHostname(name=args[0]).name
236 output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
238 node_exists, sip, is_master = output[0]
239 except (errors.OpPrereqError, errors.OpExecError):
245 ToStderr("Node %s not in the cluster"
246 " - please retry without '--readd'", node)
249 ToStderr("Node %s is the master, cannot readd", node)
253 ToStderr("Node %s already in the cluster (as %s)"
254 " - please retry with '--readd'", node, node_exists)
256 sip = opts.secondary_ip
258 # read the cluster name from the master
259 (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
261 if not readd and opts.node_setup:
262 ToStderr("-- WARNING -- \n"
263 "Performing this operation is going to replace the ssh daemon"
265 "on the target machine (%s) with the ones of the"
267 "and grant full intra-cluster ssh root access to/from it\n", node)
270 _SetupSSH(opts, cluster_name, node)
272 bootstrap.SetupNodeDaemon(opts, cluster_name, node)
275 disk_state = utils.FlatToDict(opts.disk_state)
279 hv_state = dict(opts.hv_state)
281 op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
282 readd=opts.readd, group=opts.nodegroup,
283 vm_capable=opts.vm_capable, ndparams=opts.ndparams,
284 master_capable=opts.master_capable,
285 disk_state=disk_state,
287 SubmitOpCode(op, opts=opts)
290 def ListNodes(opts, args):
291 """List nodes and their properties.
293 @param opts: the command line options selected by the user
295 @param args: nodes to list, or empty for all
297 @return: the desired exit code
300 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
302 fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
305 cl = GetClient(query=True)
307 return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
308 opts.separator, not opts.no_headers,
309 format_override=fmtoverride, verbose=opts.verbose,
310 force_filter=opts.force_filter, cl=cl)
313 def ListNodeFields(opts, args):
316 @param opts: the command line options selected by the user
318 @param args: fields to list, or empty for all
320 @return: the desired exit code
323 cl = GetClient(query=True)
325 return GenericListFields(constants.QR_NODE, args, opts.separator,
326 not opts.no_headers, cl=cl)
329 def EvacuateNode(opts, args):
330 """Relocate all secondary instance from a node.
332 @param opts: the command line options selected by the user
334 @param args: should be an empty list
336 @return: the desired exit code
339 if opts.dst_node is not None:
340 ToStderr("New secondary node given (disabling iallocator), hence evacuating"
341 " secondary instances only.")
342 opts.secondary_only = True
343 opts.primary_only = False
345 if opts.secondary_only and opts.primary_only:
346 raise errors.OpPrereqError("Only one of the --primary-only and"
347 " --secondary-only options can be passed",
349 elif opts.primary_only:
350 mode = constants.NODE_EVAC_PRI
351 elif opts.secondary_only:
352 mode = constants.NODE_EVAC_SEC
354 mode = constants.NODE_EVAC_ALL
356 # Determine affected instances
359 if not opts.secondary_only:
360 fields.append("pinst_list")
361 if not opts.primary_only:
362 fields.append("sinst_list")
366 qcl = GetClient(query=True)
367 result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
370 instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
373 # No instances to evacuate
374 ToStderr("No instances to evacuate on node(s) %s, exiting.",
375 utils.CommaJoin(args))
376 return constants.EXIT_SUCCESS
378 if not (opts.force or
379 AskUser("Relocate instance(s) %s from node(s) %s?" %
380 (utils.CommaJoin(utils.NiceSort(instances)),
381 utils.CommaJoin(args)))):
382 return constants.EXIT_CONFIRMATION
385 op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
386 remote_node=opts.dst_node,
387 iallocator=opts.iallocator,
388 early_release=opts.early_release)
389 result = SubmitOrSend(op, opts, cl=cl)
391 # Keep track of submitted jobs
392 jex = JobExecutor(cl=cl, opts=opts)
394 for (status, job_id) in result[constants.JOB_IDS_KEY]:
395 jex.AddJobId(None, status, job_id)
397 results = jex.GetResults()
398 bad_cnt = len([row for row in results if not row[0]])
400 ToStdout("All instances evacuated successfully.")
401 rcode = constants.EXIT_SUCCESS
403 ToStdout("There were %s errors during the evacuation.", bad_cnt)
404 rcode = constants.EXIT_FAILURE
409 def FailoverNode(opts, args):
410 """Failover all primary instance on a node.
412 @param opts: the command line options selected by the user
414 @param args: should be an empty list
416 @return: the desired exit code
421 selected_fields = ["name", "pinst_list"]
423 # these fields are static data anyway, so it doesn't matter, but
424 # locking=True should be safer
425 qcl = GetClient(query=True)
426 result = cl.QueryNodes(names=args, fields=selected_fields,
429 node, pinst = result[0]
432 ToStderr("No primary instances on node %s, exiting.", node)
435 pinst = utils.NiceSort(pinst)
439 if not force and not AskUser("Fail over instance(s) %s?" %
440 (",".join("'%s'" % name for name in pinst))):
443 jex = JobExecutor(cl=cl, opts=opts)
445 op = opcodes.OpInstanceFailover(instance_name=iname,
446 ignore_consistency=opts.ignore_consistency,
447 iallocator=opts.iallocator)
448 jex.QueueJob(iname, op)
449 results = jex.GetResults()
450 bad_cnt = len([row for row in results if not row[0]])
452 ToStdout("All %d instance(s) failed over successfully.", len(results))
454 ToStdout("There were errors during the failover:\n"
455 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
459 def MigrateNode(opts, args):
460 """Migrate all primary instance on a node.
465 selected_fields = ["name", "pinst_list"]
467 qcl = GetClient(query=True)
468 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
470 ((node, pinst), ) = result
473 ToStdout("No primary instances on node %s, exiting." % node)
476 pinst = utils.NiceSort(pinst)
479 AskUser("Migrate instance(s) %s?" %
480 utils.CommaJoin(utils.NiceSort(pinst)))):
481 return constants.EXIT_CONFIRMATION
483 # this should be removed once --non-live is deprecated
484 if not opts.live and opts.migration_mode is not None:
485 raise errors.OpPrereqError("Only one of the --non-live and "
486 "--migration-mode options can be passed",
488 if not opts.live: # --non-live passed
489 mode = constants.HT_MIGRATION_NONLIVE
491 mode = opts.migration_mode
493 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
494 iallocator=opts.iallocator,
495 target_node=opts.dst_node,
496 allow_runtime_changes=opts.allow_runtime_chgs,
497 ignore_ipolicy=opts.ignore_ipolicy)
499 result = SubmitOrSend(op, opts, cl=cl)
501 # Keep track of submitted jobs
502 jex = JobExecutor(cl=cl, opts=opts)
504 for (status, job_id) in result[constants.JOB_IDS_KEY]:
505 jex.AddJobId(None, status, job_id)
507 results = jex.GetResults()
508 bad_cnt = len([row for row in results if not row[0]])
510 ToStdout("All instances migrated successfully.")
511 rcode = constants.EXIT_SUCCESS
513 ToStdout("There were %s errors during the node migration.", bad_cnt)
514 rcode = constants.EXIT_FAILURE
519 def _FormatNodeInfo(node_info):
520 """Format node information for L{cli.PrintGenericInfo()}.
523 (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
524 master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info
527 ("primary ip", primary_ip),
528 ("secondary ip", secondary_ip),
529 ("master candidate", is_mc),
530 ("drained", drained),
531 ("offline", offline),
533 if powered is not None:
534 info.append(("powered", powered))
536 ("master_capable", master_capable),
537 ("vm_capable", vm_capable),
541 ("primary for instances",
542 [iname for iname in utils.NiceSort(pinst)]),
543 ("secondary for instances",
544 [iname for iname in utils.NiceSort(sinst)]),
546 info.append(("node parameters",
547 FormatParamsDictInfo(ndparams_custom, ndparams)))
551 def ShowNodeConfig(opts, args):
552 """Show node information.
554 @param opts: the command line options selected by the user
556 @param args: should either be an empty list, in which case
557 we show information about all nodes, or should contain
558 a list of nodes to be queried for information
560 @return: the desired exit code
563 cl = GetClient(query=True)
564 result = cl.QueryNodes(fields=["name", "pip", "sip",
565 "pinst_list", "sinst_list",
566 "master_candidate", "drained", "offline",
567 "master_capable", "vm_capable", "powered",
568 "ndparams", "custom_ndparams"],
569 names=args, use_locking=False)
571 _FormatNodeInfo(node_info)
572 for node_info in result
577 def RemoveNode(opts, args):
578 """Remove a node from the cluster.
580 @param opts: the command line options selected by the user
582 @param args: should contain only one element, the name of
583 the node to be removed
585 @return: the desired exit code
588 op = opcodes.OpNodeRemove(node_name=args[0])
589 SubmitOpCode(op, opts=opts)
593 def PowercycleNode(opts, args):
594 """Remove a node from the cluster.
596 @param opts: the command line options selected by the user
598 @param args: should contain only one element, the name of
599 the node to be removed
601 @return: the desired exit code
605 if (not opts.confirm and
606 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
609 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
610 result = SubmitOrSend(op, opts)
616 def PowerNode(opts, args):
617 """Change/ask power state of a node.
619 @param opts: the command line options selected by the user
621 @param args: should contain only one element, the name of
622 the node to be removed
624 @return: the desired exit code
627 command = args.pop(0)
632 headers = {"node": "Node", "status": "Status"}
634 if command not in _LIST_POWER_COMMANDS:
635 ToStderr("power subcommand %s not supported." % command)
636 return constants.EXIT_FAILURE
638 oob_command = "power-%s" % command
640 if oob_command in _OOB_COMMAND_ASK:
642 ToStderr("Please provide at least one node for this command")
643 return constants.EXIT_FAILURE
644 elif not opts.force and not ConfirmOperation(args, "nodes",
645 "power %s" % command):
646 return constants.EXIT_FAILURE
650 if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
651 # TODO: This is a little ugly as we can't catch and revert
653 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
654 auto_promote=opts.auto_promote))
656 opcodelist.append(opcodes.OpOobCommand(node_names=args,
658 ignore_status=opts.ignore_status,
659 timeout=opts.oob_timeout,
660 power_delay=opts.power_delay))
662 cli.SetGenericOpcodeOpts(opcodelist, opts)
664 job_id = cli.SendJob(opcodelist)
666 # We just want the OOB Opcode status
667 # If it fails PollJob gives us the error message in it
668 result = cli.PollJob(job_id)[-1]
672 for node_result in result:
673 (node_tuple, data_tuple) = node_result
674 (_, node_name) = node_tuple
675 (data_status, data_node) = data_tuple
676 if data_status == constants.RS_NORMAL:
677 if oob_command == constants.OOB_POWER_STATUS:
678 if data_node[constants.OOB_POWER_STATUS_POWERED]:
682 data.append([node_name, text])
684 # We don't expect data here, so we just say, it was successfully invoked
685 data.append([node_name, "invoked"])
688 data.append([node_name, cli.FormatResultError(data_status, True)])
690 data = GenerateTable(separator=opts.separator, headers=headers,
691 fields=["node", "status"], data=data)
697 return constants.EXIT_FAILURE
699 return constants.EXIT_SUCCESS
702 def Health(opts, args):
703 """Show health of a node using OOB.
705 @param opts: the command line options selected by the user
707 @param args: should contain only one element, the name of
708 the node to be removed
710 @return: the desired exit code
713 op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
714 timeout=opts.oob_timeout)
715 result = SubmitOpCode(op, opts=opts)
720 headers = {"node": "Node", "status": "Status"}
724 for node_result in result:
725 (node_tuple, data_tuple) = node_result
726 (_, node_name) = node_tuple
727 (data_status, data_node) = data_tuple
728 if data_status == constants.RS_NORMAL:
729 data.append([node_name, "%s=%s" % tuple(data_node[0])])
730 for item, status in data_node[1:]:
731 data.append(["", "%s=%s" % (item, status)])
734 data.append([node_name, cli.FormatResultError(data_status, True)])
736 data = GenerateTable(separator=opts.separator, headers=headers,
737 fields=["node", "status"], data=data)
743 return constants.EXIT_FAILURE
745 return constants.EXIT_SUCCESS
748 def ListVolumes(opts, args):
749 """List logical volumes on node(s).
751 @param opts: the command line options selected by the user
753 @param args: should either be an empty list, in which case
754 we list data for all nodes, or contain a list of nodes
755 to display data only for those
757 @return: the desired exit code
760 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
762 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
763 output = SubmitOpCode(op, opts=opts)
765 if not opts.no_headers:
766 headers = {"node": "Node", "phys": "PhysDev",
767 "vg": "VG", "name": "Name",
768 "size": "Size", "instance": "Instance"}
772 unitfields = ["size"]
776 data = GenerateTable(separator=opts.separator, headers=headers,
777 fields=selected_fields, unitfields=unitfields,
778 numfields=numfields, data=output, units=opts.units)
786 def ListStorage(opts, args):
787 """List physical volumes on node(s).
789 @param opts: the command line options selected by the user
791 @param args: should either be an empty list, in which case
792 we list data for all nodes, or contain a list of nodes
793 to display data only for those
795 @return: the desired exit code
798 # TODO: Default to ST_FILE if LVM is disabled on the cluster
799 if opts.user_storage_type is None:
800 opts.user_storage_type = constants.ST_LVM_PV
802 storage_type = ConvertStorageType(opts.user_storage_type)
804 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
806 op = opcodes.OpNodeQueryStorage(nodes=args,
807 storage_type=storage_type,
808 output_fields=selected_fields)
809 output = SubmitOpCode(op, opts=opts)
811 if not opts.no_headers:
813 constants.SF_NODE: "Node",
814 constants.SF_TYPE: "Type",
815 constants.SF_NAME: "Name",
816 constants.SF_SIZE: "Size",
817 constants.SF_USED: "Used",
818 constants.SF_FREE: "Free",
819 constants.SF_ALLOCATABLE: "Allocatable",
824 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
825 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
827 # change raw values to nicer strings
829 for idx, field in enumerate(selected_fields):
831 if field == constants.SF_ALLOCATABLE:
838 data = GenerateTable(separator=opts.separator, headers=headers,
839 fields=selected_fields, unitfields=unitfields,
840 numfields=numfields, data=output, units=opts.units)
848 def ModifyStorage(opts, args):
849 """Modify storage volume on a node.
851 @param opts: the command line options selected by the user
853 @param args: should contain 3 items: node name, storage type and volume name
855 @return: the desired exit code
858 (node_name, user_storage_type, volume_name) = args
860 storage_type = ConvertStorageType(user_storage_type)
864 if opts.allocatable is not None:
865 changes[constants.SF_ALLOCATABLE] = opts.allocatable
868 op = opcodes.OpNodeModifyStorage(node_name=node_name,
869 storage_type=storage_type,
872 SubmitOrSend(op, opts)
874 ToStderr("No changes to perform, exiting.")
877 def RepairStorage(opts, args):
878 """Repairs a storage volume on a node.
880 @param opts: the command line options selected by the user
882 @param args: should contain 3 items: node name, storage type and volume name
884 @return: the desired exit code
887 (node_name, user_storage_type, volume_name) = args
889 storage_type = ConvertStorageType(user_storage_type)
891 op = opcodes.OpRepairNodeStorage(node_name=node_name,
892 storage_type=storage_type,
894 ignore_consistency=opts.ignore_consistency)
895 SubmitOrSend(op, opts)
898 def SetNodeParams(opts, args):
901 @param opts: the command line options selected by the user
903 @param args: should contain only one element, the node name
905 @return: the desired exit code
908 all_changes = [opts.master_candidate, opts.drained, opts.offline,
909 opts.master_capable, opts.vm_capable, opts.secondary_ip,
911 if (all_changes.count(None) == len(all_changes) and
912 not (opts.hv_state or opts.disk_state)):
913 ToStderr("Please give at least one of the parameters.")
917 disk_state = utils.FlatToDict(opts.disk_state)
921 hv_state = dict(opts.hv_state)
923 op = opcodes.OpNodeSetParams(node_name=args[0],
924 master_candidate=opts.master_candidate,
925 offline=opts.offline,
926 drained=opts.drained,
927 master_capable=opts.master_capable,
928 vm_capable=opts.vm_capable,
929 secondary_ip=opts.secondary_ip,
931 ndparams=opts.ndparams,
932 auto_promote=opts.auto_promote,
933 powered=opts.node_powered,
935 disk_state=disk_state)
937 # even if here we process the result, we allow submit only
938 result = SubmitOrSend(op, opts)
941 ToStdout("Modified node %s", args[0])
942 for param, data in result:
943 ToStdout(" - %-5s -> %s", param, data)
947 def RestrictedCommand(opts, args):
948 """Runs a remote command on node(s).
950 @param opts: Command line options selected by user
952 @param args: Command line arguments
959 if len(args) > 1 or opts.nodegroup:
961 nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
963 raise errors.OpPrereqError("Node group or node names must be given",
966 op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
967 use_locking=opts.do_locking)
968 result = SubmitOrSend(op, opts, cl=cl)
970 exit_code = constants.EXIT_SUCCESS
972 for (node, (status, text)) in zip(nodes, result):
973 ToStdout("------------------------------------------------")
975 if opts.show_machine_names:
976 for line in text.splitlines():
977 ToStdout("%s: %s", node, line)
979 ToStdout("Node: %s", node)
982 exit_code = constants.EXIT_FAILURE
988 class ReplyStatus(object):
989 """Class holding a reply status for synchronous confd clients.
997 def ListDrbd(opts, args):
1000 @param opts: the command line options selected by the user
1002 @param args: should contain only one element, the node name
1004 @return: the desired exit code
1008 ToStderr("Please give one (and only one) node.")
1009 return constants.EXIT_FAILURE
1011 if not constants.ENABLE_CONFD:
1012 ToStderr("Error: this command requires confd support, but it has not"
1013 " been enabled at build time.")
1014 return constants.EXIT_FAILURE
1016 status = ReplyStatus()
1018 def ListDrbdConfdCallback(reply):
1019 """Callback for confd queries"""
1020 if reply.type == confd_client.UPCALL_REPLY:
1021 answer = reply.server_reply.answer
1022 reqtype = reply.orig_request.type
1023 if reqtype == constants.CONFD_REQ_NODE_DRBD:
1024 if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
1025 ToStderr("Query gave non-ok status '%s': %s" %
1026 (reply.server_reply.status,
1027 reply.server_reply.answer))
1028 status.failure = True
1030 if not confd.HTNodeDrbd(answer):
1031 ToStderr("Invalid response from server: expected %s, got %s",
1032 confd.HTNodeDrbd, answer)
1033 status.failure = True
1035 status.failure = False
1036 status.answer = answer
1038 ToStderr("Unexpected reply %s!?", reqtype)
1039 status.failure = True
1042 hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
1043 filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
1044 counting_callback = confd_client.ConfdCountingCallback(filter_callback)
1045 cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
1047 req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
1050 def DoConfdRequestReply(req):
1051 counting_callback.RegisterQuery(req.rsalt)
1052 cf_client.SendRequest(req, async=False)
1053 while not counting_callback.AllAnswered():
1054 if not cf_client.ReceiveReply():
1055 ToStderr("Did not receive all expected confd replies")
1058 DoConfdRequestReply(req)
1061 return constants.EXIT_FAILURE
1063 fields = ["node", "minor", "instance", "disk", "role", "peer"]
1067 headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
1068 "disk": "Disk", "role": "Role", "peer": "PeerNode"}
1070 data = GenerateTable(separator=opts.separator, headers=headers,
1071 fields=fields, data=sorted(status.answer),
1072 numfields=["minor"])
1076 return constants.EXIT_SUCCESS
1081 AddNode, [ArgHost(min=1, max=1)],
1082 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
1083 NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
1084 CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
1086 "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
1087 " [--no-node-setup] [--verbose]"
1089 "Add a node to the cluster"),
1091 EvacuateNode, ARGS_ONE_NODE,
1092 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
1093 PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT] + SUBMIT_OPTS,
1094 "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
1095 "Relocate the primary and/or secondary instances from a node"),
1097 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
1098 IALLOCATOR_OPT, PRIORITY_OPT],
1100 "Stops the primary instances on a node and start them on their"
1101 " secondary node (only for instances with drbd disk template)"),
1103 MigrateNode, ARGS_ONE_NODE,
1104 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
1105 IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
1106 NORUNTIME_CHGS_OPT] + SUBMIT_OPTS,
1108 "Migrate all the primary instance on a node away from it"
1109 " (only for instances of type drbd)"),
1111 ShowNodeConfig, ARGS_MANY_NODES, [],
1112 "[<node_name>...]", "Show information about the node(s)"),
1114 ListNodes, ARGS_MANY_NODES,
1115 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1118 "Lists the nodes in the cluster. The available fields can be shown using"
1119 " the \"list-fields\" command (see the man page for details)."
1120 " The default field list is (in order): %s." %
1121 utils.CommaJoin(_LIST_DEF_FIELDS)),
1123 ListNodeFields, [ArgUnknown()],
1124 [NOHDR_OPT, SEP_OPT],
1126 "Lists all available fields for nodes"),
1128 SetNodeParams, ARGS_ONE_NODE,
1129 [FORCE_OPT] + SUBMIT_OPTS +
1130 [MC_OPT, DRAINED_OPT, OFFLINE_OPT,
1131 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
1132 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
1133 NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
1134 "<node_name>", "Alters the parameters of a node"),
1136 PowercycleNode, ARGS_ONE_NODE,
1137 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1138 "<node_name>", "Tries to forcefully powercycle a node"),
1141 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
1144 [AUTO_PROMOTE_OPT, PRIORITY_OPT,
1145 IGNORE_STATUS_OPT, FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT,
1147 "on|off|cycle|status [nodes...]",
1148 "Change power state of node by calling out-of-band helper."),
1150 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
1151 "<node_name>", "Removes a node from the cluster"),
1153 ListVolumes, [ArgNode()],
1154 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
1155 "[<node_name>...]", "List logical volumes on node(s)"),
1157 ListStorage, ARGS_MANY_NODES,
1158 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
1160 "[<node_name>...]", "List physical volumes on node(s). The available"
1161 " fields are (see the man page for details): %s." %
1162 (utils.CommaJoin(_LIST_STOR_HEADERS))),
1165 [ArgNode(min=1, max=1),
1166 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
1167 ArgFile(min=1, max=1)],
1168 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1169 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
1172 [ArgNode(min=1, max=1),
1173 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
1174 ArgFile(min=1, max=1)],
1175 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1176 "<node_name> <storage_type> <name>",
1177 "Repairs a storage volume on a node"),
1179 ListTags, ARGS_ONE_NODE, [],
1180 "<node_name>", "List the tags of the given node"),
1182 AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
1183 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1184 "<node_name> tag...", "Add tags to the given node"),
1186 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
1187 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1188 "<node_name> tag...", "Remove tags from the given node"),
1190 Health, ARGS_MANY_NODES,
1191 [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
1192 "[<node_name>...]", "List health of node(s) using out-of-band"),
1194 ListDrbd, ARGS_ONE_NODE,
1195 [NOHDR_OPT, SEP_OPT],
1196 "[<node_name>]", "Query the list of used DRBD minors on the given node"),
1197 "restricted-command": (
1198 RestrictedCommand, [ArgUnknown(min=1, max=1)] + ARGS_MANY_NODES,
1199 [SYNC_OPT, PRIORITY_OPT] + SUBMIT_OPTS + [SHOW_MACHINE_OPT, NODEGROUP_OPT],
1200 "<command> <node_name> [<node_name>...]",
1201 "Executes a restricted command on node(s)"),
1204 #: dictionary with aliases for commands
1211 return GenericMain(commands, aliases=aliases,
1212 override={"tag_type": constants.TAG_NODE},
1213 env_override=_ENV_OVERRIDE)