#!/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 import itertools import simplejson from optparse import make_option from cStringIO import StringIO from ganeti.cli import * from ganeti import cli from ganeti import opcodes from ganeti import constants from ganeti import utils from ganeti import errors _SHUTDOWN_CLUSTER = "cluster" _SHUTDOWN_NODES_BOTH = "nodes" _SHUTDOWN_NODES_PRI = "nodes-pri" _SHUTDOWN_NODES_SEC = "nodes-sec" _SHUTDOWN_INSTANCES = "instances" _VALUE_TRUE = "true" #: default list of options for L{ListInstances} _LIST_DEF_FIELDS = [ "name", "hypervisor", "os", "pnode", "status", "oper_ram", ] def _ExpandMultiNames(mode, names, client=None): """Expand the given names using the passed mode. For _SHUTDOWN_CLUSTER, all instances will be returned. For _SHUTDOWN_NODES_PRI/SEC, all instances having those nodes as primary/secondary will be returned. For _SHUTDOWN_NODES_BOTH, all instances having those nodes as either primary or secondary will be returned. For _SHUTDOWN_INSTANCES, the given instances will be returned. @param mode: one of L{_SHUTDOWN_CLUSTER}, L{_SHUTDOWN_NODES_BOTH}, L{_SHUTDOWN_NODES_PRI}, L{_SHUTDOWN_NODES_SEC} or L{_SHUTDOWN_INSTANCES} @param names: a list of names; for cluster, it must be empty, and for node and instance it must be a list of valid item names (short names are valid as usual, e.g. node1 instead of node1.example.com) @rtype: list @return: the list of names after the expansion @raise errors.ProgrammerError: for unknown selection type @raise errors.OpPrereqError: for invalid input parameters """ if client is None: client = GetClient() if mode == _SHUTDOWN_CLUSTER: if names: raise errors.OpPrereqError("Cluster filter mode takes no arguments") idata = client.QueryInstances([], ["name"], False) inames = [row[0] for row in idata] elif mode in (_SHUTDOWN_NODES_BOTH, _SHUTDOWN_NODES_PRI, _SHUTDOWN_NODES_SEC): if not names: raise errors.OpPrereqError("No node names passed") ndata = client.QueryNodes(names, ["name", "pinst_list", "sinst_list"], False) ipri = [row[1] for row in ndata] pri_names = list(itertools.chain(*ipri)) isec = [row[2] for row in ndata] sec_names = list(itertools.chain(*isec)) if mode == _SHUTDOWN_NODES_BOTH: inames = pri_names + sec_names elif mode == _SHUTDOWN_NODES_PRI: inames = pri_names elif mode == _SHUTDOWN_NODES_SEC: inames = sec_names else: raise errors.ProgrammerError("Unhandled shutdown type") elif mode == _SHUTDOWN_INSTANCES: if not names: raise errors.OpPrereqError("No instance names passed") idata = client.QueryInstances(names, ["name"], False) inames = [row[0] for row in idata] else: raise errors.OpPrereqError("Unknown mode '%s'" % mode) return inames def _ConfirmOperation(inames, text, extra=""): """Ask the user to confirm an operation on a list of instances. This function is used to request confirmation for doing an operation on a given list of instances. @type inames: list @param inames: the list of names that we display when we ask for confirmation @type text: str @param text: the operation that the user should confirm (e.g. I{shutdown} or I{startup}) @rtype: boolean @return: True or False depending on user's confirmation. """ count = len(inames) msg = ("The %s will operate on %d instances.\n%s" "Do you want to continue?" % (text, count, extra)) affected = ("\nAffected instances:\n" + "\n".join([" %s" % name for name in inames])) choices = [('y', True, 'Yes, execute the %s' % text), ('n', False, 'No, abort the %s' % text)] if count > 20: choices.insert(1, ('v', 'v', 'View the list of affected instances')) ask = msg else: ask = msg + affected choice = AskUser(ask, choices) if choice == 'v': choices.pop(1) choice = AskUser(msg + affected, choices) return choice def _EnsureInstancesExist(client, names): """Check for and ensure the given instance names exist. This function will raise an OpPrereqError in case they don't exist. Otherwise it will exit cleanly. @type client: L{luxi.Client} @param client: the client to use for the query @type names: list @param names: the list of instance names to query @raise errors.OpPrereqError: in case any instance is missing """ # TODO: change LUQueryInstances to that it actually returns None # instead of raising an exception, or devise a better mechanism result = client.QueryInstances(names, ["name"], False) for orig_name, row in zip(names, result): if row[0] is None: raise errors.OpPrereqError("Instance '%s' does not exist" % orig_name) def ListInstances(opts, args): """List instances and their properties. @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.output is None: selected_fields = _LIST_DEF_FIELDS elif opts.output.startswith("+"): selected_fields = _LIST_DEF_FIELDS + opts.output[1:].split(",") else: selected_fields = opts.output.split(",") output = GetClient().QueryInstances(args, selected_fields, opts.do_locking) if not opts.no_headers: headers = { "name": "Instance", "os": "OS", "pnode": "Primary_node", "snodes": "Secondary_Nodes", "admin_state": "Autostart", "oper_state": "Running", "oper_ram": "Memory", "disk_template": "Disk_template", "ip": "IP_address", "mac": "MAC_address", "bridge": "Bridge", "sda_size": "Disk/0", "sdb_size": "Disk/1", "disk_usage": "DiskUsage", "status": "Status", "tags": "Tags", "network_port": "Network_port", "hv/kernel_path": "Kernel_path", "hv/initrd_path": "Initrd_path", "hv/boot_order": "HVM_boot_order", "hv/acpi": "HVM_ACPI", "hv/pae": "HVM_PAE", "hv/cdrom_image_path": "HVM_CDROM_image_path", "hv/nic_type": "HVM_NIC_type", "hv/disk_type": "HVM_Disk_type", "hv/vnc_bind_address": "VNC_bind_address", "serial_no": "SerialNo", "hypervisor": "Hypervisor", "hvparams": "Hypervisor_parameters", "be/memory": "Configured_memory", "be/vcpus": "VCPUs", "be/auto_balance": "Auto_balance", "disk.count": "Disks", "disk.sizes": "Disk_sizes", "nic.count": "NICs", "nic.ips": "NIC_IPs", "nic.bridges": "NIC_bridges", "nic.macs": "NIC_MACs", } else: headers = None unitfields = ["be/memory", "oper_ram", "sd(a|b)_size", "disk\.size/.*"] numfields = ["be/memory", "oper_ram", "sd(a|b)_size", "be/vcpus", "serial_no", "(disk|nic)\.count", "disk\.size/.*"] list_type_fields = ("tags", "disk.sizes", "nic.macs", "nic.ips", "nic.bridges") # change raw values to nicer strings for row in output: for idx, field in enumerate(selected_fields): val = row[idx] if field == "snodes": val = ",".join(val) or "-" elif field == "admin_state": if val: val = "yes" else: val = "no" elif field == "oper_state": if val is None: val = "(node down)" elif val: # True val = "running" else: val = "stopped" elif field == "oper_ram": if val is None: val = "(node down)" elif field == "sda_size" or field == "sdb_size": if val is None: val = "N/A" elif field in list_type_fields: val = ",".join(str(item) for item in val) elif val is None: val = "-" row[idx] = str(val) data = GenerateTable(separator=opts.separator, headers=headers, fields=selected_fields, unitfields=unitfields, numfields=numfields, data=output, units=opts.units) for line in data: ToStdout(line) return 0 def AddInstance(opts, args): """Add an instance to the cluster. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the new instance name @rtype: int @return: the desired exit code """ instance = args[0] (pnode, snode) = SplitNodeOption(opts.node) hypervisor = None hvparams = {} if opts.hypervisor: hypervisor, hvparams = opts.hypervisor if opts.nics: try: nic_max = max(int(nidx[0])+1 for nidx in opts.nics) except ValueError, err: raise errors.OpPrereqError("Invalid NIC index passed: %s" % str(err)) nics = [{}] * nic_max for nidx, ndict in opts.nics: nidx = int(nidx) nics[nidx] = ndict elif opts.no_nics: # no nics nics = [] else: # default of one nic, all auto nics = [{}] if opts.disk_template == constants.DT_DISKLESS: if opts.disks or opts.sd_size is not None: raise errors.OpPrereqError("Diskless instance but disk" " information passed") disks = [] else: if not opts.disks and not opts.sd_size: raise errors.OpPrereqError("No disk information specified") if opts.disks and opts.sd_size is not None: raise errors.OpPrereqError("Please use either the '--disk' or" " '-s' option") if opts.sd_size is not None: opts.disks = [(0, {"size": opts.sd_size})] try: disk_max = max(int(didx[0])+1 for didx in opts.disks) except ValueError, err: raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err)) disks = [{}] * disk_max for didx, ddict in opts.disks: didx = int(didx) if "size" not in ddict: raise errors.OpPrereqError("Missing size for disk %d" % didx) try: ddict["size"] = utils.ParseUnit(ddict["size"]) except ValueError, err: raise errors.OpPrereqError("Invalid disk size for disk %d: %s" % (didx, err)) disks[didx] = ddict utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_TYPES) utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES) op = opcodes.OpCreateInstance(instance_name=instance, disks=disks, disk_template=opts.disk_template, nics=nics, mode=constants.INSTANCE_CREATE, os_type=opts.os, pnode=pnode, snode=snode, start=opts.start, ip_check=opts.ip_check, wait_for_sync=opts.wait_for_sync, hypervisor=hypervisor, hvparams=hvparams, beparams=opts.beparams, iallocator=opts.iallocator, file_storage_dir=opts.file_storage_dir, file_driver=opts.file_driver, ) SubmitOrSend(op, opts) return 0 def BatchCreate(opts, args): """Create instances using a definition file. This function reads a json file with instances defined in the form:: {"instance-name":{ "disk_size": [20480], "template": "drbd", "backend": { "memory": 512, "vcpus": 1 }, "os": "debootstrap", "primary_node": "firstnode", "secondary_node": "secondnode", "iallocator": "dumb"} } Note that I{primary_node} and I{secondary_node} have precedence over I{iallocator}. @param opts: the command line options selected by the user @type args: list @param args: should contain one element, the json filename @rtype: int @return: the desired exit code """ _DEFAULT_SPECS = {"disk_size": [20 * 1024], "backend": {}, "iallocator": None, "primary_node": None, "secondary_node": None, "ip": 'none', "mac": 'auto', "bridge": None, "start": True, "ip_check": True, "hypervisor": None, "hvparams": {}, "file_storage_dir": None, "file_driver": 'loop'} def _PopulateWithDefaults(spec): """Returns a new hash combined with default values.""" mydict = _DEFAULT_SPECS.copy() mydict.update(spec) return mydict def _Validate(spec): """Validate the instance specs.""" # Validate fields required under any circumstances for required_field in ('os', 'template'): if required_field not in spec: raise errors.OpPrereqError('Required field "%s" is missing.' % required_field) # Validate special fields if spec['primary_node'] is not None: if (spec['template'] in constants.DTS_NET_MIRROR and spec['secondary_node'] is None): raise errors.OpPrereqError('Template requires secondary node, but' ' there was no secondary provided.') elif spec['iallocator'] is None: raise errors.OpPrereqError('You have to provide at least a primary_node' ' or an iallocator.') if (spec['hvparams'] and not isinstance(spec['hvparams'], dict)): raise errors.OpPrereqError('Hypervisor parameters must be a dict.') json_filename = args[0] try: fd = open(json_filename, 'r') instance_data = simplejson.load(fd) fd.close() except Exception, err: ToStderr("Can't parse the instance definition file: %s" % str(err)) return 1 jex = JobExecutor() # Iterate over the instances and do: # * Populate the specs with default value # * Validate the instance specs i_names = utils.NiceSort(instance_data.keys()) for name in i_names: specs = instance_data[name] specs = _PopulateWithDefaults(specs) _Validate(specs) hypervisor = specs['hypervisor'] hvparams = specs['hvparams'] disks = [] for elem in specs['disk_size']: try: size = utils.ParseUnit(elem) except ValueError, err: raise errors.OpPrereqError("Invalid disk size '%s' for" " instance %s: %s" % (elem, name, err)) disks.append({"size": size}) nic0 = {'ip': specs['ip'], 'bridge': specs['bridge'], 'mac': specs['mac']} utils.ForceDictType(specs['backend'], constants.BES_PARAMETER_TYPES) utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES) op = opcodes.OpCreateInstance(instance_name=name, disks=disks, disk_template=specs['template'], mode=constants.INSTANCE_CREATE, os_type=specs['os'], pnode=specs['primary_node'], snode=specs['secondary_node'], nics=[nic0], start=specs['start'], ip_check=specs['ip_check'], wait_for_sync=True, iallocator=specs['iallocator'], hypervisor=hypervisor, hvparams=hvparams, beparams=specs['backend'], file_storage_dir=specs['file_storage_dir'], file_driver=specs['file_driver']) jex.QueueJob(name, op) # we never want to wait, just show the submitted job IDs jex.WaitOrShow(False) return 0 def ReinstallInstance(opts, args): """Reinstall an instance. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the name of the instance to be reinstalled @rtype: int @return: the desired exit code """ # first, compute the desired name list if opts.multi_mode is None: opts.multi_mode = _SHUTDOWN_INSTANCES inames = _ExpandMultiNames(opts.multi_mode, args) if not inames: raise errors.OpPrereqError("Selection filter does not match any instances") # second, if requested, ask for an OS if opts.select_os is True: op = opcodes.OpDiagnoseOS(output_fields=["name", "valid"], names=[]) result = SubmitOpCode(op) if not result: ToStdout("Can't get the OS list") return 1 ToStdout("Available OS templates:") number = 0 choices = [] for entry in result: ToStdout("%3s: %s", number, entry[0]) choices.append(("%s" % number, entry[0], entry[0])) number = number + 1 choices.append(('x', 'exit', 'Exit gnt-instance reinstall')) selected = AskUser("Enter OS template number (or x to abort):", choices) if selected == 'exit': ToStderr("User aborted reinstall, exiting") return 1 os_name = selected else: os_name = opts.os # third, get confirmation: multi-reinstall requires --force-multi # *and* --force, single-reinstall just --force multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1 if multi_on: warn_msg = "Note: this will remove *all* data for the below instances!\n" if not ((opts.force_multi and opts.force) or _ConfirmOperation(inames, "reinstall", extra=warn_msg)): return 1 else: if not opts.force: usertext = ("This will reinstall the instance %s and remove" " all data. Continue?") % instance_name if not AskUser(usertext): return 1 jex = JobExecutor(verbose=multi_on) for instance_name in inames: op = opcodes.OpReinstallInstance(instance_name=instance_name, os_type=os_name) jex.QueueJob(instance_name, op) jex.WaitOrShow(not opts.submit_only) return 0 def RemoveInstance(opts, args): """Remove an instance. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the name of the instance to be removed @rtype: int @return: the desired exit code """ instance_name = args[0] force = opts.force cl = GetClient() if not force: _EnsureInstancesExist(cl, [instance_name]) usertext = ("This will remove the volumes of the instance %s" " (including mirrors), thus removing all the data" " of the instance. Continue?") % instance_name if not AskUser(usertext): return 1 op = opcodes.OpRemoveInstance(instance_name=instance_name, ignore_failures=opts.ignore_failures) SubmitOrSend(op, opts, cl=cl) return 0 def RenameInstance(opts, args): """Rename an instance. @param opts: the command line options selected by the user @type args: list @param args: should contain two elements, the old and the new instance names @rtype: int @return: the desired exit code """ op = opcodes.OpRenameInstance(instance_name=args[0], new_name=args[1], ignore_ip=opts.ignore_ip) SubmitOrSend(op, opts) return 0 def ActivateDisks(opts, args): """Activate an instance's disks. This serves two purposes: - it allows (as long as the instance is not running) mounting the disks and modifying them from the node - it repairs inactive secondary drbds @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the instance name @rtype: int @return: the desired exit code """ instance_name = args[0] op = opcodes.OpActivateInstanceDisks(instance_name=instance_name) disks_info = SubmitOrSend(op, opts) for host, iname, nname in disks_info: ToStdout("%s:%s:%s", host, iname, nname) return 0 def DeactivateDisks(opts, args): """Deactivate an instance's disks.. This function takes the instance name, looks for its primary node and the tries to shutdown its block devices on that node. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the instance name @rtype: int @return: the desired exit code """ instance_name = args[0] op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name) SubmitOrSend(op, opts) return 0 def GrowDisk(opts, args): """Grow an instance's disks. @param opts: the command line options selected by the user @type args: list @param args: should contain two elements, the instance name whose disks we grow and the disk name, e.g. I{sda} @rtype: int @return: the desired exit code """ instance = args[0] disk = args[1] try: disk = int(disk) except ValueError, err: raise errors.OpPrereqError("Invalid disk index: %s" % str(err)) amount = utils.ParseUnit(args[2]) op = opcodes.OpGrowDisk(instance_name=instance, disk=disk, amount=amount, wait_for_sync=opts.wait_for_sync) SubmitOrSend(op, opts) return 0 def StartupInstance(opts, args): """Startup instances. Depending on the options given, this will start one or more instances. @param opts: the command line options selected by the user @type args: list @param args: the instance or node names based on which we create the final selection (in conjunction with the opts argument) @rtype: int @return: the desired exit code """ cl = GetClient() if opts.multi_mode is None: opts.multi_mode = _SHUTDOWN_INSTANCES inames = _ExpandMultiNames(opts.multi_mode, args, client=cl) if not inames: raise errors.OpPrereqError("Selection filter does not match any instances") multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1 if not (opts.force_multi or not multi_on or _ConfirmOperation(inames, "startup")): return 1 jex = cli.JobExecutor(verbose=multi_on, cl=cl) for name in inames: op = opcodes.OpStartupInstance(instance_name=name, force=opts.force) # do not add these parameters to the opcode unless they're defined if opts.hvparams: op.hvparams = opts.hvparams if opts.beparams: op.beparams = opts.beparams jex.QueueJob(name, op) jex.WaitOrShow(not opts.submit_only) return 0 def RebootInstance(opts, args): """Reboot instance(s). Depending on the parameters given, this will reboot one or more instances. @param opts: the command line options selected by the user @type args: list @param args: the instance or node names based on which we create the final selection (in conjunction with the opts argument) @rtype: int @return: the desired exit code """ cl = GetClient() if opts.multi_mode is None: opts.multi_mode = _SHUTDOWN_INSTANCES inames = _ExpandMultiNames(opts.multi_mode, args, client=cl) if not inames: raise errors.OpPrereqError("Selection filter does not match any instances") multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1 if not (opts.force_multi or not multi_on or _ConfirmOperation(inames, "reboot")): return 1 jex = JobExecutor(verbose=multi_on, cl=cl) for name in inames: op = opcodes.OpRebootInstance(instance_name=name, reboot_type=opts.reboot_type, ignore_secondaries=opts.ignore_secondaries) jex.QueueJob(name, op) jex.WaitOrShow(not opts.submit_only) return 0 def ShutdownInstance(opts, args): """Shutdown an instance. @param opts: the command line options selected by the user @type args: list @param args: the instance or node names based on which we create the final selection (in conjunction with the opts argument) @rtype: int @return: the desired exit code """ cl = GetClient() if opts.multi_mode is None: opts.multi_mode = _SHUTDOWN_INSTANCES inames = _ExpandMultiNames(opts.multi_mode, args, client=cl) if not inames: raise errors.OpPrereqError("Selection filter does not match any instances") multi_on = opts.multi_mode != _SHUTDOWN_INSTANCES or len(inames) > 1 if not (opts.force_multi or not multi_on or _ConfirmOperation(inames, "shutdown")): return 1 jex = cli.JobExecutor(verbose=multi_on, cl=cl) for name in inames: op = opcodes.OpShutdownInstance(instance_name=name) jex.QueueJob(name, op) jex.WaitOrShow(not opts.submit_only) return 0 def ReplaceDisks(opts, args): """Replace the disks of an instance @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the instance name @rtype: int @return: the desired exit code """ instance_name = args[0] new_2ndary = opts.new_secondary iallocator = opts.iallocator if opts.disks is None: disks = [] else: try: disks = [int(i) for i in opts.disks.split(",")] except ValueError, err: raise errors.OpPrereqError("Invalid disk index passed: %s" % str(err)) cnt = [opts.on_primary, opts.on_secondary, new_2ndary is not None, iallocator is not None].count(True) if cnt != 1: raise errors.OpPrereqError("One and only one of the -p, -s, -n and -i" " options must be passed") elif opts.on_primary: mode = constants.REPLACE_DISK_PRI elif opts.on_secondary: mode = constants.REPLACE_DISK_SEC elif new_2ndary is not None or iallocator is not None: # replace secondary mode = constants.REPLACE_DISK_CHG op = opcodes.OpReplaceDisks(instance_name=args[0], disks=disks, remote_node=new_2ndary, mode=mode, iallocator=iallocator) SubmitOrSend(op, opts) return 0 def FailoverInstance(opts, args): """Failover an instance. The failover is done by shutting it down on its present node and starting it on the secondary. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the instance name @rtype: int @return: the desired exit code """ cl = GetClient() instance_name = args[0] force = opts.force if not force: _EnsureInstancesExist(cl, [instance_name]) usertext = ("Failover will happen to image %s." " This requires a shutdown of the instance. Continue?" % (instance_name,)) if not AskUser(usertext): return 1 op = opcodes.OpFailoverInstance(instance_name=instance_name, ignore_consistency=opts.ignore_consistency) SubmitOrSend(op, opts, cl=cl) return 0 def MigrateInstance(opts, args): """Migrate an instance. The migrate is done without shutdown. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the instance name @rtype: int @return: the desired exit code """ cl = GetClient() instance_name = args[0] force = opts.force if not force: _EnsureInstancesExist(cl, [instance_name]) if opts.cleanup: usertext = ("Instance %s will be recovered from a failed migration." " Note that the migration procedure (including cleanup)" % (instance_name,)) else: usertext = ("Instance %s will be migrated. Note that migration" % (instance_name,)) usertext += (" is **experimental** in this version." " This might impact the instance if anything goes wrong." " Continue?") if not AskUser(usertext): return 1 op = opcodes.OpMigrateInstance(instance_name=instance_name, live=opts.live, cleanup=opts.cleanup) SubmitOpCode(op, cl=cl) return 0 def ConnectToInstanceConsole(opts, args): """Connect to the console of an instance. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the instance name @rtype: int @return: the desired exit code """ instance_name = args[0] op = opcodes.OpConnectConsole(instance_name=instance_name) cmd = SubmitOpCode(op) if opts.show_command: ToStdout("%s", utils.ShellQuoteArgs(cmd)) else: try: os.execvp(cmd[0], cmd) finally: ToStderr("Can't run console command %s with arguments:\n'%s'", cmd[0], " ".join(cmd)) os._exit(1) def _FormatLogicalID(dev_type, logical_id): """Formats the logical_id of a disk. """ if dev_type == constants.LD_DRBD8: node_a, node_b, port, minor_a, minor_b, key = logical_id data = [ ("nodeA", "%s, minor=%s" % (node_a, minor_a)), ("nodeB", "%s, minor=%s" % (node_b, minor_b)), ("port", port), ("auth key", key), ] elif dev_type == constants.LD_LV: vg_name, lv_name = logical_id data = ["%s/%s" % (vg_name, lv_name)] else: data = [str(logical_id)] return data def _FormatBlockDevInfo(idx, top_level, dev, static): """Show block device information. This is only used by L{ShowInstanceConfig}, but it's too big to be left for an inline definition. @type idx: int @param idx: the index of the current disk @type top_level: boolean @param top_level: if this a top-level disk? @type dev: dict @param dev: dictionary with disk information @type static: boolean @param static: wheter the device information doesn't contain runtime information but only static data @return: a list of either strings, tuples or lists (which should be formatted at a higher indent level) """ def helper(dtype, status): """Format one line for physical device status. @type dtype: str @param dtype: a constant from the L{constants.LDS_BLOCK} set @type status: tuple @param status: a tuple as returned from L{backend.FindBlockDevice} @return: the string representing the status """ if not status: return "not active" txt = "" (path, major, minor, syncp, estt, degr, ldisk) = status if major is None: major_string = "N/A" else: major_string = str(major) if minor is None: minor_string = "N/A" else: minor_string = str(minor) txt += ("%s (%s:%s)" % (path, major_string, minor_string)) if dtype in (constants.LD_DRBD8, ): if syncp is not None: sync_text = "*RECOVERING* %5.2f%%," % syncp if estt: sync_text += " ETA %ds" % estt else: sync_text += " ETA unknown" else: sync_text = "in sync" if degr: degr_text = "*DEGRADED*" else: degr_text = "ok" if ldisk: ldisk_text = " *MISSING DISK*" else: ldisk_text = "" txt += (" %s, status %s%s" % (sync_text, degr_text, ldisk_text)) elif dtype == constants.LD_LV: if ldisk: ldisk_text = " *FAILED* (failed drive?)" else: ldisk_text = "" txt += ldisk_text return txt # the header if top_level: if dev["iv_name"] is not None: txt = dev["iv_name"] else: txt = "disk %d" % idx else: txt = "child %d" % idx d1 = ["- %s: %s" % (txt, dev["dev_type"])] data = [] if top_level: data.append(("access mode", dev["mode"])) if dev["logical_id"] is not None: try: l_id = _FormatLogicalID(dev["dev_type"], dev["logical_id"]) except ValueError: l_id = [str(dev["logical_id"])] if len(l_id) == 1: data.append(("logical_id", l_id[0])) else: data.extend(l_id) elif dev["physical_id"] is not None: data.append("physical_id:") data.append([dev["physical_id"]]) if not static: data.append(("on primary", helper(dev["dev_type"], dev["pstatus"]))) if dev["sstatus"] and not static: data.append(("on secondary", helper(dev["dev_type"], dev["sstatus"]))) if dev["children"]: data.append("child devices:") for c_idx, child in enumerate(dev["children"]): data.append(_FormatBlockDevInfo(c_idx, False, child, static)) d1.append(data) return d1 def _FormatList(buf, data, indent_level): """Formats a list of data at a given indent level. If the element of the list is: - a string, it is simply formatted as is - a tuple, it will be split into key, value and the all the values in a list will be aligned all at the same start column - a list, will be recursively formatted @type buf: StringIO @param buf: the buffer into which we write the output @param data: the list to format @type indent_level: int @param indent_level: the indent level to format at """ max_tlen = max([len(elem[0]) for elem in data if isinstance(elem, tuple)] or [0]) for elem in data: if isinstance(elem, basestring): buf.write("%*s%s\n" % (2*indent_level, "", elem)) elif isinstance(elem, tuple): key, value = elem spacer = "%*s" % (max_tlen - len(key), "") buf.write("%*s%s:%s %s\n" % (2*indent_level, "", key, spacer, value)) elif isinstance(elem, list): _FormatList(buf, elem, indent_level+1) def ShowInstanceConfig(opts, args): """Compute instance run-time status. @param opts: the command line options selected by the user @type args: list @param args: either an empty list, and then we query all instances, or should contain a list of instance names @rtype: int @return: the desired exit code """ if not args and not opts.show_all: ToStderr("No instance selected." " Please pass in --all if you want to query all instances.\n" "Note that this can take a long time on a big cluster.") return 1 elif args and opts.show_all: ToStderr("Cannot use --all if you specify instance names.") return 1 retcode = 0 op = opcodes.OpQueryInstanceData(instances=args, static=opts.static) result = SubmitOpCode(op) if not result: ToStdout("No instances.") return 1 buf = StringIO() retcode = 0 for instance_name in result: instance = result[instance_name] buf.write("Instance name: %s\n" % instance["name"]) buf.write("State: configured to be %s" % instance["config_state"]) if not opts.static: buf.write(", actual state is %s" % instance["run_state"]) buf.write("\n") ##buf.write("Considered for memory checks in cluster verify: %s\n" % ## instance["auto_balance"]) buf.write(" Nodes:\n") buf.write(" - primary: %s\n" % instance["pnode"]) buf.write(" - secondaries: %s\n" % ", ".join(instance["snodes"])) buf.write(" Operating system: %s\n" % instance["os"]) if instance.has_key("network_port"): buf.write(" Allocated network port: %s\n" % instance["network_port"]) buf.write(" Hypervisor: %s\n" % instance["hypervisor"]) # custom VNC console information vnc_bind_address = instance["hv_actual"].get(constants.HV_VNC_BIND_ADDRESS, None) if vnc_bind_address: port = instance["network_port"] display = int(port) - constants.VNC_BASE_PORT if display > 0 and vnc_bind_address == constants.BIND_ADDRESS_GLOBAL: vnc_console_port = "%s:%s (display %s)" % (instance["pnode"], port, display) elif display > 0 and utils.IsValidIP(vnc_bind_address): vnc_console_port = ("%s:%s (node %s) (display %s)" % (vnc_bind_address, port, instance["pnode"], display)) else: # vnc bind address is a file vnc_console_port = "%s:%s" % (instance["pnode"], vnc_bind_address) buf.write(" - console connection: vnc to %s\n" % vnc_console_port) for key in instance["hv_actual"]: if key in instance["hv_instance"]: val = instance["hv_instance"][key] else: val = "default (%s)" % instance["hv_actual"][key] buf.write(" - %s: %s\n" % (key, val)) buf.write(" Hardware:\n") buf.write(" - VCPUs: %d\n" % instance["be_actual"][constants.BE_VCPUS]) buf.write(" - memory: %dMiB\n" % instance["be_actual"][constants.BE_MEMORY]) buf.write(" - NICs:\n") for idx, (mac, ip, bridge) in enumerate(instance["nics"]): buf.write(" - nic/%d: MAC: %s, IP: %s, bridge: %s\n" % (idx, mac, ip, bridge)) buf.write(" Disks:\n") for idx, device in enumerate(instance["disks"]): _FormatList(buf, _FormatBlockDevInfo(idx, True, device, opts.static), 2) ToStdout(buf.getvalue().rstrip('\n')) return retcode def SetInstanceParams(opts, args): """Modifies an instance. All parameters take effect only at the next restart of the instance. @param opts: the command line options selected by the user @type args: list @param args: should contain only one element, the instance name @rtype: int @return: the desired exit code """ if not (opts.nics or opts.disks or opts.hypervisor or opts.beparams): ToStderr("Please give at least one of the parameters.") return 1 for param in opts.beparams: if isinstance(opts.beparams[param], basestring): if opts.beparams[param].lower() == "default": opts.beparams[param] = constants.VALUE_DEFAULT utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_TYPES, allowed_values=[constants.VALUE_DEFAULT]) for param in opts.hypervisor: if isinstance(opts.hypervisor[param], basestring): if opts.hypervisor[param].lower() == "default": opts.hypervisor[param] = constants.VALUE_DEFAULT utils.ForceDictType(opts.hypervisor, constants.HVS_PARAMETER_TYPES, allowed_values=[constants.VALUE_DEFAULT]) for idx, (nic_op, nic_dict) in enumerate(opts.nics): try: nic_op = int(nic_op) opts.nics[idx] = (nic_op, nic_dict) except ValueError: pass for idx, (disk_op, disk_dict) in enumerate(opts.disks): try: disk_op = int(disk_op) opts.disks[idx] = (disk_op, disk_dict) except ValueError: pass if disk_op == constants.DDM_ADD: if 'size' not in disk_dict: raise errors.OpPrereqError("Missing required parameter 'size'") disk_dict['size'] = utils.ParseUnit(disk_dict['size']) op = opcodes.OpSetInstanceParams(instance_name=args[0], nics=opts.nics, disks=opts.disks, hvparams=opts.hypervisor, beparams=opts.beparams, force=opts.force) # even if here we process the result, we allow submit only result = SubmitOrSend(op, opts) if result: ToStdout("Modified instance %s", args[0]) for param, data in result: ToStdout(" - %-5s -> %s", param, data) ToStdout("Please don't forget that these parameters take effect" " only at the next start of the instance.") return 0 # options used in more than one cmd node_opt = make_option("-n", "--node", dest="node", help="Target node", metavar="") os_opt = cli_option("-o", "--os-type", dest="os", help="What OS to run", metavar="") # multi-instance selection options m_force_multi = make_option("--force-multiple", dest="force_multi", help="Do not ask for confirmation when more than" " one instance is affected", action="store_true", default=False) m_pri_node_opt = make_option("--primary", dest="multi_mode", help="Filter by nodes (primary only)", const=_SHUTDOWN_NODES_PRI, action="store_const") m_sec_node_opt = make_option("--secondary", dest="multi_mode", help="Filter by nodes (secondary only)", const=_SHUTDOWN_NODES_SEC, action="store_const") m_node_opt = make_option("--node", dest="multi_mode", help="Filter by nodes (primary and secondary)", const=_SHUTDOWN_NODES_BOTH, action="store_const") m_clust_opt = make_option("--all", dest="multi_mode", help="Select all instances in the cluster", const=_SHUTDOWN_CLUSTER, action="store_const") m_inst_opt = make_option("--instance", dest="multi_mode", help="Filter by instance name [default]", const=_SHUTDOWN_INSTANCES, action="store_const") # this is defined separately due to readability only add_opts = [ DEBUG_OPT, make_option("-n", "--node", dest="node", help="Target node and optional secondary node", metavar="[:]"), os_opt, keyval_option("-B", "--backend", dest="beparams", type="keyval", default={}, help="Backend parameters"), make_option("-t", "--disk-template", dest="disk_template", help="Custom disk setup (diskless, file, plain or drbd)", default=None, metavar="TEMPL"), cli_option("-s", "--os-size", dest="sd_size", help="Disk size for a" " single-disk configuration, when not using the --disk option," " in MiB unless a suffix is used", default=None, type="unit", metavar=""), ikv_option("--disk", help="Disk information", default=[], dest="disks", action="append", type="identkeyval"), ikv_option("--net", help="NIC information", default=[], dest="nics", action="append", type="identkeyval"), make_option("--no-nics", default=False, action="store_true", help="Do not create any network cards for the instance"), make_option("--no-wait-for-sync", dest="wait_for_sync", default=True, action="store_false", help="Don't wait for sync (DANGEROUS!)"), make_option("--no-start", dest="start", default=True, action="store_false", help="Don't start the instance after" " creation"), make_option("--no-ip-check", dest="ip_check", default=True, action="store_false", help="Don't check that the instance's IP" " is alive (only valid with --no-start)"), make_option("--file-storage-dir", dest="file_storage_dir", help="Relative path under default cluster-wide file storage dir" " to store file-based disks", default=None, metavar=""), make_option("--file-driver", dest="file_driver", help="Driver to use" " for image files", default="loop", metavar=""), make_option("-I", "--iallocator", metavar="", help="Select nodes for the instance automatically using the" " iallocator plugin", default=None, type="string"), ikv_option("-H", "--hypervisor", dest="hypervisor", help="Hypervisor and hypervisor options, in the format" " hypervisor:option=value,option=value,...", default=None, type="identkeyval"), SUBMIT_OPT, ] commands = { 'add': (AddInstance, ARGS_ONE, add_opts, "[...] -t disk-type -n node[:secondary-node] -o os-type ", "Creates and adds a new instance to the cluster"), 'batch-create': (BatchCreate, ARGS_ONE, [DEBUG_OPT], "", "Create a bunch of instances based on specs in the file."), 'console': (ConnectToInstanceConsole, ARGS_ONE, [DEBUG_OPT, make_option("--show-cmd", dest="show_command", action="store_true", default=False, help=("Show command instead of executing it"))], "[--show-cmd] ", "Opens a console on the specified instance"), 'failover': (FailoverInstance, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, make_option("--ignore-consistency", dest="ignore_consistency", action="store_true", default=False, help="Ignore the consistency of the disks on" " the secondary"), SUBMIT_OPT, ], "[-f] ", "Stops the instance and starts it on the backup node, using" " the remote mirror (only for instances of type drbd)"), 'migrate': (MigrateInstance, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, make_option("--non-live", dest="live", default=True, action="store_false", help="Do a non-live migration (this usually means" " freeze the instance, save the state," " transfer and only then resume running on the" " secondary node)"), make_option("--cleanup", dest="cleanup", default=False, action="store_true", help="Instead of performing the migration, try to" " recover from a failed cleanup. This is safe" " to run even if the instance is healthy, but it" " will create extra replication traffic and " " disrupt briefly the replication (like during the" " migration"), ], "[-f] ", "Migrate instance to its secondary node" " (only for instances of type drbd)"), 'info': (ShowInstanceConfig, ARGS_ANY, [DEBUG_OPT, make_option("-s", "--static", dest="static", action="store_true", default=False, help="Only show configuration data, not runtime data"), make_option("--all", dest="show_all", default=False, action="store_true", help="Show info on all instances on the cluster." " This can take a long time to run, use wisely."), ], "[-s] {--all | ...}", "Show information on the specified instance(s)"), 'list': (ListInstances, ARGS_ANY, [DEBUG_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, SYNC_OPT], "[...]", "Lists the instances and their status. The available fields are" " (see the man page for details): status, oper_state, oper_ram," " name, os, pnode, snodes, admin_state, admin_ram, disk_template," " ip, mac, bridge, sda_size, sdb_size, vcpus, serial_no," " hypervisor." " The default field" " list is (in order): %s." % ", ".join(_LIST_DEF_FIELDS), ), 'reinstall': (ReinstallInstance, ARGS_ANY, [DEBUG_OPT, FORCE_OPT, os_opt, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, make_option("--select-os", dest="select_os", action="store_true", default=False, help="Interactive OS reinstall, lists available" " OS templates for selection"), SUBMIT_OPT, ], "[-f] ", "Reinstall a stopped instance"), 'remove': (RemoveInstance, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, make_option("--ignore-failures", dest="ignore_failures", action="store_true", default=False, help=("Remove the instance from the cluster even" " if there are failures during the removal" " process (shutdown, disk removal, etc.)")), SUBMIT_OPT, ], "[-f] ", "Shuts down the instance and removes it"), 'rename': (RenameInstance, ARGS_FIXED(2), [DEBUG_OPT, make_option("--no-ip-check", dest="ignore_ip", help="Do not check that the IP of the new name" " is alive", default=False, action="store_true"), SUBMIT_OPT, ], " ", "Rename the instance"), 'replace-disks': (ReplaceDisks, ARGS_ONE, [DEBUG_OPT, make_option("-n", "--new-secondary", dest="new_secondary", help=("New secondary node (for secondary" " node change)"), metavar="NODE", default=None), make_option("-p", "--on-primary", dest="on_primary", default=False, action="store_true", help=("Replace the disk(s) on the primary" " node (only for the drbd template)")), make_option("-s", "--on-secondary", dest="on_secondary", default=False, action="store_true", help=("Replace the disk(s) on the secondary" " node (only for the drbd template)")), make_option("--disks", dest="disks", default=None, help="Comma-separated list of disks" " indices to replace (e.g. 0,2) (optional," " defaults to all disks)"), make_option("-I", "--iallocator", metavar="", help="Select new secondary for the instance" " automatically using the" " iallocator plugin (enables" " secondary node replacement)", default=None, type="string"), SUBMIT_OPT, ], "[-s|-p|-n NODE|-I NAME] ", "Replaces all disks for the instance"), 'modify': (SetInstanceParams, ARGS_ONE, [DEBUG_OPT, FORCE_OPT, keyval_option("-H", "--hypervisor", type="keyval", default={}, dest="hypervisor", help="Change hypervisor parameters"), keyval_option("-B", "--backend", type="keyval", default={}, dest="beparams", help="Change backend parameters"), ikv_option("--disk", help="Disk changes", default=[], dest="disks", action="append", type="identkeyval"), ikv_option("--net", help="NIC changes", default=[], dest="nics", action="append", type="identkeyval"), SUBMIT_OPT, ], "", "Alters the parameters of an instance"), 'shutdown': (ShutdownInstance, ARGS_ANY, [DEBUG_OPT, m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, m_force_multi, SUBMIT_OPT, ], "", "Stops an instance"), 'startup': (StartupInstance, ARGS_ANY, [DEBUG_OPT, FORCE_OPT, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT, keyval_option("-H", "--hypervisor", type="keyval", default={}, dest="hvparams", help="Temporary hypervisor parameters"), keyval_option("-B", "--backend", type="keyval", default={}, dest="beparams", help="Temporary backend parameters"), ], "", "Starts an instance"), 'reboot': (RebootInstance, ARGS_ANY, [DEBUG_OPT, m_force_multi, make_option("-t", "--type", dest="reboot_type", help="Type of reboot: soft/hard/full", default=constants.INSTANCE_REBOOT_HARD, type="string", metavar=""), make_option("--ignore-secondaries", dest="ignore_secondaries", default=False, action="store_true", help="Ignore errors from secondaries"), m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT, ], "", "Reboots an instance"), 'activate-disks': (ActivateDisks, ARGS_ONE, [DEBUG_OPT, SUBMIT_OPT], "", "Activate an instance's disks"), 'deactivate-disks': (DeactivateDisks, ARGS_ONE, [DEBUG_OPT, SUBMIT_OPT], "", "Deactivate an instance's disks"), 'grow-disk': (GrowDisk, ARGS_FIXED(3), [DEBUG_OPT, SUBMIT_OPT, make_option("--no-wait-for-sync", dest="wait_for_sync", default=True, action="store_false", help="Don't wait for sync (DANGEROUS!)"), ], " ", "Grow an instance's disk"), 'list-tags': (ListTags, ARGS_ONE, [DEBUG_OPT], "", "List the tags of the given instance"), 'add-tags': (AddTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT], " tag...", "Add tags to the given instance"), 'remove-tags': (RemoveTags, ARGS_ATLEAST(1), [DEBUG_OPT, TAG_SRC_OPT], " tag...", "Remove tags from given instance"), } #: dictionary with aliases for commands aliases = { 'activate_block_devs': 'activate-disks', 'replace_disks': 'replace-disks', 'start': 'startup', 'stop': 'shutdown', } if __name__ == '__main__': sys.exit(GenericMain(commands, aliases=aliases, override={"tag_type": constants.TAG_INSTANCE}))