4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
43 from cStringIO import StringIO
45 from ganeti import confd
46 from ganeti.confd import client as confd_client
48 #: default list of field for L{ListNodes}
50 "name", "dtotal", "dfree",
51 "mtotal", "mnode", "mfree",
52 "pinst_cnt", "sinst_cnt",
56 #: Default field list for L{ListVolumes}
57 _LIST_VOL_DEF_FIELDS = ["node", "phys", "vg", "name", "size", "instance"]
60 #: default list of field for L{ListStorage}
61 _LIST_STOR_DEF_FIELDS = [
68 constants.SF_ALLOCATABLE,
72 #: default list of power commands
73 _LIST_POWER_COMMANDS = ["on", "off", "cycle", "status"]
76 #: headers (and full field list) for L{ListStorage}
77 _LIST_STOR_HEADERS = {
78 constants.SF_NODE: "Node",
79 constants.SF_TYPE: "Type",
80 constants.SF_NAME: "Name",
81 constants.SF_SIZE: "Size",
82 constants.SF_USED: "Used",
83 constants.SF_FREE: "Free",
84 constants.SF_ALLOCATABLE: "Allocatable",
88 #: User-facing storage unit types
89 _USER_STORAGE_TYPE = {
90 constants.ST_FILE: "file",
91 constants.ST_LVM_PV: "lvm-pv",
92 constants.ST_LVM_VG: "lvm-vg",
96 cli_option("-t", "--storage-type",
97 dest="user_storage_type",
98 choices=_USER_STORAGE_TYPE.keys(),
100 metavar="STORAGE_TYPE",
101 help=("Storage type (%s)" %
102 utils.CommaJoin(_USER_STORAGE_TYPE.keys())))
104 _REPAIRABLE_STORAGE_TYPES = \
105 [st for st, so in constants.VALID_STORAGE_OPERATIONS.iteritems()
106 if constants.SO_FIX_CONSISTENCY in so]
108 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
110 _OOB_COMMAND_ASK = compat.UniqueFrozenset([
111 constants.OOB_POWER_OFF,
112 constants.OOB_POWER_CYCLE,
115 _ENV_OVERRIDE = compat.UniqueFrozenset(["list"])
117 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
118 action="store_false", dest="node_setup",
119 help=("Do not make initial SSH setup on remote"
120 " node (needs to be done manually)"))
122 IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
123 action="store_true", dest="ignore_status",
124 help=("Ignore the Node(s) offline status"
125 " (potentially DANGEROUS)"))
128 def ConvertStorageType(user_storage_type):
129 """Converts a user storage type to its internal name.
133 return _USER_STORAGE_TYPE[user_storage_type]
135 raise errors.OpPrereqError("Unknown storage type: %s" % user_storage_type,
139 def _TryReadFile(path):
140 """Tries to read a file.
142 If the file is not found, C{None} is returned.
145 @param path: Filename
146 @rtype: None or string
147 @todo: Consider adding a generic ENOENT wrapper
151 return utils.ReadFile(path)
152 except EnvironmentError, err:
153 if err.errno == errno.ENOENT:
159 def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
160 """Reads SSH keys according to C{keyfiles}.
163 @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values
164 tuples (private and public key file)
166 @return: List of three-values tuples (L{constants.SSHK_ALL}, private and
167 public key as strings)
172 for (kind, (private_file, public_file)) in keyfiles.items():
173 private_key = _TryReadFile(private_file)
174 public_key = _TryReadFile(public_file)
176 if public_key and private_key:
177 result.append((kind, private_key, public_key))
178 elif public_key or private_key:
179 _tostderr_fn("Couldn't find a complete set of keys for kind '%s'; files"
180 " '%s' and '%s'", kind, private_file, public_file)
185 def _SetupSSH(options, cluster_name, node):
186 """Configures a destination node's SSH daemon.
188 @param options: Command line options
190 @param cluster_name: Cluster name
192 @param node: Destination node name
195 if options.force_join:
196 ToStderr("The \"--force-join\" option is no longer supported and will be"
199 host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
201 (_, root_keyfiles) = \
202 ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
204 root_keys = _ReadSshKeys(root_keyfiles)
207 utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
210 constants.SSHS_CLUSTER_NAME: cluster_name,
211 constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
212 constants.SSHS_SSH_HOST_KEY: host_keys,
213 constants.SSHS_SSH_ROOT_KEY: root_keys,
216 bootstrap.RunNodeSetupCmd(cluster_name, node, pathutils.PREPARE_NODE_JOIN,
217 options.debug, options.verbose, False,
218 options.ssh_key_check, options.ssh_key_check, data)
222 def AddNode(opts, args):
223 """Add a node to the cluster.
225 @param opts: the command line options selected by the user
227 @param args: should contain only one element, the new node name
229 @return: the desired exit code
233 node = netutils.GetHostname(name=args[0]).name
237 output = cl.QueryNodes(names=[node], fields=["name", "sip", "master"],
239 node_exists, sip, is_master = output[0]
240 except (errors.OpPrereqError, errors.OpExecError):
246 ToStderr("Node %s not in the cluster"
247 " - please retry without '--readd'", node)
250 ToStderr("Node %s is the master, cannot readd", node)
254 ToStderr("Node %s already in the cluster (as %s)"
255 " - please retry with '--readd'", node, node_exists)
257 sip = opts.secondary_ip
259 # read the cluster name from the master
260 (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
262 if not readd and opts.node_setup:
263 ToStderr("-- WARNING -- \n"
264 "Performing this operation is going to replace the ssh daemon"
266 "on the target machine (%s) with the ones of the"
268 "and grant full intra-cluster ssh root access to/from it\n", node)
271 _SetupSSH(opts, cluster_name, node)
273 bootstrap.SetupNodeDaemon(opts, cluster_name, node)
276 disk_state = utils.FlatToDict(opts.disk_state)
280 hv_state = dict(opts.hv_state)
282 op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
283 readd=opts.readd, group=opts.nodegroup,
284 vm_capable=opts.vm_capable, ndparams=opts.ndparams,
285 master_capable=opts.master_capable,
286 disk_state=disk_state,
288 SubmitOpCode(op, opts=opts)
291 def ListNodes(opts, args):
292 """List nodes and their properties.
294 @param opts: the command line options selected by the user
296 @param args: nodes to list, or empty for all
298 @return: the desired exit code
301 selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
303 fmtoverride = dict.fromkeys(["pinst_list", "sinst_list", "tags"],
306 cl = GetClient(query=True)
308 return GenericList(constants.QR_NODE, selected_fields, args, opts.units,
309 opts.separator, not opts.no_headers,
310 format_override=fmtoverride, verbose=opts.verbose,
311 force_filter=opts.force_filter, cl=cl)
314 def ListNodeFields(opts, args):
317 @param opts: the command line options selected by the user
319 @param args: fields to list, or empty for all
321 @return: the desired exit code
324 cl = GetClient(query=True)
326 return GenericListFields(constants.QR_NODE, args, opts.separator,
327 not opts.no_headers, cl=cl)
330 def EvacuateNode(opts, args):
331 """Relocate all secondary instance from a node.
333 @param opts: the command line options selected by the user
335 @param args: should be an empty list
337 @return: the desired exit code
340 if opts.dst_node is not None:
341 ToStderr("New secondary node given (disabling iallocator), hence evacuating"
342 " secondary instances only.")
343 opts.secondary_only = True
344 opts.primary_only = False
346 if opts.secondary_only and opts.primary_only:
347 raise errors.OpPrereqError("Only one of the --primary-only and"
348 " --secondary-only options can be passed",
350 elif opts.primary_only:
351 mode = constants.NODE_EVAC_PRI
352 elif opts.secondary_only:
353 mode = constants.NODE_EVAC_SEC
355 mode = constants.NODE_EVAC_ALL
357 # Determine affected instances
360 if not opts.secondary_only:
361 fields.append("pinst_list")
362 if not opts.primary_only:
363 fields.append("sinst_list")
367 qcl = GetClient(query=True)
368 result = qcl.QueryNodes(names=args, fields=fields, use_locking=False)
371 instances = set(itertools.chain(*itertools.chain(*itertools.chain(result))))
374 # No instances to evacuate
375 ToStderr("No instances to evacuate on node(s) %s, exiting.",
376 utils.CommaJoin(args))
377 return constants.EXIT_SUCCESS
379 if not (opts.force or
380 AskUser("Relocate instance(s) %s from node(s) %s?" %
381 (utils.CommaJoin(utils.NiceSort(instances)),
382 utils.CommaJoin(args)))):
383 return constants.EXIT_CONFIRMATION
386 op = opcodes.OpNodeEvacuate(node_name=args[0], mode=mode,
387 remote_node=opts.dst_node,
388 iallocator=opts.iallocator,
389 early_release=opts.early_release)
390 result = SubmitOrSend(op, opts, cl=cl)
392 # Keep track of submitted jobs
393 jex = JobExecutor(cl=cl, opts=opts)
395 for (status, job_id) in result[constants.JOB_IDS_KEY]:
396 jex.AddJobId(None, status, job_id)
398 results = jex.GetResults()
399 bad_cnt = len([row for row in results if not row[0]])
401 ToStdout("All instances evacuated successfully.")
402 rcode = constants.EXIT_SUCCESS
404 ToStdout("There were %s errors during the evacuation.", bad_cnt)
405 rcode = constants.EXIT_FAILURE
410 def FailoverNode(opts, args):
411 """Failover all primary instance on a node.
413 @param opts: the command line options selected by the user
415 @param args: should be an empty list
417 @return: the desired exit code
422 selected_fields = ["name", "pinst_list"]
424 # these fields are static data anyway, so it doesn't matter, but
425 # locking=True should be safer
426 qcl = GetClient(query=True)
427 result = cl.QueryNodes(names=args, fields=selected_fields,
430 node, pinst = result[0]
433 ToStderr("No primary instances on node %s, exiting.", node)
436 pinst = utils.NiceSort(pinst)
440 if not force and not AskUser("Fail over instance(s) %s?" %
441 (",".join("'%s'" % name for name in pinst))):
444 jex = JobExecutor(cl=cl, opts=opts)
446 op = opcodes.OpInstanceFailover(instance_name=iname,
447 ignore_consistency=opts.ignore_consistency,
448 iallocator=opts.iallocator)
449 jex.QueueJob(iname, op)
450 results = jex.GetResults()
451 bad_cnt = len([row for row in results if not row[0]])
453 ToStdout("All %d instance(s) failed over successfully.", len(results))
455 ToStdout("There were errors during the failover:\n"
456 "%d error(s) out of %d instance(s).", bad_cnt, len(results))
460 def MigrateNode(opts, args):
461 """Migrate all primary instance on a node.
466 selected_fields = ["name", "pinst_list"]
468 qcl = GetClient(query=True)
469 result = cl.QueryNodes(names=args, fields=selected_fields, use_locking=False)
471 ((node, pinst), ) = result
474 ToStdout("No primary instances on node %s, exiting." % node)
477 pinst = utils.NiceSort(pinst)
480 AskUser("Migrate instance(s) %s?" %
481 utils.CommaJoin(utils.NiceSort(pinst)))):
482 return constants.EXIT_CONFIRMATION
484 # this should be removed once --non-live is deprecated
485 if not opts.live and opts.migration_mode is not None:
486 raise errors.OpPrereqError("Only one of the --non-live and "
487 "--migration-mode options can be passed",
489 if not opts.live: # --non-live passed
490 mode = constants.HT_MIGRATION_NONLIVE
492 mode = opts.migration_mode
494 op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
495 iallocator=opts.iallocator,
496 target_node=opts.dst_node,
497 allow_runtime_changes=opts.allow_runtime_chgs,
498 ignore_ipolicy=opts.ignore_ipolicy)
500 result = SubmitOrSend(op, opts, cl=cl)
502 # Keep track of submitted jobs
503 jex = JobExecutor(cl=cl, opts=opts)
505 for (status, job_id) in result[constants.JOB_IDS_KEY]:
506 jex.AddJobId(None, status, job_id)
508 results = jex.GetResults()
509 bad_cnt = len([row for row in results if not row[0]])
511 ToStdout("All instances migrated successfully.")
512 rcode = constants.EXIT_SUCCESS
514 ToStdout("There were %s errors during the node migration.", bad_cnt)
515 rcode = constants.EXIT_FAILURE
520 def ShowNodeConfig(opts, args):
521 """Show node information.
523 @param opts: the command line options selected by the user
525 @param args: should either be an empty list, in which case
526 we show information about all nodes, or should contain
527 a list of nodes to be queried for information
529 @return: the desired exit code
532 cl = GetClient(query=True)
533 result = cl.QueryNodes(fields=["name", "pip", "sip",
534 "pinst_list", "sinst_list",
535 "master_candidate", "drained", "offline",
536 "master_capable", "vm_capable", "powered",
537 "ndparams", "custom_ndparams"],
538 names=args, use_locking=False)
540 for (name, primary_ip, secondary_ip, pinst, sinst, is_mc, drained, offline,
541 master_capable, vm_capable, powered, ndparams,
542 ndparams_custom) in result:
543 ToStdout("Node name: %s", name)
544 ToStdout(" primary ip: %s", primary_ip)
545 ToStdout(" secondary ip: %s", secondary_ip)
546 ToStdout(" master candidate: %s", is_mc)
547 ToStdout(" drained: %s", drained)
548 ToStdout(" offline: %s", offline)
549 if powered is not None:
550 ToStdout(" powered: %s", powered)
551 ToStdout(" master_capable: %s", master_capable)
552 ToStdout(" vm_capable: %s", vm_capable)
555 ToStdout(" primary for instances:")
556 for iname in utils.NiceSort(pinst):
557 ToStdout(" - %s", iname)
559 ToStdout(" primary for no instances")
561 ToStdout(" secondary for instances:")
562 for iname in utils.NiceSort(sinst):
563 ToStdout(" - %s", iname)
565 ToStdout(" secondary for no instances")
566 ToStdout(" node parameters:")
568 FormatParameterDict(buf, ndparams_custom, ndparams, level=2)
569 ToStdout(buf.getvalue().rstrip("\n"))
574 def RemoveNode(opts, args):
575 """Remove a node from the cluster.
577 @param opts: the command line options selected by the user
579 @param args: should contain only one element, the name of
580 the node to be removed
582 @return: the desired exit code
585 op = opcodes.OpNodeRemove(node_name=args[0])
586 SubmitOpCode(op, opts=opts)
590 def PowercycleNode(opts, args):
591 """Remove a node from the cluster.
593 @param opts: the command line options selected by the user
595 @param args: should contain only one element, the name of
596 the node to be removed
598 @return: the desired exit code
602 if (not opts.confirm and
603 not AskUser("Are you sure you want to hard powercycle node %s?" % node)):
606 op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
607 result = SubmitOrSend(op, opts)
613 def PowerNode(opts, args):
614 """Change/ask power state of a node.
616 @param opts: the command line options selected by the user
618 @param args: should contain only one element, the name of
619 the node to be removed
621 @return: the desired exit code
624 command = args.pop(0)
629 headers = {"node": "Node", "status": "Status"}
631 if command not in _LIST_POWER_COMMANDS:
632 ToStderr("power subcommand %s not supported." % command)
633 return constants.EXIT_FAILURE
635 oob_command = "power-%s" % command
637 if oob_command in _OOB_COMMAND_ASK:
639 ToStderr("Please provide at least one node for this command")
640 return constants.EXIT_FAILURE
641 elif not opts.force and not ConfirmOperation(args, "nodes",
642 "power %s" % command):
643 return constants.EXIT_FAILURE
647 if not opts.ignore_status and oob_command == constants.OOB_POWER_OFF:
648 # TODO: This is a little ugly as we can't catch and revert
650 opcodelist.append(opcodes.OpNodeSetParams(node_name=node, offline=True,
651 auto_promote=opts.auto_promote))
653 opcodelist.append(opcodes.OpOobCommand(node_names=args,
655 ignore_status=opts.ignore_status,
656 timeout=opts.oob_timeout,
657 power_delay=opts.power_delay))
659 cli.SetGenericOpcodeOpts(opcodelist, opts)
661 job_id = cli.SendJob(opcodelist)
663 # We just want the OOB Opcode status
664 # If it fails PollJob gives us the error message in it
665 result = cli.PollJob(job_id)[-1]
669 for node_result in result:
670 (node_tuple, data_tuple) = node_result
671 (_, node_name) = node_tuple
672 (data_status, data_node) = data_tuple
673 if data_status == constants.RS_NORMAL:
674 if oob_command == constants.OOB_POWER_STATUS:
675 if data_node[constants.OOB_POWER_STATUS_POWERED]:
679 data.append([node_name, text])
681 # We don't expect data here, so we just say, it was successfully invoked
682 data.append([node_name, "invoked"])
685 data.append([node_name, cli.FormatResultError(data_status, True)])
687 data = GenerateTable(separator=opts.separator, headers=headers,
688 fields=["node", "status"], data=data)
694 return constants.EXIT_FAILURE
696 return constants.EXIT_SUCCESS
699 def Health(opts, args):
700 """Show health of a node using OOB.
702 @param opts: the command line options selected by the user
704 @param args: should contain only one element, the name of
705 the node to be removed
707 @return: the desired exit code
710 op = opcodes.OpOobCommand(node_names=args, command=constants.OOB_HEALTH,
711 timeout=opts.oob_timeout)
712 result = SubmitOpCode(op, opts=opts)
717 headers = {"node": "Node", "status": "Status"}
721 for node_result in result:
722 (node_tuple, data_tuple) = node_result
723 (_, node_name) = node_tuple
724 (data_status, data_node) = data_tuple
725 if data_status == constants.RS_NORMAL:
726 data.append([node_name, "%s=%s" % tuple(data_node[0])])
727 for item, status in data_node[1:]:
728 data.append(["", "%s=%s" % (item, status)])
731 data.append([node_name, cli.FormatResultError(data_status, True)])
733 data = GenerateTable(separator=opts.separator, headers=headers,
734 fields=["node", "status"], data=data)
740 return constants.EXIT_FAILURE
742 return constants.EXIT_SUCCESS
745 def ListVolumes(opts, args):
746 """List logical volumes on node(s).
748 @param opts: the command line options selected by the user
750 @param args: should either be an empty list, in which case
751 we list data for all nodes, or contain a list of nodes
752 to display data only for those
754 @return: the desired exit code
757 selected_fields = ParseFields(opts.output, _LIST_VOL_DEF_FIELDS)
759 op = opcodes.OpNodeQueryvols(nodes=args, output_fields=selected_fields)
760 output = SubmitOpCode(op, opts=opts)
762 if not opts.no_headers:
763 headers = {"node": "Node", "phys": "PhysDev",
764 "vg": "VG", "name": "Name",
765 "size": "Size", "instance": "Instance"}
769 unitfields = ["size"]
773 data = GenerateTable(separator=opts.separator, headers=headers,
774 fields=selected_fields, unitfields=unitfields,
775 numfields=numfields, data=output, units=opts.units)
783 def ListStorage(opts, args):
784 """List physical volumes on node(s).
786 @param opts: the command line options selected by the user
788 @param args: should either be an empty list, in which case
789 we list data for all nodes, or contain a list of nodes
790 to display data only for those
792 @return: the desired exit code
795 # TODO: Default to ST_FILE if LVM is disabled on the cluster
796 if opts.user_storage_type is None:
797 opts.user_storage_type = constants.ST_LVM_PV
799 storage_type = ConvertStorageType(opts.user_storage_type)
801 selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
803 op = opcodes.OpNodeQueryStorage(nodes=args,
804 storage_type=storage_type,
805 output_fields=selected_fields)
806 output = SubmitOpCode(op, opts=opts)
808 if not opts.no_headers:
810 constants.SF_NODE: "Node",
811 constants.SF_TYPE: "Type",
812 constants.SF_NAME: "Name",
813 constants.SF_SIZE: "Size",
814 constants.SF_USED: "Used",
815 constants.SF_FREE: "Free",
816 constants.SF_ALLOCATABLE: "Allocatable",
821 unitfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
822 numfields = [constants.SF_SIZE, constants.SF_USED, constants.SF_FREE]
824 # change raw values to nicer strings
826 for idx, field in enumerate(selected_fields):
828 if field == constants.SF_ALLOCATABLE:
835 data = GenerateTable(separator=opts.separator, headers=headers,
836 fields=selected_fields, unitfields=unitfields,
837 numfields=numfields, data=output, units=opts.units)
845 def ModifyStorage(opts, args):
846 """Modify storage volume on a node.
848 @param opts: the command line options selected by the user
850 @param args: should contain 3 items: node name, storage type and volume name
852 @return: the desired exit code
855 (node_name, user_storage_type, volume_name) = args
857 storage_type = ConvertStorageType(user_storage_type)
861 if opts.allocatable is not None:
862 changes[constants.SF_ALLOCATABLE] = opts.allocatable
865 op = opcodes.OpNodeModifyStorage(node_name=node_name,
866 storage_type=storage_type,
869 SubmitOrSend(op, opts)
871 ToStderr("No changes to perform, exiting.")
874 def RepairStorage(opts, args):
875 """Repairs a storage volume on a node.
877 @param opts: the command line options selected by the user
879 @param args: should contain 3 items: node name, storage type and volume name
881 @return: the desired exit code
884 (node_name, user_storage_type, volume_name) = args
886 storage_type = ConvertStorageType(user_storage_type)
888 op = opcodes.OpRepairNodeStorage(node_name=node_name,
889 storage_type=storage_type,
891 ignore_consistency=opts.ignore_consistency)
892 SubmitOrSend(op, opts)
895 def SetNodeParams(opts, args):
898 @param opts: the command line options selected by the user
900 @param args: should contain only one element, the node name
902 @return: the desired exit code
905 all_changes = [opts.master_candidate, opts.drained, opts.offline,
906 opts.master_capable, opts.vm_capable, opts.secondary_ip,
908 if (all_changes.count(None) == len(all_changes) and
909 not (opts.hv_state or opts.disk_state)):
910 ToStderr("Please give at least one of the parameters.")
914 disk_state = utils.FlatToDict(opts.disk_state)
918 hv_state = dict(opts.hv_state)
920 op = opcodes.OpNodeSetParams(node_name=args[0],
921 master_candidate=opts.master_candidate,
922 offline=opts.offline,
923 drained=opts.drained,
924 master_capable=opts.master_capable,
925 vm_capable=opts.vm_capable,
926 secondary_ip=opts.secondary_ip,
928 ndparams=opts.ndparams,
929 auto_promote=opts.auto_promote,
930 powered=opts.node_powered,
932 disk_state=disk_state)
934 # even if here we process the result, we allow submit only
935 result = SubmitOrSend(op, opts)
938 ToStdout("Modified node %s", args[0])
939 for param, data in result:
940 ToStdout(" - %-5s -> %s", param, data)
944 def RestrictedCommand(opts, args):
945 """Runs a remote command on node(s).
947 @param opts: Command line options selected by user
949 @param args: Command line arguments
956 if len(args) > 1 or opts.nodegroup:
958 nodes = GetOnlineNodes(nodes=args[1:], cl=cl, nodegroup=opts.nodegroup)
960 raise errors.OpPrereqError("Node group or node names must be given",
963 op = opcodes.OpRestrictedCommand(command=args[0], nodes=nodes,
964 use_locking=opts.do_locking)
965 result = SubmitOrSend(op, opts, cl=cl)
967 exit_code = constants.EXIT_SUCCESS
969 for (node, (status, text)) in zip(nodes, result):
970 ToStdout("------------------------------------------------")
972 if opts.show_machine_names:
973 for line in text.splitlines():
974 ToStdout("%s: %s", node, line)
976 ToStdout("Node: %s", node)
979 exit_code = constants.EXIT_FAILURE
985 class ReplyStatus(object):
986 """Class holding a reply status for synchronous confd clients.
994 def ListDrbd(opts, args):
997 @param opts: the command line options selected by the user
999 @param args: should contain only one element, the node name
1001 @return: the desired exit code
1005 ToStderr("Please give one (and only one) node.")
1006 return constants.EXIT_FAILURE
1008 if not constants.ENABLE_CONFD:
1009 ToStderr("Error: this command requires confd support, but it has not"
1010 " been enabled at build time.")
1011 return constants.EXIT_FAILURE
1013 status = ReplyStatus()
1015 def ListDrbdConfdCallback(reply):
1016 """Callback for confd queries"""
1017 if reply.type == confd_client.UPCALL_REPLY:
1018 answer = reply.server_reply.answer
1019 reqtype = reply.orig_request.type
1020 if reqtype == constants.CONFD_REQ_NODE_DRBD:
1021 if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
1022 ToStderr("Query gave non-ok status '%s': %s" %
1023 (reply.server_reply.status,
1024 reply.server_reply.answer))
1025 status.failure = True
1027 if not confd.HTNodeDrbd(answer):
1028 ToStderr("Invalid response from server: expected %s, got %s",
1029 confd.HTNodeDrbd, answer)
1030 status.failure = True
1032 status.failure = False
1033 status.answer = answer
1035 ToStderr("Unexpected reply %s!?", reqtype)
1036 status.failure = True
1039 hmac = utils.ReadFile(pathutils.CONFD_HMAC_KEY)
1040 filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
1041 counting_callback = confd_client.ConfdCountingCallback(filter_callback)
1042 cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
1044 req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
1047 def DoConfdRequestReply(req):
1048 counting_callback.RegisterQuery(req.rsalt)
1049 cf_client.SendRequest(req, async=False)
1050 while not counting_callback.AllAnswered():
1051 if not cf_client.ReceiveReply():
1052 ToStderr("Did not receive all expected confd replies")
1055 DoConfdRequestReply(req)
1058 return constants.EXIT_FAILURE
1060 fields = ["node", "minor", "instance", "disk", "role", "peer"]
1064 headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
1065 "disk": "Disk", "role": "Role", "peer": "PeerNode"}
1067 data = GenerateTable(separator=opts.separator, headers=headers,
1068 fields=fields, data=sorted(status.answer),
1069 numfields=["minor"])
1073 return constants.EXIT_SUCCESS
1078 AddNode, [ArgHost(min=1, max=1)],
1079 [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
1080 NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
1081 CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
1083 "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
1084 " [--no-node-setup] [--verbose]"
1086 "Add a node to the cluster"),
1088 EvacuateNode, ARGS_ONE_NODE,
1089 [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
1090 PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT, SUBMIT_OPT],
1091 "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
1092 "Relocate the primary and/or secondary instances from a node"),
1094 FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
1095 IALLOCATOR_OPT, PRIORITY_OPT],
1097 "Stops the primary instances on a node and start them on their"
1098 " secondary node (only for instances with drbd disk template)"),
1100 MigrateNode, ARGS_ONE_NODE,
1101 [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
1102 IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
1103 NORUNTIME_CHGS_OPT, SUBMIT_OPT],
1105 "Migrate all the primary instance on a node away from it"
1106 " (only for instances of type drbd)"),
1108 ShowNodeConfig, ARGS_MANY_NODES, [],
1109 "[<node_name>...]", "Show information about the node(s)"),
1111 ListNodes, ARGS_MANY_NODES,
1112 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT,
1115 "Lists the nodes in the cluster. The available fields can be shown using"
1116 " the \"list-fields\" command (see the man page for details)."
1117 " The default field list is (in order): %s." %
1118 utils.CommaJoin(_LIST_DEF_FIELDS)),
1120 ListNodeFields, [ArgUnknown()],
1121 [NOHDR_OPT, SEP_OPT],
1123 "Lists all available fields for nodes"),
1125 SetNodeParams, ARGS_ONE_NODE,
1126 [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
1127 CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
1128 AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
1129 NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
1130 "<node_name>", "Alters the parameters of a node"),
1132 PowercycleNode, ARGS_ONE_NODE,
1133 [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1134 "<node_name>", "Tries to forcefully powercycle a node"),
1137 [ArgChoice(min=1, max=1, choices=_LIST_POWER_COMMANDS),
1139 [SUBMIT_OPT, AUTO_PROMOTE_OPT, PRIORITY_OPT, IGNORE_STATUS_OPT,
1140 FORCE_OPT, NOHDR_OPT, SEP_OPT, OOB_TIMEOUT_OPT, POWER_DELAY_OPT],
1141 "on|off|cycle|status [nodes...]",
1142 "Change power state of node by calling out-of-band helper."),
1144 RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
1145 "<node_name>", "Removes a node from the cluster"),
1147 ListVolumes, [ArgNode()],
1148 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
1149 "[<node_name>...]", "List logical volumes on node(s)"),
1151 ListStorage, ARGS_MANY_NODES,
1152 [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
1154 "[<node_name>...]", "List physical volumes on node(s). The available"
1155 " fields are (see the man page for details): %s." %
1156 (utils.CommaJoin(_LIST_STOR_HEADERS))),
1159 [ArgNode(min=1, max=1),
1160 ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
1161 ArgFile(min=1, max=1)],
1162 [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1163 "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
1166 [ArgNode(min=1, max=1),
1167 ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
1168 ArgFile(min=1, max=1)],
1169 [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
1170 "<node_name> <storage_type> <name>",
1171 "Repairs a storage volume on a node"),
1173 ListTags, ARGS_ONE_NODE, [],
1174 "<node_name>", "List the tags of the given node"),
1176 AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
1177 [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1178 "<node_name> tag...", "Add tags to the given node"),
1180 RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
1181 [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
1182 "<node_name> tag...", "Remove tags from the given node"),
1184 Health, ARGS_MANY_NODES,
1185 [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
1186 "[<node_name>...]", "List health of node(s) using out-of-band"),
1188 ListDrbd, ARGS_ONE_NODE,
1189 [NOHDR_OPT, SEP_OPT],
1190 "[<node_name>]", "Query the list of used DRBD minors on the given node"),
1191 "restricted-command": (
1192 RestrictedCommand, [ArgUnknown(min=1, max=1)] + ARGS_MANY_NODES,
1193 [SYNC_OPT, PRIORITY_OPT, SUBMIT_OPT, SHOW_MACHINE_OPT, NODEGROUP_OPT],
1194 "<command> <node_name> [<node_name>...]",
1195 "Executes a restricted command on node(s)"),
1198 #: dictionary with aliases for commands
1205 return GenericMain(commands, aliases=aliases,
1206 override={"tag_type": constants.TAG_NODE},
1207 env_override=_ENV_OVERRIDE)