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)"))
126 OVS_OPT = cli_option("--ovs", default=False, action="store_true", dest="ovs",
127 help=("Enable OpenvSwitch on the new node. This will"
128 " initialize OpenvSwitch during gnt-node add"))
130 OVS_NAME_OPT = cli_option("--ovs-name", action="store", dest="ovs_name",
131 type="string", default=None,
132 help=("Set name of OpenvSwitch to connect instances"))
134 OVS_LINK_OPT = cli_option("--ovs-link", action="store", dest="ovs_link",
135 type="string", default=None,
136 help=("Physical trunk interface for OpenvSwitch"))
139 def ConvertStorageType(user_storage_type):
140 """Converts a user storage type to its internal name.
144 return _USER_STORAGE_TYPE[user_storage_type]
146 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
150 def _TryReadFile(path):
151 """Tries to read a file.
153 If the file is not found, C{None} is returned.
156 @param path: Filename
157 @rtype: None or string
158 @todo: Consider adding a generic ENOENT wrapper
162 return utils.ReadFile(path)
163 except EnvironmentError, err:
164 if err.errno == errno.ENOENT:
170 def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
171 """Reads SSH keys according to C{keyfiles}.
174 @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values
175 tuples (private and public key file)
177 @return: List of three-values tuples (L{constants.SSHK_ALL}, private and
178 public key as strings)
183 for (kind, (private_file, public_file)) in keyfiles.items():
184 private_key = _TryReadFile(private_file)
185 public_key = _TryReadFile(public_file)
187 if public_key and private_key:
188 result.append((kind, private_key, public_key))
189 elif public_key or private_key:
190 _tostderr_fn("Couldn't find a complete set of keys for kind '%s'; files"
191 " '%s' and '%s'", kind, private_file, public_file)
196 def _SetupSSH(options, cluster_name, node):
197 """Configures a destination node's SSH daemon.
199 @param options: Command line options
201 @param cluster_name: Cluster name
203 @param node: Destination node name
206 if options.force_join:
207 ToStderr("The \"--force-join\" option is no longer supported and will be"
210 host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
212 (_, root_keyfiles) = \
213 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
215 root_keys = _ReadSshKeys(root_keyfiles)
218 utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
221 constants.SSHS_CLUSTER_NAME: cluster_name,
222 constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
223 constants.SSHS_SSH_HOST_KEY: host_keys,
224 constants.SSHS_SSH_ROOT_KEY: root_keys,
227 bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
228 options.debug, options.verbose, False,
229 options.ssh_key_check, options.ssh_key_check, data)
233 def AddNode(opts, args):
234 """Add a node to the cluster.
236 @param opts: the command line options selected by the user
238 @param args: should contain only one element, the new node name
240 @return: the desired exit code
244 node = netutils.GetHostname(name=args[0]).name
248 output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
250 node_exists, sip, is_master = output[0]
251 except (errors.OpPrereqError, errors.OpExecError):
257 ToStderr("Node %s not in the cluster"
258 " - please retry without '--readd'", node)
261 ToStderr("Node %s is the master, cannot readd", node)
265 ToStderr("Node %s already in the cluster (as %s)"
266 " - please retry with '--readd'", node, node_exists)
268 sip = opts.secondary_ip
270 # read the cluster name from the master
271 (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
273 if not readd and opts.node_setup:
274 ToStderr("-- WARNING -- \n"
275 "Performing this operation is going to replace the ssh daemon"
277 "on the target machine (%s) with the ones of the"
279 "and grant full intra-cluster ssh root access to/from it\n", node)
282 _SetupSSH(opts, cluster_name, node)
284 bootstrap.SetupNodeDaemon(opts, cluster_name, node)
287 disk_state = utils.FlatToDict(opts.disk_state)
291 hv_state = dict(opts.hv_state)
293 if not opts.ndparams:
294 ndparams = {constants.ND_OVS: opts.ovs,
295 constants.ND_OVS_NAME: opts.ovs_name,
296 constants.ND_OVS_LINK: opts.ovs_link}
298 ndparams = opts.ndparams
299 ndparams[constants.ND_OVS] = opts.ovs
300 ndparams[constants.ND_OVS_NAME] = opts.ovs_name
301 ndparams[constants.ND_OVS_LINK] = opts.ovs_link
303 op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
304 readd=opts.readd, group=opts.nodegroup,
305 vm_capable=opts.vm_capable, ndparams=ndparams,
306 master_capable=opts.master_capable,
307 disk_state=disk_state,
309 SubmitOpCode(op, opts=opts)
312 def ListNodes(opts, args):
313 """List nodes and their properties.
315 @param opts: the command line options selected by the user
317 @param args: nodes to list, or empty for all
319 @return: the desired exit code
322 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
324 fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
327 cl = GetClient(query=True)
329 return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
330 opts.separator, not opts.no_headers,
331 format_override=fmtoverride, verbose=opts.verbose,
332 force_filter=opts.force_filter, cl=cl)
335 def ListNodeFields(opts, args):
338 @param opts: the command line options selected by the user
340 @param args: fields to list, or empty for all
342 @return: the desired exit code
345 cl = GetClient(query=True)
347 return GenericListFields(constants.QR_NODE, args, opts.separator,
348 not opts.no_headers, cl=cl)
351 def EvacuateNode(opts, args):
352 """Relocate all secondary instance from a node.
354 @param opts: the command line options selected by the user
356 @param args: should be an empty list
358 @return: the desired exit code
361 if opts.dst_node is not None:
362 ToStderr("New secondary node given (disabling iallocator), hence evacuating"
363 " secondary instances only.")
364 opts.secondary_only = True
365 opts.primary_only = False
367 if opts.secondary_only and opts.primary_only:
368 raise errors.OpPrereqError("Only one of the --primary-only and"
369 " --secondary-only options can be passed",
371 elif opts.primary_only:
372 mode = constants.NODE_EVAC_PRI
373 elif opts.secondary_only:
374 mode = constants.NODE_EVAC_SEC
376 mode = constants.NODE_EVAC_ALL
378 # Determine affected instances
381 if not opts.secondary_only:
382 fields.append("pinst_list")
383 if not opts.primary_only:
384 fields.append("sinst_list")
388 qcl = GetClient(query=True)
389 result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
392 instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
395 # No instances to evacuate
396 ToStderr("No instances to evacuate on node(s) %s, exiting.",
397 utils.CommaJoin(args))
398 return constants.EXIT_SUCCESS
400 if not (opts.force or
401 AskUser("Relocate instance(s) %s from node(s) %s?" %
402 (utils.CommaJoin(utils.NiceSort(instances)),
403 utils.CommaJoin(args)))):
404 return constants.EXIT_CONFIRMATION
407 op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
408 remote_node=opts.dst_node,
409 iallocator=opts.iallocator,
410 early_release=opts.early_release)
411 result = SubmitOrSend(op, opts, cl=cl)
413 # Keep track of submitted jobs
414 jex = JobExecutor(cl=cl, opts=opts)
416 for (status, job_id) in result[constants.JOB_IDS_KEY]:
417 jex.AddJobId(None, status, job_id)
419 results = jex.GetResults()
420 bad_cnt = len([row for row in results if not row[0]])
422 ToStdout("All instances evacuated successfully.")
423 rcode = constants.EXIT_SUCCESS
425 ToStdout("There were %s errors during the evacuation.", bad_cnt)
426 rcode = constants.EXIT_FAILURE
431 def FailoverNode(opts, args):
432 """Failover all primary instance on a node.
434 @param opts: the command line options selected by the user
436 @param args: should be an empty list
438 @return: the desired exit code
443 selected_fields = ["name", "pinst_list"]
445 # these fields are static data anyway, so it doesn't matter, but
446 # locking=True should be safer
447 qcl = GetClient(query=True)
448 result = cl.QueryNodes(names=args, fields=selected_fields,
451 node, pinst = result[0]
454 ToStderr("No primary instances on node %s, exiting.", node)
457 pinst = utils.NiceSort(pinst)
461 if not force and not AskUser("Fail over instance(s) %s?" %
462 (",".join("'%s'" % name for name in pinst))):
465 jex = JobExecutor(cl=cl, opts=opts)
467 op = opcodes.OpInstanceFailover(instance_name=iname,
468 ignore_consistency=opts.ignore_consistency,
469 iallocator=opts.iallocator)
470 jex.QueueJob(iname, op)
471 results = jex.GetResults()
472 bad_cnt = len([row for row in results if not row[0]])
474 ToStdout("All %d instance(s) failed over successfully.", len(results))
476 ToStdout("There were errors during the failover:\n"
477 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
481 def MigrateNode(opts, args):
482 """Migrate all primary instance on a node.
487 selected_fields = ["name", "pinst_list"]
489 qcl = GetClient(query=True)
490 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
492 ((node, pinst), ) = result
495 ToStdout("No primary instances on node %s, exiting." % node)
498 pinst = utils.NiceSort(pinst)
501 AskUser("Migrate instance(s) %s?" %
502 utils.CommaJoin(utils.NiceSort(pinst)))):
503 return constants.EXIT_CONFIRMATION
505 # this should be removed once --non-live is deprecated
506 if not opts.live and opts.migration_mode is not None:
507 raise errors.OpPrereqError("Only one of the --non-live and "
508 "--migration-mode options can be passed",
510 if not opts.live: # --non-live passed
511 mode = constants.HT_MIGRATION_NONLIVE
513 mode = opts.migration_mode
515 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
516 iallocator=opts.iallocator,
517 target_node=opts.dst_node,
518 allow_runtime_changes=opts.allow_runtime_chgs,
519 ignore_ipolicy=opts.ignore_ipolicy)
521 result = SubmitOrSend(op, opts, cl=cl)
523 # Keep track of submitted jobs
524 jex = JobExecutor(cl=cl, opts=opts)
526 for (status, job_id) in result[constants.JOB_IDS_KEY]:
527 jex.AddJobId(None, status, job_id)
529 results = jex.GetResults()
530 bad_cnt = len([row for row in results if not row[0]])
532 ToStdout("All instances migrated successfully.")
533 rcode = constants.EXIT_SUCCESS
535 ToStdout("There were %s errors during the node migration.", bad_cnt)
536 rcode = constants.EXIT_FAILURE
541 def _FormatNodeInfo(node_info):
542 """Format node information for L{cli.PrintGenericInfo()}.
545 (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
546 master_capable, vm_capable, powered, ndparams, ndparams_custom) = node_info
549 ("primary ip", primary_ip),
550 ("secondary ip", secondary_ip),
551 ("master candidate", is_mc),
552 ("drained", drained),
553 ("offline", offline),
555 if powered is not None:
556 info.append(("powered", powered))
558 ("master_capable", master_capable),
559 ("vm_capable", vm_capable),
563 ("primary for instances",
564 [iname for iname in utils.NiceSort(pinst)]),
565 ("secondary for instances",
566 [iname for iname in utils.NiceSort(sinst)]),
568 info.append(("node parameters",
569 FormatParamsDictInfo(ndparams_custom, ndparams)))
573 def ShowNodeConfig(opts, args):
574 """Show node information.
576 @param opts: the command line options selected by the user
578 @param args: should either be an empty list, in which case
579 we show information about all nodes, or should contain
580 a list of nodes to be queried for information
582 @return: the desired exit code
585 cl = GetClient(query=True)
586 result = cl.QueryNodes(fields=["name", "pip", "sip",
587 "pinst_list", "sinst_list",
588 "master_candidate", "drained", "offline",
589 "master_capable", "vm_capable", "powered",
590 "ndparams", "custom_ndparams"],
591 names=args, use_locking=False)
593 _FormatNodeInfo(node_info)
594 for node_info in result
599 def RemoveNode(opts, args):
600 """Remove a node from the cluster.
602 @param opts: the command line options selected by the user
604 @param args: should contain only one element, the name of
605 the node to be removed
607 @return: the desired exit code
610 op = opcodes.OpNodeRemove(node_name=args[0])
611 SubmitOpCode(op, opts=opts)
615 def PowercycleNode(opts, args):
616 """Remove a node from the cluster.
618 @param opts: the command line options selected by the user
620 @param args: should contain only one element, the name of
621 the node to be removed
623 @return: the desired exit code
627 if (not opts.confirm and
628 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
631 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
632 result = SubmitOrSend(op, opts)
638 def PowerNode(opts, args):
639 """Change/ask power state of a node.
641 @param opts: the command line options selected by the user
643 @param args: should contain only one element, the name of
644 the node to be removed
646 @return: the desired exit code
649 command = args.pop(0)
654 headers = {"node": "Node", "status": "Status"}
656 if command not in _LIST_POWER_COMMANDS:
657 ToStderr("power subcommand %s not supported." % command)
658 return constants.EXIT_FAILURE
660 oob_command = "power-%s" % command
662 if oob_command in _OOB_COMMAND_ASK:
664 ToStderr("Please provide at least one node for this command")
665 return constants.EXIT_FAILURE
666 elif not opts.force and not ConfirmOperation(args, "nodes",
667 "power %s" % command):
668 return constants.EXIT_FAILURE
672 if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
673 # TODO: This is a little ugly as we can't catch and revert
675 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
676 auto_promote=opts.auto_promote))
678 opcodelist.append(opcodes.OpOobCommand(node_names=args,
680 ignore_status=opts.ignore_status,
681 timeout=opts.oob_timeout,
682 power_delay=opts.power_delay))
684 cli.SetGenericOpcodeOpts(opcodelist, opts)
686 job_id = cli.SendJob(opcodelist)
688 # We just want the OOB Opcode status
689 # If it fails PollJob gives us the error message in it
690 result = cli.PollJob(job_id)[-1]
694 for node_result in result:
695 (node_tuple, data_tuple) = node_result
696 (_, node_name) = node_tuple
697 (data_status, data_node) = data_tuple
698 if data_status == constants.RS_NORMAL:
699 if oob_command == constants.OOB_POWER_STATUS:
700 if data_node[constants.OOB_POWER_STATUS_POWERED]:
704 data.append([node_name, text])
706 # We don't expect data here, so we just say, it was successfully invoked
707 data.append([node_name, "invoked"])
710 data.append([node_name, cli.FormatResultError(data_status, True)])
712 data = GenerateTable(separator=opts.separator, headers=headers,
713 fields=["node", "status"], data=data)
719 return constants.EXIT_FAILURE
721 return constants.EXIT_SUCCESS
724 def Health(opts, args):
725 """Show health of a node using OOB.
727 @param opts: the command line options selected by the user
729 @param args: should contain only one element, the name of
730 the node to be removed
732 @return: the desired exit code
735 op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
736 timeout=opts.oob_timeout)
737 result = SubmitOpCode(op, opts=opts)
742 headers = {"node": "Node", "status": "Status"}
746 for node_result in result:
747 (node_tuple, data_tuple) = node_result
748 (_, node_name) = node_tuple
749 (data_status, data_node) = data_tuple
750 if data_status == constants.RS_NORMAL:
751 data.append([node_name, "%s=%s" % tuple(data_node[0])])
752 for item, status in data_node[1:]:
753 data.append(["", "%s=%s" % (item, status)])
756 data.append([node_name, cli.FormatResultError(data_status, True)])
758 data = GenerateTable(separator=opts.separator, headers=headers,
759 fields=["node", "status"], data=data)
765 return constants.EXIT_FAILURE
767 return constants.EXIT_SUCCESS
770 def ListVolumes(opts, args):
771 """List logical volumes on node(s).
773 @param opts: the command line options selected by the user
775 @param args: should either be an empty list, in which case
776 we list data for all nodes, or contain a list of nodes
777 to display data only for those
779 @return: the desired exit code
782 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
784 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
785 output = SubmitOpCode(op, opts=opts)
787 if not opts.no_headers:
788 headers = {"node": "Node", "phys": "PhysDev",
789 "vg": "VG", "name": "Name",
790 "size": "Size", "instance": "Instance"}
794 unitfields = ["size"]
798 data = GenerateTable(separator=opts.separator, headers=headers,
799 fields=selected_fields, unitfields=unitfields,
800 numfields=numfields, data=output, units=opts.units)
808 def ListStorage(opts, args):
809 """List physical volumes on node(s).
811 @param opts: the command line options selected by the user
813 @param args: should either be an empty list, in which case
814 we list data for all nodes, or contain a list of nodes
815 to display data only for those
817 @return: the desired exit code
820 # TODO: Default to ST_FILE if LVM is disabled on the cluster
821 if opts.user_storage_type is None:
822 opts.user_storage_type = constants.ST_LVM_PV
824 storage_type = ConvertStorageType(opts.user_storage_type)
826 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
828 op = opcodes.OpNodeQueryStorage(nodes=args,
829 storage_type=storage_type,
830 output_fields=selected_fields)
831 output = SubmitOpCode(op, opts=opts)
833 if not opts.no_headers:
835 constants.SF_NODE: "Node",
836 constants.SF_TYPE: "Type",
837 constants.SF_NAME: "Name",
838 constants.SF_SIZE: "Size",
839 constants.SF_USED: "Used",
840 constants.SF_FREE: "Free",
841 constants.SF_ALLOCATABLE: "Allocatable",
846 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
847 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
849 # change raw values to nicer strings
851 for idx, field in enumerate(selected_fields):
853 if field == constants.SF_ALLOCATABLE:
860 data = GenerateTable(separator=opts.separator, headers=headers,
861 fields=selected_fields, unitfields=unitfields,
862 numfields=numfields, data=output, units=opts.units)
870 def ModifyStorage(opts, args):
871 """Modify storage volume on a node.
873 @param opts: the command line options selected by the user
875 @param args: should contain 3 items: node name, storage type and volume name
877 @return: the desired exit code
880 (node_name, user_storage_type, volume_name) = args
882 storage_type = ConvertStorageType(user_storage_type)
886 if opts.allocatable is not None:
887 changes[constants.SF_ALLOCATABLE] = opts.allocatable
890 op = opcodes.OpNodeModifyStorage(node_name=node_name,
891 storage_type=storage_type,
894 SubmitOrSend(op, opts)
896 ToStderr("No changes to perform, exiting.")
899 def RepairStorage(opts, args):
900 """Repairs a storage volume on a node.
902 @param opts: the command line options selected by the user
904 @param args: should contain 3 items: node name, storage type and volume name
906 @return: the desired exit code
909 (node_name, user_storage_type, volume_name) = args
911 storage_type = ConvertStorageType(user_storage_type)
913 op = opcodes.OpRepairNodeStorage(node_name=node_name,
914 storage_type=storage_type,
916 ignore_consistency=opts.ignore_consistency)
917 SubmitOrSend(op, opts)
920 def SetNodeParams(opts, args):
923 @param opts: the command line options selected by the user
925 @param args: should contain only one element, the node name
927 @return: the desired exit code
930 all_changes = [opts.master_candidate, opts.drained, opts.offline,
931 opts.master_capable, opts.vm_capable, opts.secondary_ip,
933 if (all_changes.count(None) == len(all_changes) and
934 not (opts.hv_state or opts.disk_state)):
935 ToStderr("Please give at least one of the parameters.")
939 disk_state = utils.FlatToDict(opts.disk_state)
943 hv_state = dict(opts.hv_state)
945 op = opcodes.OpNodeSetParams(node_name=args[0],
946 master_candidate=opts.master_candidate,
947 offline=opts.offline,
948 drained=opts.drained,
949 master_capable=opts.master_capable,
950 vm_capable=opts.vm_capable,
951 secondary_ip=opts.secondary_ip,
953 ndparams=opts.ndparams,
954 auto_promote=opts.auto_promote,
955 powered=opts.node_powered,
957 disk_state=disk_state)
959 # even if here we process the result, we allow submit only
960 result = SubmitOrSend(op, opts)
963 ToStdout("Modified node %s", args[0])
964 for param, data in result:
965 ToStdout(" - %-5s -> %s", param, data)
969 def RestrictedCommand(opts, args):
970 """Runs a remote command on node(s).
972 @param opts: Command line options selected by user
974 @param args: Command line arguments
981 if len(args) > 1 or opts.nodegroup:
983 nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
985 raise errors.OpPrereqError("Node group or node names must be given",
988 op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
989 use_locking=opts.do_locking)
990 result = SubmitOrSend(op, opts, cl=cl)
992 exit_code = constants.EXIT_SUCCESS
994 for (node, (status, text)) in zip(nodes, result):
995 ToStdout("------------------------------------------------")
997 if opts.show_machine_names:
998 for line in text.splitlines():
999 ToStdout("%s: %s", node, line)
1001 ToStdout("Node: %s", node)
1004 exit_code = constants.EXIT_FAILURE
1010 class ReplyStatus(object):
1011 """Class holding a reply status for synchronous confd clients.
1019 def ListDrbd(opts, args):
1022 @param opts: the command line options selected by the user
1024 @param args: should contain only one element, the node name
1026 @return: the desired exit code
1030 ToStderr("Please give one (and only one) node.")
1031 return constants.EXIT_FAILURE
1033 if not constants.ENABLE_CONFD:
1034 ToStderr("Error: this command requires confd support, but it has not"
1035 " been enabled at build time.")
1036 return constants.EXIT_FAILURE
1038 status = ReplyStatus()
1040 def ListDrbdConfdCallback(reply):
1041 """Callback for confd queries"""
1042 if reply.type == confd_client.UPCALL_REPLY:
1043 answer = reply.server_reply.answer
1044 reqtype = reply.orig_request.type
1045 if reqtype == constants.CONFD_REQ_NODE_DRBD:
1046 if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
1047 ToStderr("Query gave non-ok status '%s': %s" %
1048 (reply.server_reply.status,
1049 reply.server_reply.answer))
1050 status.failure = True
1052 if not confd.HTNodeDrbd(answer):
1053 ToStderr("Invalid response from server: expected %s, got %s",
1054 confd.HTNodeDrbd, answer)
1055 status.failure = True
1057 status.failure = False
1058 status.answer = answer
1060 ToStderr("Unexpected reply %s!?", reqtype)
1061 status.failure = True
1064 hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
1065 filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
1066 counting_callback = confd_client.ConfdCountingCallback(filter_callback)
1067 cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
1069 req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
1072 def DoConfdRequestReply(req):
1073 counting_callback.RegisterQuery(req.rsalt)
1074 cf_client.SendRequest(req, async=False)
1075 while not counting_callback.AllAnswered():
1076 if not cf_client.ReceiveReply():
1077 ToStderr("Did not receive all expected confd replies")
1080 DoConfdRequestReply(req)
1083 return constants.EXIT_FAILURE
1085 fields = ["node", "minor", "instance", "disk", "role", "peer"]
1089 headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
1090 "disk": "Disk", "role": "Role", "peer": "PeerNode"}
1092 data = GenerateTable(separator=opts.separator, headers=headers,
1093 fields=fields, data=sorted(status.answer),
1094 numfields=["minor"])
1098 return constants.EXIT_SUCCESS
1103 AddNode, [ArgHost(min=1, max=1)],
1104 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
1105 NONODE_SETUP_OPT, VERBOSE_OPT, OVS_OPT, OVS_NAME_OPT, OVS_LINK_OPT,
1106 NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT, CAPAB_VM_OPT,
1107 NODE_PARAMS_OPT, HV_STATE_OPT, DISK_STATE_OPT],
1108 "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
1109 " [--no-node-setup] [--verbose] [--network] [--ovs] [--ovs-name <vswitch>]"
1110 " [--ovs-link <phys. if>] <node_name>",
1111 "Add a node to the cluster"),
1113 EvacuateNode, ARGS_ONE_NODE,
1114 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
1115 PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT] + SUBMIT_OPTS,
1116 "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
1117 "Relocate the primary and/or secondary instances from a node"),
1119 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
1120 IALLOCATOR_OPT, PRIORITY_OPT],
1122 "Stops the primary instances on a node and start them on their"
1123 " secondary node (only for instances with drbd disk template)"),
1125 MigrateNode, ARGS_ONE_NODE,
1126 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
1127 IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
1128 NORUNTIME_CHGS_OPT] + SUBMIT_OPTS,
1130 "Migrate all the primary instance on a node away from it"
1131 " (only for instances of type drbd)"),
1133 ShowNodeConfig, ARGS_MANY_NODES, [],
1134 "[<node_name>...]", "Show information about the node(s)"),
1136 ListNodes, ARGS_MANY_NODES,
1137 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1140 "Lists the nodes in the cluster. The available fields can be shown using"
1141 " the \"list-fields\" command (see the man page for details)."
1142 " The default field list is (in order): %s." %
1143 utils.CommaJoin(_LIST_DEF_FIELDS)),
1145 ListNodeFields, [ArgUnknown()],
1146 [NOHDR_OPT, SEP_OPT],
1148 "Lists all available fields for nodes"),
1150 SetNodeParams, ARGS_ONE_NODE,
1151 [FORCE_OPT] + SUBMIT_OPTS +
1152 [MC_OPT, DRAINED_OPT, OFFLINE_OPT,
1153 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
1154 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
1155 NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
1156 "<node_name>", "Alters the parameters of a node"),
1158 PowercycleNode, ARGS_ONE_NODE,
1159 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1160 "<node_name>", "Tries to forcefully powercycle a node"),
1163 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
1166 [AUTO_PROMOTE_OPT, PRIORITY_OPT,
1167 IGNORE_STATUS_OPT, FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT,
1169 "on|off|cycle|status [nodes...]",
1170 "Change power state of node by calling out-of-band helper."),
1172 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
1173 "<node_name>", "Removes a node from the cluster"),
1175 ListVolumes, [ArgNode()],
1176 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
1177 "[<node_name>...]", "List logical volumes on node(s)"),
1179 ListStorage, ARGS_MANY_NODES,
1180 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
1182 "[<node_name>...]", "List physical volumes on node(s). The available"
1183 " fields are (see the man page for details): %s." %
1184 (utils.CommaJoin(_LIST_STOR_HEADERS))),
1187 [ArgNode(min=1, max=1),
1188 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
1189 ArgFile(min=1, max=1)],
1190 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1191 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
1194 [ArgNode(min=1, max=1),
1195 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
1196 ArgFile(min=1, max=1)],
1197 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1198 "<node_name> <storage_type> <name>",
1199 "Repairs a storage volume on a node"),
1201 ListTags, ARGS_ONE_NODE, [],
1202 "<node_name>", "List the tags of the given node"),
1204 AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
1205 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1206 "<node_name> tag...", "Add tags to the given node"),
1208 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
1209 [TAG_SRC_OPT, PRIORITY_OPT] + SUBMIT_OPTS,
1210 "<node_name> tag...", "Remove tags from the given node"),
1212 Health, ARGS_MANY_NODES,
1213 [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
1214 "[<node_name>...]", "List health of node(s) using out-of-band"),
1216 ListDrbd, ARGS_ONE_NODE,
1217 [NOHDR_OPT, SEP_OPT],
1218 "[<node_name>]", "Query the list of used DRBD minors on the given node"),
1219 "restricted-command": (
1220 RestrictedCommand, [ArgUnknown(min=1, max=1)] + ARGS_MANY_NODES,
1221 [SYNC_OPT, PRIORITY_OPT] + SUBMIT_OPTS + [SHOW_MACHINE_OPT, NODEGROUP_OPT],
1222 "<command> <node_name> [<node_name>...]",
1223 "Executes a restricted command on node(s)"),
1226 #: dictionary with aliases for commands
1233 return GenericMain(commands, aliases=aliases,
1234 override={"tag_type": constants.TAG_NODE},
1235 env_override=_ENV_OVERRIDE)