#!/usr/bin/python # # Copyright (C) 2006, 2007 Google Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. # pylint: disable-msg=W0401,W0614 # W0401: Wildcard import ganeti.cli # W0614: Unused import %s from wildcard import (since we need cli) import sys import os.path import time from ganeti.cli import * from ganeti import opcodes from ganeti import constants from ganeti import errors from ganeti import utils from ganeti import bootstrap from ganeti import ssh from ganeti import objects @UsesRPC def InitCluster(opts, args): """Initialize the cluster. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the desired cluster name @rtype: int @return: the desired exit code """ if not opts.lvm_storage and opts.vg_name: ToStderr("Options --no-lvm-storage and --vg-name conflict.") return 1 vg_name = opts.vg_name if opts.lvm_storage and not opts.vg_name: vg_name = constants.DEFAULT_VG hvlist = opts.enabled_hypervisors if hvlist is None: hvlist = constants.DEFAULT_ENABLED_HYPERVISOR hvlist = hvlist.split(",") hvparams = dict(opts.hvparams) beparams = opts.beparams nicparams = opts.nicparams # prepare beparams dict beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams) utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES) # prepare nicparams dict nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams) utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES) # prepare hvparams dict for hv in constants.HYPER_TYPES: if hv not in hvparams: hvparams[hv] = {} hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv]) utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES) if opts.candidate_pool_size is None: opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT if opts.mac_prefix is None: opts.mac_prefix = constants.DEFAULT_MAC_PREFIX bootstrap.InitCluster(cluster_name=args[0], secondary_ip=opts.secondary_ip, vg_name=vg_name, mac_prefix=opts.mac_prefix, master_netdev=opts.master_netdev, file_storage_dir=opts.file_storage_dir, enabled_hypervisors=hvlist, hvparams=hvparams, beparams=beparams, nicparams=nicparams, candidate_pool_size=opts.candidate_pool_size, modify_etc_hosts=opts.modify_etc_hosts, modify_ssh_setup=opts.modify_ssh_setup, ) op = opcodes.OpPostInitCluster() SubmitOpCode(op) return 0 @UsesRPC def DestroyCluster(opts, args): """Destroy the cluster. @param opts: the command line options selected by the user @type args: list @param args: should be an empty list @rtype: int @return: the desired exit code """ if not opts.yes_do_it: ToStderr("Destroying a cluster is irreversible. If you really want" " destroy this cluster, supply the --yes-do-it option.") return 1 op = opcodes.OpDestroyCluster() master = SubmitOpCode(op) # if we reached this, the opcode didn't fail; we can proceed to # shutdown all the daemons bootstrap.FinalizeClusterDestroy(master) return 0 def RenameCluster(opts, args): """Rename the cluster. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the new cluster name @rtype: int @return: the desired exit code """ name = args[0] if not opts.force: usertext = ("This will rename the cluster to '%s'. If you are connected" " over the network to the cluster name, the operation is very" " dangerous as the IP address will be removed from the node" " and the change may not go through. Continue?") % name if not AskUser(usertext): return 1 op = opcodes.OpRenameCluster(name=name) SubmitOpCode(op) return 0 def RedistributeConfig(opts, args): """Forces push of the cluster configuration. @param opts: the command line options selected by the user @type args: list @param args: empty list @rtype: int @return: the desired exit code """ op = opcodes.OpRedistributeConfig() SubmitOrSend(op, opts) return 0 def ShowClusterVersion(opts, args): """Write version of ganeti software to the standard output. @param opts: the command line options selected by the user @type args: list @param args: should be an empty list @rtype: int @return: the desired exit code """ cl = GetClient() result = cl.QueryClusterInfo() ToStdout("Software version: %s", result["software_version"]) ToStdout("Internode protocol: %s", result["protocol_version"]) ToStdout("Configuration format: %s", result["config_version"]) ToStdout("OS api version: %s", result["os_api_version"]) ToStdout("Export interface: %s", result["export_version"]) return 0 def ShowClusterMaster(opts, args): """Write name of master node to the standard output. @param opts: the command line options selected by the user @type args: list @param args: should be an empty list @rtype: int @return: the desired exit code """ master = bootstrap.GetMaster() ToStdout(master) return 0 def _PrintGroupedParams(paramsdict): """Print Grouped parameters (be, nic, disk) by group. @type paramsdict: dict of dicts @param paramsdict: {group: {param: value, ...}, ...} """ for gr_name, gr_dict in paramsdict.items(): ToStdout(" - %s:", gr_name) for item, val in gr_dict.iteritems(): ToStdout(" %s: %s", item, val) def ShowClusterConfig(opts, args): """Shows cluster information. @param opts: the command line options selected by the user @type args: list @param args: should be an empty list @rtype: int @return: the desired exit code """ cl = GetClient() result = cl.QueryClusterInfo() ToStdout("Cluster name: %s", result["name"]) ToStdout("Cluster UUID: %s", result["uuid"]) ToStdout("Creation time: %s", utils.FormatTime(result["ctime"])) ToStdout("Modification time: %s", utils.FormatTime(result["mtime"])) ToStdout("Master node: %s", result["master"]) ToStdout("Architecture (this node): %s (%s)", result["architecture"][0], result["architecture"][1]) if result["tags"]: tags = ", ".join(utils.NiceSort(result["tags"])) else: tags = "(none)" ToStdout("Tags: %s", tags) ToStdout("Default hypervisor: %s", result["default_hypervisor"]) ToStdout("Enabled hypervisors: %s", ", ".join(result["enabled_hypervisors"])) ToStdout("Hypervisor parameters:") _PrintGroupedParams(result["hvparams"]) ToStdout("Cluster parameters:") ToStdout(" - candidate pool size: %s", result["candidate_pool_size"]) ToStdout(" - master netdev: %s", result["master_netdev"]) ToStdout(" - lvm volume group: %s", result["volume_group_name"]) ToStdout(" - file storage path: %s", result["file_storage_dir"]) ToStdout("Default instance parameters:") _PrintGroupedParams(result["beparams"]) ToStdout("Default nic parameters:") _PrintGroupedParams(result["nicparams"]) return 0 def ClusterCopyFile(opts, args): """Copy a file from master to some nodes. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the path of the file to be copied @rtype: int @return: the desired exit code """ filename = args[0] if not os.path.exists(filename): raise errors.OpPrereqError("No such filename '%s'" % filename) cl = GetClient() myname = utils.HostInfo().name cluster_name = cl.QueryConfigValues(["cluster_name"])[0] results = GetOnlineNodes(nodes=opts.nodes, cl=cl) results = [name for name in results if name != myname] srun = ssh.SshRunner(cluster_name=cluster_name) for node in results: if not srun.CopyFileToNode(node, filename): ToStderr("Copy of file %s to node %s failed", filename, node) return 0 def RunClusterCommand(opts, args): """Run a command on some nodes. @param opts: the command line options selected by the user @type args: list @param args: should contain the command to be run and its arguments @rtype: int @return: the desired exit code """ cl = GetClient() command = " ".join(args) nodes = GetOnlineNodes(nodes=opts.nodes, cl=cl) cluster_name, master_node = cl.QueryConfigValues(["cluster_name", "master_node"]) srun = ssh.SshRunner(cluster_name=cluster_name) # Make sure master node is at list end if master_node in nodes: nodes.remove(master_node) nodes.append(master_node) for name in nodes: result = srun.Run(name, "root", command) ToStdout("------------------------------------------------") ToStdout("node: %s", name) ToStdout("%s", result.output) ToStdout("return code = %s", result.exit_code) return 0 def VerifyCluster(opts, args): """Verify integrity of cluster, performing various test on nodes. @param opts: the command line options selected by the user @type args: list @param args: should be an empty list @rtype: int @return: the desired exit code """ skip_checks = [] if opts.skip_nplusone_mem: skip_checks.append(constants.VERIFY_NPLUSONE_MEM) op = opcodes.OpVerifyCluster(skip_checks=skip_checks, verbose=opts.verbose, error_codes=opts.error_codes, debug_simulate_errors=opts.simulate_errors) if SubmitOpCode(op): return 0 else: return 1 def VerifyDisks(opts, args): """Verify integrity of cluster disks. @param opts: the command line options selected by the user @type args: list @param args: should be an empty list @rtype: int @return: the desired exit code """ op = opcodes.OpVerifyDisks() result = SubmitOpCode(op) if not isinstance(result, (list, tuple)) or len(result) != 3: raise errors.ProgrammerError("Unknown result type for OpVerifyDisks") bad_nodes, instances, missing = result retcode = constants.EXIT_SUCCESS if bad_nodes: for node, text in bad_nodes.items(): ToStdout("Error gathering data on node %s: %s", node, utils.SafeEncode(text[-400:])) retcode |= 1 ToStdout("You need to fix these nodes first before fixing instances") if instances: for iname in instances: if iname in missing: continue op = opcodes.OpActivateInstanceDisks(instance_name=iname) try: ToStdout("Activating disks for instance '%s'", iname) SubmitOpCode(op) except errors.GenericError, err: nret, msg = FormatError(err) retcode |= nret ToStderr("Error activating disks for instance %s: %s", iname, msg) if missing: for iname, ival in missing.iteritems(): all_missing = utils.all(ival, lambda x: x[0] in bad_nodes) if all_missing: ToStdout("Instance %s cannot be verified as it lives on" " broken nodes", iname) else: ToStdout("Instance %s has missing logical volumes:", iname) ival.sort() for node, vol in ival: if node in bad_nodes: ToStdout("\tbroken node %s /dev/xenvg/%s", node, vol) else: ToStdout("\t%s /dev/xenvg/%s", node, vol) ToStdout("You need to run replace_disks for all the above" " instances, if this message persist after fixing nodes.") retcode |= 1 return retcode def RepairDiskSizes(opts, args): """Verify sizes of cluster disks. @param opts: the command line options selected by the user @type args: list @param args: optional list of instances to restrict check to @rtype: int @return: the desired exit code """ op = opcodes.OpRepairDiskSizes(instances=args) SubmitOpCode(op) @UsesRPC def MasterFailover(opts, args): """Failover the master node. This command, when run on a non-master node, will cause the current master to cease being master, and the non-master to become new master. @param opts: the command line options selected by the user @type args: list @param args: should be an empty list @rtype: int @return: the desired exit code """ if opts.no_voting: usertext = ("This will perform the failover even if most other nodes" " are down, or if this node is outdated. This is dangerous" " as it can lead to a non-consistent cluster. Check the" " gnt-cluster(8) man page before proceeding. Continue?") if not AskUser(usertext): return 1 return bootstrap.MasterFailover(no_voting=opts.no_voting) def SearchTags(opts, args): """Searches the tags on all the cluster. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the tag pattern @rtype: int @return: the desired exit code """ op = opcodes.OpSearchTags(pattern=args[0]) result = SubmitOpCode(op) if not result: return 1 result = list(result) result.sort() for path, tag in result: ToStdout("%s %s", path, tag) def SetClusterParams(opts, args): """Modify the cluster. @param opts: the command line options selected by the user @type args: list @param args: should be an empty list @rtype: int @return: the desired exit code """ if not (not opts.lvm_storage or opts.vg_name or opts.enabled_hypervisors or opts.hvparams or opts.beparams or opts.nicparams or opts.candidate_pool_size is not None): ToStderr("Please give at least one of the parameters.") return 1 vg_name = opts.vg_name if not opts.lvm_storage and opts.vg_name: ToStdout("Options --no-lvm-storage and --vg-name conflict.") return 1 elif not opts.lvm_storage: vg_name = '' hvlist = opts.enabled_hypervisors if hvlist is not None: hvlist = hvlist.split(",") # a list of (name, dict) we can pass directly to dict() (or []) hvparams = dict(opts.hvparams) for hv, hv_params in hvparams.iteritems(): utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES) beparams = opts.beparams utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES) nicparams = opts.nicparams utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES) op = opcodes.OpSetClusterParams(vg_name=vg_name, enabled_hypervisors=hvlist, hvparams=hvparams, beparams=beparams, nicparams=nicparams, candidate_pool_size=opts.candidate_pool_size) SubmitOpCode(op) return 0 def QueueOps(opts, args): """Queue operations. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the subcommand @rtype: int @return: the desired exit code """ command = args[0] client = GetClient() if command in ("drain", "undrain"): drain_flag = command == "drain" client.SetQueueDrainFlag(drain_flag) elif command == "info": result = client.QueryConfigValues(["drain_flag"]) if result[0]: val = "set" else: val = "unset" ToStdout("The drain flag is %s" % val) else: raise errors.OpPrereqError("Command '%s' is not valid." % command) return 0 def _ShowWatcherPause(until): if until is None or until < time.time(): ToStdout("The watcher is not paused.") else: ToStdout("The watcher is paused until %s.", time.ctime(until)) def WatcherOps(opts, args): """Watcher operations. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the subcommand @rtype: int @return: the desired exit code """ command = args[0] client = GetClient() if command == "continue": client.SetWatcherPause(None) ToStdout("The watcher is no longer paused.") elif command == "pause": if len(args) < 2: raise errors.OpPrereqError("Missing pause duration") result = client.SetWatcherPause(time.time() + ParseTimespec(args[1])) _ShowWatcherPause(result) elif command == "info": result = client.QueryConfigValues(["watcher_pause"]) _ShowWatcherPause(result) else: raise errors.OpPrereqError("Command '%s' is not valid." % command) return 0 commands = { 'init': ( InitCluster, [ArgHost(min=1, max=1)], [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT, HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT, SECONDARY_IP_OPT, VG_NAME_OPT], "[opts...] ", "Initialises a new cluster configuration"), 'destroy': ( DestroyCluster, ARGS_NONE, [YES_DOIT_OPT], "", "Destroy cluster"), 'rename': ( RenameCluster, [ArgHost(min=1, max=1)], [FORCE_OPT], "", "Renames the cluster"), 'redist-conf': ( RedistributeConfig, ARGS_NONE, [SUBMIT_OPT], "", "Forces a push of the configuration file and ssconf files" " to the nodes in the cluster"), 'verify': ( VerifyCluster, ARGS_NONE, [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT], "", "Does a check on the cluster configuration"), 'verify-disks': ( VerifyDisks, ARGS_NONE, [], "", "Does a check on the cluster disk status"), 'repair-disk-sizes': ( RepairDiskSizes, ARGS_MANY_INSTANCES, [], "", "Updates mismatches in recorded disk sizes"), 'masterfailover': ( MasterFailover, ARGS_NONE, [NOVOTING_OPT], "", "Makes the current node the master"), 'version': ( ShowClusterVersion, ARGS_NONE, [], "", "Shows the cluster version"), 'getmaster': ( ShowClusterMaster, ARGS_NONE, [], "", "Shows the cluster master"), 'copyfile': ( ClusterCopyFile, [ArgFile(min=1, max=1)], [NODE_LIST_OPT], "[-n node...] ", "Copies a file to all (or only some) nodes"), 'command': ( RunClusterCommand, [ArgCommand(min=1)], [NODE_LIST_OPT], "[-n node...] ", "Runs a command on all (or only some) nodes"), 'info': ( ShowClusterConfig, ARGS_NONE, [], "", "Show cluster configuration"), 'list-tags': ( ListTags, ARGS_NONE, [], "", "List the tags of the cluster"), 'add-tags': ( AddTags, [ArgUnknown()], [TAG_SRC_OPT], "tag...", "Add tags to the cluster"), 'remove-tags': ( RemoveTags, [ArgUnknown()], [TAG_SRC_OPT], "tag...", "Remove tags from the cluster"), 'search-tags': ( SearchTags, [ArgUnknown(min=1, max=1)], [], "", "Searches the tags on all objects on" " the cluster for a given pattern (regex)"), 'queue': ( QueueOps, [ArgChoice(min=1, max=1, choices=["drain", "undrain", "info"])], [], "drain|undrain|info", "Change queue properties"), 'watcher': ( WatcherOps, [ArgChoice(min=1, max=1, choices=["pause", "continue", "info"]), ArgSuggest(min=0, max=1, choices=["30m", "1h", "4h"])], [], "{pause |continue|info}", "Change watcher properties"), 'modify': ( SetClusterParams, ARGS_NONE, [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT, NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT], "[opts...]", "Alters the parameters of the cluster"), } if __name__ == '__main__': sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER}))