4 # Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21 """Cluster 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-cluster
34 from ganeti.cli import *
35 from ganeti import opcodes
36 from ganeti import constants
37 from ganeti import errors
38 from ganeti import utils
39 from ganeti import bootstrap
40 from ganeti import ssh
41 from ganeti import objects
42 from ganeti import uidpool
43 from ganeti import compat
44 from ganeti import netutils
47 ON_OPT = cli_option("--on", default=False,
48 action="store_true", dest="on",
49 help="Recover from an EPO")
51 GROUPS_OPT = cli_option("--groups", default=False,
52 action="store_true", dest="groups",
53 help="Arguments are node groups instead of nodes")
55 _EPO_PING_INTERVAL = 30 # 30 seconds between pings
56 _EPO_PING_TIMEOUT = 1 # 1 second
57 _EPO_REACHABLE_TIMEOUT = 15 * 60 # 15 minutes
61 def InitCluster(opts, args):
62 """Initialize the cluster.
64 @param opts: the command line options selected by the user
66 @param args: should contain only one element, the desired
69 @return: the desired exit code
72 if not opts.lvm_storage and opts.vg_name:
73 ToStderr("Options --no-lvm-storage and --vg-name conflict.")
76 vg_name = opts.vg_name
77 if opts.lvm_storage and not opts.vg_name:
78 vg_name = constants.DEFAULT_VG
80 if not opts.drbd_storage and opts.drbd_helper:
81 ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
84 drbd_helper = opts.drbd_helper
85 if opts.drbd_storage and not opts.drbd_helper:
86 drbd_helper = constants.DEFAULT_DRBD_HELPER
88 master_netdev = opts.master_netdev
89 if master_netdev is None:
90 master_netdev = constants.DEFAULT_BRIDGE
92 hvlist = opts.enabled_hypervisors
94 hvlist = constants.DEFAULT_ENABLED_HYPERVISOR
95 hvlist = hvlist.split(",")
97 hvparams = dict(opts.hvparams)
98 beparams = opts.beparams
99 nicparams = opts.nicparams
101 diskparams = dict(opts.diskparams)
103 # check the disk template types here, as we cannot rely on the type check done
104 # by the opcode parameter types
105 diskparams_keys = set(diskparams.keys())
106 if not (diskparams_keys <= constants.DISK_TEMPLATES):
107 unknown = utils.NiceSort(diskparams_keys - constants.DISK_TEMPLATES)
108 ToStderr("Disk templates unknown: %s" % utils.CommaJoin(unknown))
111 # prepare beparams dict
112 beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
113 utils.ForceDictType(beparams, constants.BES_PARAMETER_COMPAT)
115 # prepare nicparams dict
116 nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
117 utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
119 # prepare ndparams dict
120 if opts.ndparams is None:
121 ndparams = dict(constants.NDC_DEFAULTS)
123 ndparams = objects.FillDict(constants.NDC_DEFAULTS, opts.ndparams)
124 utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
126 # prepare hvparams dict
127 for hv in constants.HYPER_TYPES:
128 if hv not in hvparams:
130 hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
131 utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
133 # prepare diskparams dict
134 for templ in constants.DISK_TEMPLATES:
135 if templ not in diskparams:
136 diskparams[templ] = {}
137 diskparams[templ] = objects.FillDict(constants.DISK_DT_DEFAULTS[templ],
139 utils.ForceDictType(diskparams[templ], constants.DISK_DT_TYPES)
141 # prepare ipolicy dict
143 objects.CreateIPolicyFromOpts(ispecs_mem_size=opts.ispecs_mem_size,
144 ispecs_cpu_count=opts.ispecs_cpu_count,
145 ispecs_disk_count=opts.ispecs_disk_count,
146 ispecs_disk_size=opts.ispecs_disk_size,
147 ispecs_nic_count=opts.ispecs_nic_count)
148 ipolicy = objects.FillDictOfDicts(constants.IPOLICY_DEFAULTS, ipolicy_raw)
149 for value in ipolicy.values():
150 utils.ForceDictType(value, constants.ISPECS_PARAMETER_TYPES)
152 if opts.candidate_pool_size is None:
153 opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
155 if opts.mac_prefix is None:
156 opts.mac_prefix = constants.DEFAULT_MAC_PREFIX
158 uid_pool = opts.uid_pool
159 if uid_pool is not None:
160 uid_pool = uidpool.ParseUidPool(uid_pool)
162 if opts.prealloc_wipe_disks is None:
163 opts.prealloc_wipe_disks = False
165 external_ip_setup_script = opts.use_external_mip_script
166 if external_ip_setup_script is None:
167 external_ip_setup_script = False
170 primary_ip_version = int(opts.primary_ip_version)
171 except (ValueError, TypeError), err:
172 ToStderr("Invalid primary ip version value: %s" % str(err))
175 master_netmask = opts.master_netmask
177 if master_netmask is not None:
178 master_netmask = int(master_netmask)
179 except (ValueError, TypeError), err:
180 ToStderr("Invalid master netmask value: %s" % str(err))
184 disk_state = utils.FlatToDict(opts.disk_state)
188 hv_state = dict(opts.hv_state)
190 bootstrap.InitCluster(cluster_name=args[0],
191 secondary_ip=opts.secondary_ip,
193 mac_prefix=opts.mac_prefix,
194 master_netmask=master_netmask,
195 master_netdev=master_netdev,
196 file_storage_dir=opts.file_storage_dir,
197 shared_file_storage_dir=opts.shared_file_storage_dir,
198 enabled_hypervisors=hvlist,
203 diskparams=diskparams,
205 candidate_pool_size=opts.candidate_pool_size,
206 modify_etc_hosts=opts.modify_etc_hosts,
207 modify_ssh_setup=opts.modify_ssh_setup,
208 maintain_node_health=opts.maintain_node_health,
209 drbd_helper=drbd_helper,
211 default_iallocator=opts.default_iallocator,
212 primary_ip_version=primary_ip_version,
213 prealloc_wipe_disks=opts.prealloc_wipe_disks,
214 use_external_mip_script=external_ip_setup_script,
216 disk_state=disk_state,
218 op = opcodes.OpClusterPostInit()
219 SubmitOpCode(op, opts=opts)
224 def DestroyCluster(opts, args):
225 """Destroy the cluster.
227 @param opts: the command line options selected by the user
229 @param args: should be an empty list
231 @return: the desired exit code
234 if not opts.yes_do_it:
235 ToStderr("Destroying a cluster is irreversible. If you really want"
236 " destroy this cluster, supply the --yes-do-it option.")
239 op = opcodes.OpClusterDestroy()
240 master = SubmitOpCode(op, opts=opts)
241 # if we reached this, the opcode didn't fail; we can proceed to
242 # shutdown all the daemons
243 bootstrap.FinalizeClusterDestroy(master)
247 def RenameCluster(opts, args):
248 """Rename the cluster.
250 @param opts: the command line options selected by the user
252 @param args: should contain only one element, the new cluster name
254 @return: the desired exit code
259 (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
263 usertext = ("This will rename the cluster from '%s' to '%s'. If you are"
264 " connected over the network to the cluster name, the"
265 " operation is very dangerous as the IP address will be"
266 " removed from the node and the change may not go through."
267 " Continue?") % (cluster_name, new_name)
268 if not AskUser(usertext):
271 op = opcodes.OpClusterRename(name=new_name)
272 result = SubmitOpCode(op, opts=opts, cl=cl)
275 ToStdout("Cluster renamed from '%s' to '%s'", cluster_name, result)
280 def ActivateMasterIp(opts, args):
281 """Activates the master IP.
284 op = opcodes.OpClusterActivateMasterIp()
289 def DeactivateMasterIp(opts, args):
290 """Deactivates the master IP.
294 usertext = ("This will disable the master IP. All the open connections to"
295 " the master IP will be closed. To reach the master you will"
296 " need to use its node IP."
298 if not AskUser(usertext):
301 op = opcodes.OpClusterDeactivateMasterIp()
306 def RedistributeConfig(opts, args):
307 """Forces push of the cluster configuration.
309 @param opts: the command line options selected by the user
311 @param args: empty list
313 @return: the desired exit code
316 op = opcodes.OpClusterRedistConf()
317 SubmitOrSend(op, opts)
321 def ShowClusterVersion(opts, args):
322 """Write version of ganeti software to the standard output.
324 @param opts: the command line options selected by the user
326 @param args: should be an empty list
328 @return: the desired exit code
332 result = cl.QueryClusterInfo()
333 ToStdout("Software version: %s", result["software_version"])
334 ToStdout("Internode protocol: %s", result["protocol_version"])
335 ToStdout("Configuration format: %s", result["config_version"])
336 ToStdout("OS api version: %s", result["os_api_version"])
337 ToStdout("Export interface: %s", result["export_version"])
341 def ShowClusterMaster(opts, args):
342 """Write name of master node to the standard output.
344 @param opts: the command line options selected by the user
346 @param args: should be an empty list
348 @return: the desired exit code
351 master = bootstrap.GetMaster()
356 def _PrintGroupedParams(paramsdict, level=1, roman=False):
357 """Print Grouped parameters (be, nic, disk) by group.
359 @type paramsdict: dict of dicts
360 @param paramsdict: {group: {param: value, ...}, ...}
362 @param level: Level of indention
366 for item, val in sorted(paramsdict.items()):
367 if isinstance(val, dict):
368 ToStdout("%s- %s:", indent, item)
369 _PrintGroupedParams(val, level=level + 1, roman=roman)
370 elif roman and isinstance(val, int):
371 ToStdout("%s %s: %s", indent, item, compat.TryToRoman(val))
373 ToStdout("%s %s: %s", indent, item, val)
376 def ShowClusterConfig(opts, args):
377 """Shows cluster information.
379 @param opts: the command line options selected by the user
381 @param args: should be an empty list
383 @return: the desired exit code
387 result = cl.QueryClusterInfo()
389 ToStdout("Cluster name: %s", result["name"])
390 ToStdout("Cluster UUID: %s", result["uuid"])
392 ToStdout("Creation time: %s", utils.FormatTime(result["ctime"]))
393 ToStdout("Modification time: %s", utils.FormatTime(result["mtime"]))
395 ToStdout("Master node: %s", result["master"])
397 ToStdout("Architecture (this node): %s (%s)",
398 result["architecture"][0], result["architecture"][1])
401 tags = utils.CommaJoin(utils.NiceSort(result["tags"]))
405 ToStdout("Tags: %s", tags)
407 ToStdout("Default hypervisor: %s", result["default_hypervisor"])
408 ToStdout("Enabled hypervisors: %s",
409 utils.CommaJoin(result["enabled_hypervisors"]))
411 ToStdout("Hypervisor parameters:")
412 _PrintGroupedParams(result["hvparams"])
414 ToStdout("OS-specific hypervisor parameters:")
415 _PrintGroupedParams(result["os_hvp"])
417 ToStdout("OS parameters:")
418 _PrintGroupedParams(result["osparams"])
420 ToStdout("Hidden OSes: %s", utils.CommaJoin(result["hidden_os"]))
421 ToStdout("Blacklisted OSes: %s", utils.CommaJoin(result["blacklisted_os"]))
423 ToStdout("Cluster parameters:")
424 ToStdout(" - candidate pool size: %s",
425 compat.TryToRoman(result["candidate_pool_size"],
426 convert=opts.roman_integers))
427 ToStdout(" - master netdev: %s", result["master_netdev"])
428 ToStdout(" - master netmask: %s", result["master_netmask"])
429 ToStdout(" - use external master IP address setup script: %s",
430 result["use_external_mip_script"])
431 ToStdout(" - lvm volume group: %s", result["volume_group_name"])
432 if result["reserved_lvs"]:
433 reserved_lvs = utils.CommaJoin(result["reserved_lvs"])
435 reserved_lvs = "(none)"
436 ToStdout(" - lvm reserved volumes: %s", reserved_lvs)
437 ToStdout(" - drbd usermode helper: %s", result["drbd_usermode_helper"])
438 ToStdout(" - file storage path: %s", result["file_storage_dir"])
439 ToStdout(" - shared file storage path: %s",
440 result["shared_file_storage_dir"])
441 ToStdout(" - maintenance of node health: %s",
442 result["maintain_node_health"])
443 ToStdout(" - uid pool: %s",
444 uidpool.FormatUidPool(result["uid_pool"],
445 roman=opts.roman_integers))
446 ToStdout(" - default instance allocator: %s", result["default_iallocator"])
447 ToStdout(" - primary ip version: %d", result["primary_ip_version"])
448 ToStdout(" - preallocation wipe disks: %s", result["prealloc_wipe_disks"])
449 ToStdout(" - OS search path: %s", utils.CommaJoin(constants.OS_SEARCH_PATH))
451 ToStdout("Default node parameters:")
452 _PrintGroupedParams(result["ndparams"], roman=opts.roman_integers)
454 ToStdout("Default instance parameters:")
455 _PrintGroupedParams(result["beparams"], roman=opts.roman_integers)
457 ToStdout("Default nic parameters:")
458 _PrintGroupedParams(result["nicparams"], roman=opts.roman_integers)
460 ToStdout("Instance policy - limits for instances:")
461 for key in constants.IPOLICY_PARAMETERS:
462 ToStdout(" - %s", key)
463 _PrintGroupedParams(result["ipolicy"][key], roman=opts.roman_integers)
468 def ClusterCopyFile(opts, args):
469 """Copy a file from master to some nodes.
471 @param opts: the command line options selected by the user
473 @param args: should contain only one element, the path of
474 the file to be copied
476 @return: the desired exit code
480 if not os.path.exists(filename):
481 raise errors.OpPrereqError("No such filename '%s'" % filename,
486 cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
488 results = GetOnlineNodes(nodes=opts.nodes, cl=cl, filter_master=True,
489 secondary_ips=opts.use_replication_network,
490 nodegroup=opts.nodegroup)
492 srun = ssh.SshRunner(cluster_name=cluster_name)
494 if not srun.CopyFileToNode(node, filename):
495 ToStderr("Copy of file %s to node %s failed", filename, node)
500 def RunClusterCommand(opts, args):
501 """Run a command on some nodes.
503 @param opts: the command line options selected by the user
505 @param args: should contain the command to be run and its arguments
507 @return: the desired exit code
512 command = " ".join(args)
514 nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl, nodegroup=opts.nodegroup)
516 cluster_name, master_node = cl.QueryConfigValues(["cluster_name",
519 srun = ssh.SshRunner(cluster_name=cluster_name)
521 # Make sure master node is at list end
522 if master_node in nodes:
523 nodes.remove(master_node)
524 nodes.append(master_node)
527 result = srun.Run(name, "root", command)
528 ToStdout("------------------------------------------------")
529 ToStdout("node: %s", name)
530 ToStdout("%s", result.output)
531 ToStdout("return code = %s", result.exit_code)
536 def VerifyCluster(opts, args):
537 """Verify integrity of cluster, performing various test on nodes.
539 @param opts: the command line options selected by the user
541 @param args: should be an empty list
543 @return: the desired exit code
548 if opts.skip_nplusone_mem:
549 skip_checks.append(constants.VERIFY_NPLUSONE_MEM)
553 op = opcodes.OpClusterVerify(verbose=opts.verbose,
554 error_codes=opts.error_codes,
555 debug_simulate_errors=opts.simulate_errors,
556 skip_checks=skip_checks,
557 ignore_errors=opts.ignore_errors,
558 group_name=opts.nodegroup)
559 result = SubmitOpCode(op, cl=cl, opts=opts)
561 # Keep track of submitted jobs
562 jex = JobExecutor(cl=cl, opts=opts)
564 for (status, job_id) in result[constants.JOB_IDS_KEY]:
565 jex.AddJobId(None, status, job_id)
567 results = jex.GetResults()
569 (bad_jobs, bad_results) = \
571 # Convert iterators to lists
574 map(compat.partial(itertools.ifilterfalse, bool),
575 # Convert result to booleans in a tuple
576 zip(*((job_success, len(op_results) == 1 and op_results[0])
577 for (job_success, op_results) in results)))))
579 if bad_jobs == 0 and bad_results == 0:
580 rcode = constants.EXIT_SUCCESS
582 rcode = constants.EXIT_FAILURE
584 ToStdout("%s job(s) failed while verifying the cluster.", bad_jobs)
589 def VerifyDisks(opts, args):
590 """Verify integrity of cluster disks.
592 @param opts: the command line options selected by the user
594 @param args: should be an empty list
596 @return: the desired exit code
601 op = opcodes.OpClusterVerifyDisks()
603 result = SubmitOpCode(op, cl=cl, opts=opts)
605 # Keep track of submitted jobs
606 jex = JobExecutor(cl=cl, opts=opts)
608 for (status, job_id) in result[constants.JOB_IDS_KEY]:
609 jex.AddJobId(None, status, job_id)
611 retcode = constants.EXIT_SUCCESS
613 for (status, result) in jex.GetResults():
615 ToStdout("Job failed: %s", result)
618 ((bad_nodes, instances, missing), ) = result
620 for node, text in bad_nodes.items():
621 ToStdout("Error gathering data on node %s: %s",
622 node, utils.SafeEncode(text[-400:]))
623 retcode = constants.EXIT_FAILURE
624 ToStdout("You need to fix these nodes first before fixing instances")
626 for iname in instances:
629 op = opcodes.OpInstanceActivateDisks(instance_name=iname)
631 ToStdout("Activating disks for instance '%s'", iname)
632 SubmitOpCode(op, opts=opts, cl=cl)
633 except errors.GenericError, err:
634 nret, msg = FormatError(err)
636 ToStderr("Error activating disks for instance %s: %s", iname, msg)
639 for iname, ival in missing.iteritems():
640 all_missing = compat.all(x[0] in bad_nodes for x in ival)
642 ToStdout("Instance %s cannot be verified as it lives on"
643 " broken nodes", iname)
645 ToStdout("Instance %s has missing logical volumes:", iname)
647 for node, vol in ival:
648 if node in bad_nodes:
649 ToStdout("\tbroken node %s /dev/%s", node, vol)
651 ToStdout("\t%s /dev/%s", node, vol)
653 ToStdout("You need to replace or recreate disks for all the above"
654 " instances if this message persists after fixing broken nodes.")
655 retcode = constants.EXIT_FAILURE
660 def RepairDiskSizes(opts, args):
661 """Verify sizes of cluster disks.
663 @param opts: the command line options selected by the user
665 @param args: optional list of instances to restrict check to
667 @return: the desired exit code
670 op = opcodes.OpClusterRepairDiskSizes(instances=args)
671 SubmitOpCode(op, opts=opts)
675 def MasterFailover(opts, args):
676 """Failover the master node.
678 This command, when run on a non-master node, will cause the current
679 master to cease being master, and the non-master to become new
682 @param opts: the command line options selected by the user
684 @param args: should be an empty list
686 @return: the desired exit code
690 usertext = ("This will perform the failover even if most other nodes"
691 " are down, or if this node is outdated. This is dangerous"
692 " as it can lead to a non-consistent cluster. Check the"
693 " gnt-cluster(8) man page before proceeding. Continue?")
694 if not AskUser(usertext):
697 return bootstrap.MasterFailover(no_voting=opts.no_voting)
700 def MasterPing(opts, args):
701 """Checks if the master is alive.
703 @param opts: the command line options selected by the user
705 @param args: should be an empty list
707 @return: the desired exit code
712 cl.QueryClusterInfo()
714 except Exception: # pylint: disable=W0703
718 def SearchTags(opts, args):
719 """Searches the tags on all the cluster.
721 @param opts: the command line options selected by the user
723 @param args: should contain only one element, the tag pattern
725 @return: the desired exit code
728 op = opcodes.OpTagsSearch(pattern=args[0])
729 result = SubmitOpCode(op, opts=opts)
732 result = list(result)
734 for path, tag in result:
735 ToStdout("%s %s", path, tag)
738 def _ReadAndVerifyCert(cert_filename, verify_private_key=False):
739 """Reads and verifies an X509 certificate.
741 @type cert_filename: string
742 @param cert_filename: the path of the file containing the certificate to
743 verify encoded in PEM format
744 @type verify_private_key: bool
745 @param verify_private_key: whether to verify the private key in addition to
746 the public certificate
748 @return: a string containing the PEM-encoded certificate.
752 pem = utils.ReadFile(cert_filename)
754 raise errors.X509CertError(cert_filename,
755 "Unable to read certificate: %s" % str(err))
758 OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem)
759 except Exception, err:
760 raise errors.X509CertError(cert_filename,
761 "Unable to load certificate: %s" % str(err))
763 if verify_private_key:
765 OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, pem)
766 except Exception, err:
767 raise errors.X509CertError(cert_filename,
768 "Unable to load private key: %s" % str(err))
773 def _RenewCrypto(new_cluster_cert, new_rapi_cert, #pylint: disable=R0911
774 rapi_cert_filename, new_spice_cert, spice_cert_filename,
775 spice_cacert_filename, new_confd_hmac_key, new_cds,
776 cds_filename, force):
777 """Renews cluster certificates, keys and secrets.
779 @type new_cluster_cert: bool
780 @param new_cluster_cert: Whether to generate a new cluster certificate
781 @type new_rapi_cert: bool
782 @param new_rapi_cert: Whether to generate a new RAPI certificate
783 @type rapi_cert_filename: string
784 @param rapi_cert_filename: Path to file containing new RAPI certificate
785 @type new_spice_cert: bool
786 @param new_spice_cert: Whether to generate a new SPICE certificate
787 @type spice_cert_filename: string
788 @param spice_cert_filename: Path to file containing new SPICE certificate
789 @type spice_cacert_filename: string
790 @param spice_cacert_filename: Path to file containing the certificate of the
791 CA that signed the SPICE certificate
792 @type new_confd_hmac_key: bool
793 @param new_confd_hmac_key: Whether to generate a new HMAC key
795 @param new_cds: Whether to generate a new cluster domain secret
796 @type cds_filename: string
797 @param cds_filename: Path to file containing new cluster domain secret
799 @param force: Whether to ask user for confirmation
802 if new_rapi_cert and rapi_cert_filename:
803 ToStderr("Only one of the --new-rapi-certificate and --rapi-certificate"
804 " options can be specified at the same time.")
807 if new_cds and cds_filename:
808 ToStderr("Only one of the --new-cluster-domain-secret and"
809 " --cluster-domain-secret options can be specified at"
813 if new_spice_cert and (spice_cert_filename or spice_cacert_filename):
814 ToStderr("When using --new-spice-certificate, the --spice-certificate"
815 " and --spice-ca-certificate must not be used.")
818 if bool(spice_cacert_filename) ^ bool(spice_cert_filename):
819 ToStderr("Both --spice-certificate and --spice-ca-certificate must be"
823 rapi_cert_pem, spice_cert_pem, spice_cacert_pem = (None, None, None)
825 if rapi_cert_filename:
826 rapi_cert_pem = _ReadAndVerifyCert(rapi_cert_filename, True)
827 if spice_cert_filename:
828 spice_cert_pem = _ReadAndVerifyCert(spice_cert_filename, True)
829 spice_cacert_pem = _ReadAndVerifyCert(spice_cacert_filename)
830 except errors.X509CertError, err:
831 ToStderr("Unable to load X509 certificate from %s: %s", err[0], err[1])
836 cds = utils.ReadFile(cds_filename)
837 except Exception, err: # pylint: disable=W0703
838 ToStderr("Can't load new cluster domain secret from %s: %s" %
839 (cds_filename, str(err)))
845 usertext = ("This requires all daemons on all nodes to be restarted and"
846 " may take some time. Continue?")
847 if not AskUser(usertext):
850 def _RenewCryptoInner(ctx):
851 ctx.feedback_fn("Updating certificates and keys")
852 bootstrap.GenerateClusterCrypto(new_cluster_cert,
857 rapi_cert_pem=rapi_cert_pem,
858 spice_cert_pem=spice_cert_pem,
859 spice_cacert_pem=spice_cacert_pem,
865 files_to_copy.append(constants.NODED_CERT_FILE)
867 if new_rapi_cert or rapi_cert_pem:
868 files_to_copy.append(constants.RAPI_CERT_FILE)
870 if new_spice_cert or spice_cert_pem:
871 files_to_copy.append(constants.SPICE_CERT_FILE)
872 files_to_copy.append(constants.SPICE_CACERT_FILE)
874 if new_confd_hmac_key:
875 files_to_copy.append(constants.CONFD_HMAC_KEY)
878 files_to_copy.append(constants.CLUSTER_DOMAIN_SECRET_FILE)
881 for node_name in ctx.nonmaster_nodes:
882 ctx.feedback_fn("Copying %s to %s" %
883 (", ".join(files_to_copy), node_name))
884 for file_name in files_to_copy:
885 ctx.ssh.CopyFileToNode(node_name, file_name)
887 RunWhileClusterStopped(ToStdout, _RenewCryptoInner)
889 ToStdout("All requested certificates and keys have been replaced."
890 " Running \"gnt-cluster verify\" now is recommended.")
895 def RenewCrypto(opts, args):
896 """Renews cluster certificates, keys and secrets.
899 return _RenewCrypto(opts.new_cluster_cert,
905 opts.new_confd_hmac_key,
906 opts.new_cluster_domain_secret,
907 opts.cluster_domain_secret,
911 def SetClusterParams(opts, args):
912 """Modify the cluster.
914 @param opts: the command line options selected by the user
916 @param args: should be an empty list
918 @return: the desired exit code
921 if not (not opts.lvm_storage or opts.vg_name or
922 not opts.drbd_storage or opts.drbd_helper or
923 opts.enabled_hypervisors or opts.hvparams or
924 opts.beparams or opts.nicparams or
925 opts.ndparams or opts.diskparams or
926 opts.candidate_pool_size is not None or
927 opts.uid_pool is not None or
928 opts.maintain_node_health is not None or
929 opts.add_uids is not None or
930 opts.remove_uids is not None or
931 opts.default_iallocator is not None or
932 opts.reserved_lvs is not None or
933 opts.master_netdev is not None or
934 opts.master_netmask is not None or
935 opts.use_external_mip_script is not None or
936 opts.prealloc_wipe_disks is not None or
939 opts.ispecs_mem_size is not None or
940 opts.ispecs_cpu_count is not None or
941 opts.ispecs_disk_count is not None or
942 opts.ispecs_disk_size is not None or
943 opts.ispecs_nic_count is not None):
944 ToStderr("Please give at least one of the parameters.")
947 vg_name = opts.vg_name
948 if not opts.lvm_storage and opts.vg_name:
949 ToStderr("Options --no-lvm-storage and --vg-name conflict.")
952 if not opts.lvm_storage:
955 drbd_helper = opts.drbd_helper
956 if not opts.drbd_storage and opts.drbd_helper:
957 ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
960 if not opts.drbd_storage:
963 hvlist = opts.enabled_hypervisors
964 if hvlist is not None:
965 hvlist = hvlist.split(",")
967 # a list of (name, dict) we can pass directly to dict() (or [])
968 hvparams = dict(opts.hvparams)
969 for hv_params in hvparams.values():
970 utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
972 diskparams = dict(opts.diskparams)
974 for dt_params in hvparams.values():
975 utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
977 beparams = opts.beparams
978 utils.ForceDictType(beparams, constants.BES_PARAMETER_COMPAT)
980 nicparams = opts.nicparams
981 utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
983 ndparams = opts.ndparams
984 if ndparams is not None:
985 utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
988 objects.CreateIPolicyFromOpts(ispecs_mem_size=opts.ispecs_mem_size,
989 ispecs_cpu_count=opts.ispecs_cpu_count,
990 ispecs_disk_count=opts.ispecs_disk_count,
991 ispecs_disk_size=opts.ispecs_disk_size,
992 ispecs_nic_count=opts.ispecs_nic_count)
993 for value in ipolicy.values():
994 utils.ForceDictType(value, constants.ISPECS_PARAMETER_TYPES)
996 mnh = opts.maintain_node_health
998 uid_pool = opts.uid_pool
999 if uid_pool is not None:
1000 uid_pool = uidpool.ParseUidPool(uid_pool)
1002 add_uids = opts.add_uids
1003 if add_uids is not None:
1004 add_uids = uidpool.ParseUidPool(add_uids)
1006 remove_uids = opts.remove_uids
1007 if remove_uids is not None:
1008 remove_uids = uidpool.ParseUidPool(remove_uids)
1010 if opts.reserved_lvs is not None:
1011 if opts.reserved_lvs == "":
1012 opts.reserved_lvs = []
1014 opts.reserved_lvs = utils.UnescapeAndSplit(opts.reserved_lvs, sep=",")
1016 if opts.master_netmask is not None:
1018 opts.master_netmask = int(opts.master_netmask)
1020 ToStderr("The --master-netmask option expects an int parameter.")
1023 ext_ip_script = opts.use_external_mip_script
1026 disk_state = utils.FlatToDict(opts.disk_state)
1030 hv_state = dict(opts.hv_state)
1032 op = opcodes.OpClusterSetParams(vg_name=vg_name,
1033 drbd_helper=drbd_helper,
1034 enabled_hypervisors=hvlist,
1038 nicparams=nicparams,
1040 diskparams=diskparams,
1042 candidate_pool_size=opts.candidate_pool_size,
1043 maintain_node_health=mnh,
1046 remove_uids=remove_uids,
1047 default_iallocator=opts.default_iallocator,
1048 prealloc_wipe_disks=opts.prealloc_wipe_disks,
1049 master_netdev=opts.master_netdev,
1050 master_netmask=opts.master_netmask,
1051 reserved_lvs=opts.reserved_lvs,
1052 use_external_mip_script=ext_ip_script,
1054 disk_state=disk_state,
1056 SubmitOpCode(op, opts=opts)
1060 def QueueOps(opts, args):
1061 """Queue operations.
1063 @param opts: the command line options selected by the user
1065 @param args: should contain only one element, the subcommand
1067 @return: the desired exit code
1071 client = GetClient()
1072 if command in ("drain", "undrain"):
1073 drain_flag = command == "drain"
1074 client.SetQueueDrainFlag(drain_flag)
1075 elif command == "info":
1076 result = client.QueryConfigValues(["drain_flag"])
1081 ToStdout("The drain flag is %s" % val)
1083 raise errors.OpPrereqError("Command '%s' is not valid." % command,
1089 def _ShowWatcherPause(until):
1090 if until is None or until < time.time():
1091 ToStdout("The watcher is not paused.")
1093 ToStdout("The watcher is paused until %s.", time.ctime(until))
1096 def WatcherOps(opts, args):
1097 """Watcher operations.
1099 @param opts: the command line options selected by the user
1101 @param args: should contain only one element, the subcommand
1103 @return: the desired exit code
1107 client = GetClient()
1109 if command == "continue":
1110 client.SetWatcherPause(None)
1111 ToStdout("The watcher is no longer paused.")
1113 elif command == "pause":
1115 raise errors.OpPrereqError("Missing pause duration", errors.ECODE_INVAL)
1117 result = client.SetWatcherPause(time.time() + ParseTimespec(args[1]))
1118 _ShowWatcherPause(result)
1120 elif command == "info":
1121 result = client.QueryConfigValues(["watcher_pause"])
1122 _ShowWatcherPause(result[0])
1125 raise errors.OpPrereqError("Command '%s' is not valid." % command,
1131 def _OobPower(opts, node_list, power):
1132 """Puts the node in the list to desired power state.
1134 @param opts: The command line options selected by the user
1135 @param node_list: The list of nodes to operate on
1136 @param power: True if they should be powered on, False otherwise
1137 @return: The success of the operation (none failed)
1141 command = constants.OOB_POWER_ON
1143 command = constants.OOB_POWER_OFF
1145 op = opcodes.OpOobCommand(node_names=node_list,
1148 timeout=opts.oob_timeout,
1149 power_delay=opts.power_delay)
1150 result = SubmitOpCode(op, opts=opts)
1152 for node_result in result:
1153 (node_tuple, data_tuple) = node_result
1154 (_, node_name) = node_tuple
1155 (data_status, _) = data_tuple
1156 if data_status != constants.RS_NORMAL:
1157 assert data_status != constants.RS_UNAVAIL
1159 ToStderr("There was a problem changing power for %s, please investigate",
1168 def _InstanceStart(opts, inst_list, start):
1169 """Puts the instances in the list to desired state.
1171 @param opts: The command line options selected by the user
1172 @param inst_list: The list of instances to operate on
1173 @param start: True if they should be started, False for shutdown
1174 @return: The success of the operation (none failed)
1178 opcls = opcodes.OpInstanceStartup
1179 text_submit, text_success, text_failed = ("startup", "started", "starting")
1181 opcls = compat.partial(opcodes.OpInstanceShutdown,
1182 timeout=opts.shutdown_timeout)
1183 text_submit, text_success, text_failed = ("shutdown", "stopped", "stopping")
1185 jex = JobExecutor(opts=opts)
1187 for inst in inst_list:
1188 ToStdout("Submit %s of instance %s", text_submit, inst)
1189 op = opcls(instance_name=inst)
1190 jex.QueueJob(inst, op)
1192 results = jex.GetResults()
1193 bad_cnt = len([1 for (success, _) in results if not success])
1196 ToStdout("All instances have been %s successfully", text_success)
1198 ToStderr("There were errors while %s instances:\n"
1199 "%d error(s) out of %d instance(s)", text_failed, bad_cnt,
1206 class _RunWhenNodesReachableHelper:
1207 """Helper class to make shared internal state sharing easier.
1209 @ivar success: Indicates if all action_cb calls were successful
1212 def __init__(self, node_list, action_cb, node2ip, port, feedback_fn,
1213 _ping_fn=netutils.TcpPing, _sleep_fn=time.sleep):
1216 @param node_list: The list of nodes to be reachable
1217 @param action_cb: Callback called when a new host is reachable
1219 @param node2ip: Node to ip mapping
1220 @param port: The port to use for the TCP ping
1221 @param feedback_fn: The function used for feedback
1222 @param _ping_fn: Function to check reachabilty (for unittest use only)
1223 @param _sleep_fn: Function to sleep (for unittest use only)
1226 self.down = set(node_list)
1228 self.node2ip = node2ip
1230 self.action_cb = action_cb
1232 self.feedback_fn = feedback_fn
1233 self._ping_fn = _ping_fn
1234 self._sleep_fn = _sleep_fn
1237 """When called we run action_cb.
1239 @raises utils.RetryAgain: When there are still down nodes
1242 if not self.action_cb(self.up):
1243 self.success = False
1246 raise utils.RetryAgain()
1250 def Wait(self, secs):
1251 """Checks if a host is up or waits remaining seconds.
1253 @param secs: The secs remaining
1257 for node in self.down:
1258 if self._ping_fn(self.node2ip[node], self.port, timeout=_EPO_PING_TIMEOUT,
1259 live_port_needed=True):
1260 self.feedback_fn("Node %s became available" % node)
1262 self.down -= self.up
1263 # If we have a node available there is the possibility to run the
1264 # action callback successfully, therefore we don't wait and return
1267 self._sleep_fn(max(0.0, start + secs - time.time()))
1270 def _RunWhenNodesReachable(node_list, action_cb, interval):
1271 """Run action_cb when nodes become reachable.
1273 @param node_list: The list of nodes to be reachable
1274 @param action_cb: Callback called when a new host is reachable
1275 @param interval: The earliest time to retry
1278 client = GetClient()
1279 cluster_info = client.QueryClusterInfo()
1280 if cluster_info["primary_ip_version"] == constants.IP4_VERSION:
1281 family = netutils.IPAddress.family
1283 family = netutils.IP6Address.family
1285 node2ip = dict((node, netutils.GetHostname(node, family=family).ip)
1286 for node in node_list)
1288 port = netutils.GetDaemonPort(constants.NODED)
1289 helper = _RunWhenNodesReachableHelper(node_list, action_cb, node2ip, port,
1293 return utils.Retry(helper, interval, _EPO_REACHABLE_TIMEOUT,
1294 wait_fn=helper.Wait)
1295 except utils.RetryTimeout:
1296 ToStderr("Time exceeded while waiting for nodes to become reachable"
1297 " again:\n - %s", " - ".join(helper.down))
1301 def _MaybeInstanceStartup(opts, inst_map, nodes_online,
1302 _instance_start_fn=_InstanceStart):
1303 """Start the instances conditional based on node_states.
1305 @param opts: The command line options selected by the user
1306 @param inst_map: A dict of inst -> nodes mapping
1307 @param nodes_online: A list of nodes online
1308 @param _instance_start_fn: Callback to start instances (unittest use only)
1309 @return: Success of the operation on all instances
1312 start_inst_list = []
1313 for (inst, nodes) in inst_map.items():
1314 if not (nodes - nodes_online):
1315 # All nodes the instance lives on are back online
1316 start_inst_list.append(inst)
1318 for inst in start_inst_list:
1322 return _instance_start_fn(opts, start_inst_list, True)
1327 def _EpoOn(opts, full_node_list, node_list, inst_map):
1328 """Does the actual power on.
1330 @param opts: The command line options selected by the user
1331 @param full_node_list: All nodes to operate on (includes nodes not supporting
1333 @param node_list: The list of nodes to operate on (all need to support OOB)
1334 @param inst_map: A dict of inst -> nodes mapping
1335 @return: The desired exit status
1338 if node_list and not _OobPower(opts, node_list, False):
1339 ToStderr("Not all nodes seem to get back up, investigate and start"
1340 " manually if needed")
1342 # Wait for the nodes to be back up
1343 action_cb = compat.partial(_MaybeInstanceStartup, opts, dict(inst_map))
1345 ToStdout("Waiting until all nodes are available again")
1346 if not _RunWhenNodesReachable(full_node_list, action_cb, _EPO_PING_INTERVAL):
1347 ToStderr("Please investigate and start stopped instances manually")
1348 return constants.EXIT_FAILURE
1350 return constants.EXIT_SUCCESS
1353 def _EpoOff(opts, node_list, inst_map):
1354 """Does the actual power off.
1356 @param opts: The command line options selected by the user
1357 @param node_list: The list of nodes to operate on (all need to support OOB)
1358 @param inst_map: A dict of inst -> nodes mapping
1359 @return: The desired exit status
1362 if not _InstanceStart(opts, inst_map.keys(), False):
1363 ToStderr("Please investigate and stop instances manually before continuing")
1364 return constants.EXIT_FAILURE
1367 return constants.EXIT_SUCCESS
1369 if _OobPower(opts, node_list, False):
1370 return constants.EXIT_SUCCESS
1372 return constants.EXIT_FAILURE
1375 def Epo(opts, args):
1378 @param opts: the command line options selected by the user
1380 @param args: should contain only one element, the subcommand
1382 @return: the desired exit code
1385 if opts.groups and opts.show_all:
1386 ToStderr("Only one of --groups or --all are allowed")
1387 return constants.EXIT_FAILURE
1388 elif args and opts.show_all:
1389 ToStderr("Arguments in combination with --all are not allowed")
1390 return constants.EXIT_FAILURE
1392 client = GetClient()
1395 node_query_list = itertools.chain(*client.QueryGroups(names=args,
1396 fields=["node_list"],
1399 node_query_list = args
1401 result = client.QueryNodes(names=node_query_list,
1402 fields=["name", "master", "pinst_list",
1403 "sinst_list", "powered", "offline"],
1407 for (idx, (node, master, pinsts, sinsts, powered,
1408 offline)) in enumerate(result):
1409 # Normalize the node_query_list as well
1410 if not opts.show_all:
1411 node_query_list[idx] = node
1413 for inst in (pinsts + sinsts):
1414 if inst in inst_map:
1416 inst_map[inst].add(node)
1418 inst_map[inst] = set()
1420 inst_map[inst] = set([node])
1422 if master and opts.on:
1423 # We ignore the master for turning on the machines, in fact we are
1424 # already operating on the master at this point :)
1426 elif master and not opts.show_all:
1427 ToStderr("%s is the master node, please do a master-failover to another"
1428 " node not affected by the EPO or use --all if you intend to"
1429 " shutdown the whole cluster", node)
1430 return constants.EXIT_FAILURE
1431 elif powered is None:
1432 ToStdout("Node %s does not support out-of-band handling, it can not be"
1433 " handled in a fully automated manner", node)
1434 elif powered == opts.on:
1435 ToStdout("Node %s is already in desired power state, skipping", node)
1436 elif not offline or (offline and powered):
1437 node_list.append(node)
1439 if not opts.force and not ConfirmOperation(node_query_list, "nodes", "epo"):
1440 return constants.EXIT_FAILURE
1443 return _EpoOn(opts, node_query_list, node_list, inst_map)
1445 return _EpoOff(opts, node_list, inst_map)
1447 INSTANCE_POLICY_OPTS = [
1448 SPECS_CPU_COUNT_OPT,
1449 SPECS_DISK_COUNT_OPT,
1450 SPECS_DISK_SIZE_OPT,
1452 SPECS_NIC_COUNT_OPT,
1457 InitCluster, [ArgHost(min=1, max=1)],
1458 [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
1459 HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, MASTER_NETMASK_OPT,
1460 NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT,
1461 NOMODIFY_SSH_SETUP_OPT, SECONDARY_IP_OPT, VG_NAME_OPT,
1462 MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, DRBD_HELPER_OPT, NODRBD_STORAGE_OPT,
1463 DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT,
1464 NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT, USE_EXTERNAL_MIP_SCRIPT,
1465 DISK_PARAMS_OPT, HV_STATE_OPT, DISK_STATE_OPT] + INSTANCE_POLICY_OPTS,
1466 "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
1468 DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
1469 "", "Destroy cluster"),
1471 RenameCluster, [ArgHost(min=1, max=1)],
1472 [FORCE_OPT, DRY_RUN_OPT],
1474 "Renames the cluster"),
1476 RedistributeConfig, ARGS_NONE, [SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
1477 "", "Forces a push of the configuration file and ssconf files"
1478 " to the nodes in the cluster"),
1480 VerifyCluster, ARGS_NONE,
1481 [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT,
1482 DRY_RUN_OPT, PRIORITY_OPT, NODEGROUP_OPT, IGNORE_ERRORS_OPT],
1483 "", "Does a check on the cluster configuration"),
1485 VerifyDisks, ARGS_NONE, [PRIORITY_OPT],
1486 "", "Does a check on the cluster disk status"),
1487 "repair-disk-sizes": (
1488 RepairDiskSizes, ARGS_MANY_INSTANCES, [DRY_RUN_OPT, PRIORITY_OPT],
1489 "[instance...]", "Updates mismatches in recorded disk sizes"),
1490 "master-failover": (
1491 MasterFailover, ARGS_NONE, [NOVOTING_OPT],
1492 "", "Makes the current node the master"),
1494 MasterPing, ARGS_NONE, [],
1495 "", "Checks if the master is alive"),
1497 ShowClusterVersion, ARGS_NONE, [],
1498 "", "Shows the cluster version"),
1500 ShowClusterMaster, ARGS_NONE, [],
1501 "", "Shows the cluster master"),
1503 ClusterCopyFile, [ArgFile(min=1, max=1)],
1504 [NODE_LIST_OPT, USE_REPL_NET_OPT, NODEGROUP_OPT],
1505 "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
1507 RunClusterCommand, [ArgCommand(min=1)],
1508 [NODE_LIST_OPT, NODEGROUP_OPT],
1509 "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
1511 ShowClusterConfig, ARGS_NONE, [ROMAN_OPT],
1512 "[--roman]", "Show cluster configuration"),
1514 ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
1516 AddTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
1517 "tag...", "Add tags to the cluster"),
1519 RemoveTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
1520 "tag...", "Remove tags from the cluster"),
1522 SearchTags, [ArgUnknown(min=1, max=1)], [PRIORITY_OPT], "",
1523 "Searches the tags on all objects on"
1524 " the cluster for a given pattern (regex)"),
1527 [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])],
1528 [], "drain|undrain|info", "Change queue properties"),
1531 [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]),
1532 ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])],
1534 "{pause <timespec>|continue|info}", "Change watcher properties"),
1536 SetClusterParams, ARGS_NONE,
1537 [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT, MASTER_NETDEV_OPT,
1538 MASTER_NETMASK_OPT, NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT,
1539 MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT,
1540 DRBD_HELPER_OPT, NODRBD_STORAGE_OPT, DEFAULT_IALLOCATOR_OPT,
1541 RESERVED_LVS_OPT, DRY_RUN_OPT, PRIORITY_OPT, PREALLOC_WIPE_DISKS_OPT,
1542 NODE_PARAMS_OPT, USE_EXTERNAL_MIP_SCRIPT, DISK_PARAMS_OPT, HV_STATE_OPT,
1544 INSTANCE_POLICY_OPTS,
1546 "Alters the parameters of the cluster"),
1548 RenewCrypto, ARGS_NONE,
1549 [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT,
1550 NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT,
1551 NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT,
1552 NEW_SPICE_CERT_OPT, SPICE_CERT_OPT, SPICE_CACERT_OPT],
1554 "Renews cluster certificates, keys and secrets"),
1556 Epo, [ArgUnknown()],
1557 [FORCE_OPT, ON_OPT, GROUPS_OPT, ALL_OPT, OOB_TIMEOUT_OPT,
1558 SHUTDOWN_TIMEOUT_OPT, POWER_DELAY_OPT],
1560 "Performs an emergency power-off on given args"),
1561 "activate-master-ip": (
1562 ActivateMasterIp, ARGS_NONE, [], "", "Activates the master IP"),
1563 "deactivate-master-ip": (
1564 DeactivateMasterIp, ARGS_NONE, [CONFIRM_OPT], "",
1565 "Deactivates the master IP"),
1569 #: dictionary with aliases for commands
1571 "masterfailover": "master-failover",
1576 return GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER},