X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/73ebb9ff366620c2a89dfac88eaf9b398e0d7ce1..c90d8e610bcd21701bbdd03409bd3aee4916919f:/lib/backend.py diff --git a/lib/backend.py b/lib/backend.py index 8205381..338a30d 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -60,6 +60,8 @@ from ganeti import ssconf from ganeti import serializer from ganeti import netutils from ganeti import runtime +from ganeti import mcpu +from ganeti import compat _BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id" @@ -79,6 +81,10 @@ _IES_CA_FILE = "ca" #: Valid LVS output line regex _LVSLINE_REGEX = re.compile("^ *([^|]+)\|([^|]+)\|([0-9.]+)\|([^|]{6})\|?$") +# Actions for the master setup script +_MASTER_START = "start" +_MASTER_STOP = "stop" + class RPCFail(Exception): """Class denoting RPC failure. @@ -249,48 +255,109 @@ def GetMasterInfo(): master_netmask) -def ActivateMasterIp(): - """Activate the IP address of the master daemon. +def RunLocalHooks(hook_opcode, hooks_path, env_builder_fn): + """Decorator that runs hooks before and after the decorated function. + + @type hook_opcode: string + @param hook_opcode: opcode of the hook + @type hooks_path: string + @param hooks_path: path of the hooks + @type env_builder_fn: function + @param env_builder_fn: function that returns a dictionary containing the + environment variables for the hooks. Will get all the parameters of the + decorated function. + @raise RPCFail: in case of pre-hook failure """ - # GetMasterInfo will raise an exception if not able to return data - master_netdev, master_ip, _, family, master_netmask = GetMasterInfo() + def decorator(fn): + def wrapper(*args, **kwargs): + _, myself = ssconf.GetMasterAndMyself() + nodes = ([myself], [myself]) # these hooks run locally - err_msg = None - if netutils.TcpPing(master_ip, constants.DEFAULT_NODED_PORT): - if netutils.IPAddress.Own(master_ip): - # we already have the ip: - logging.debug("Master IP already configured, doing nothing") - else: - err_msg = "Someone else has the master ip, not activating" - logging.error(err_msg) + env_fn = compat.partial(env_builder_fn, *args, **kwargs) + + cfg = _GetConfig() + hr = HooksRunner() + hm = mcpu.HooksMaster(hook_opcode, hooks_path, nodes, hr.RunLocalHooks, + None, env_fn, logging.warning, cfg.GetClusterName(), + cfg.GetMasterNode()) + + hm.RunPhase(constants.HOOKS_PHASE_PRE) + result = fn(*args, **kwargs) + hm.RunPhase(constants.HOOKS_PHASE_POST) + + return result + return wrapper + return decorator + + +def _BuildMasterIpEnv(master_params, use_external_mip_script=None): + """Builds environment variables for master IP hooks. + + @type master_params: L{objects.MasterNetworkParameters} + @param master_params: network parameters of the master + @type use_external_mip_script: boolean + @param use_external_mip_script: whether to use an external master IP + address setup script (unused, but necessary per the implementation of the + _RunLocalHooks decorator) + + """ + # pylint: disable=W0613 + ver = netutils.IPAddress.GetVersionFromAddressFamily(master_params.ip_family) + env = { + "MASTER_NETDEV": master_params.netdev, + "MASTER_IP": master_params.ip, + "MASTER_NETMASK": str(master_params.netmask), + "CLUSTER_IP_VERSION": str(ver), + } + + return env + + +def _RunMasterSetupScript(master_params, action, use_external_mip_script): + """Execute the master IP address setup script. + + @type master_params: L{objects.MasterNetworkParameters} + @param master_params: network parameters of the master + @type action: string + @param action: action to pass to the script. Must be one of + L{backend._MASTER_START} or L{backend._MASTER_STOP} + @type use_external_mip_script: boolean + @param use_external_mip_script: whether to use an external master IP + address setup script + @raise backend.RPCFail: if there are errors during the execution of the + script + + """ + env = _BuildMasterIpEnv(master_params) + + if use_external_mip_script: + setup_script = constants.EXTERNAL_MASTER_SETUP_SCRIPT else: - ipcls = netutils.IP4Address - if family == netutils.IP6Address.family: - ipcls = netutils.IP6Address - - result = utils.RunCmd([constants.IP_COMMAND_PATH, "address", "add", - "%s/%s" % (master_ip, master_netmask), - "dev", master_netdev, "label", - "%s:0" % master_netdev]) - if result.failed: - err_msg = "Can't activate master IP: %s" % result.output - logging.error(err_msg) + setup_script = constants.DEFAULT_MASTER_SETUP_SCRIPT - else: - # we ignore the exit code of the following cmds - if ipcls == netutils.IP4Address: - utils.RunCmd(["arping", "-q", "-U", "-c 3", "-I", master_netdev, "-s", - master_ip, master_ip]) - elif ipcls == netutils.IP6Address: - try: - utils.RunCmd(["ndisc6", "-q", "-r 3", master_ip, master_netdev]) - except errors.OpExecError: - # TODO: Better error reporting - logging.warning("Can't execute ndisc6, please install if missing") + result = utils.RunCmd([setup_script, action], env=env, reset_env=True) + + if result.failed: + _Fail("Failed to %s the master IP. Script return value: %s" % + (action, result.exit_code), log=True) - if err_msg: - _Fail(err_msg) + +@RunLocalHooks(constants.FAKE_OP_MASTER_TURNUP, "master-ip-turnup", + _BuildMasterIpEnv) +def ActivateMasterIp(master_params, use_external_mip_script): + """Activate the IP address of the master daemon. + + @type master_params: L{objects.MasterNetworkParameters} + @param master_params: network parameters of the master + @type use_external_mip_script: boolean + @param use_external_mip_script: whether to use an external master IP + address setup script + @raise RPCFail: in case of errors during the IP startup + + """ + _RunMasterSetupScript(master_params, _MASTER_START, + use_external_mip_script) def StartMasterDaemons(no_voting): @@ -321,22 +388,21 @@ def StartMasterDaemons(no_voting): _Fail(msg) -def DeactivateMasterIp(): +@RunLocalHooks(constants.FAKE_OP_MASTER_TURNDOWN, "master-ip-turndown", + _BuildMasterIpEnv) +def DeactivateMasterIp(master_params, use_external_mip_script): """Deactivate the master IP on this node. - """ - # TODO: log and report back to the caller the error failures; we - # need to decide in which case we fail the RPC for this + @type master_params: L{objects.MasterNetworkParameters} + @param master_params: network parameters of the master + @type use_external_mip_script: boolean + @param use_external_mip_script: whether to use an external master IP + address setup script + @raise RPCFail: in case of errors during the IP turndown - # GetMasterInfo will raise an exception if not able to return data - master_netdev, master_ip, _, _, master_netmask = GetMasterInfo() - - result = utils.RunCmd([constants.IP_COMMAND_PATH, "address", "del", - "%s/%s" % (master_ip, master_netmask), - "dev", master_netdev]) - if result.failed: - logging.error("Can't remove the master IP, error: %s", result.output) - # but otherwise ignore the failure + """ + _RunMasterSetupScript(master_params, _MASTER_STOP, + use_external_mip_script) def StopMasterDaemons(): @@ -357,27 +423,35 @@ def StopMasterDaemons(): result.cmd, result.exit_code, result.output) -def ChangeMasterNetmask(netmask): +def ChangeMasterNetmask(old_netmask, netmask, master_ip, master_netdev): """Change the netmask of the master IP. + @param old_netmask: the old value of the netmask + @param netmask: the new value of the netmask + @param master_ip: the master IP + @param master_netdev: the master network device + """ - master_netdev, master_ip, _, _, old_netmask = GetMasterInfo() if old_netmask == netmask: return + if not netutils.IPAddress.Own(master_ip): + _Fail("The master IP address is not up, not attempting to change its" + " netmask") + result = utils.RunCmd([constants.IP_COMMAND_PATH, "address", "add", "%s/%s" % (master_ip, netmask), "dev", master_netdev, "label", "%s:0" % master_netdev]) if result.failed: - _Fail("Could not change the master IP netmask") + _Fail("Could not set the new netmask on the master IP address") result = utils.RunCmd([constants.IP_COMMAND_PATH, "address", "del", "%s/%s" % (master_ip, old_netmask), "dev", master_netdev, "label", "%s:0" % master_netdev]) if result.failed: - _Fail("Could not change the master IP netmask") + _Fail("Could not bring down the master IP address with the old netmask") def EtcHostsModify(mode, host, ip): @@ -448,44 +522,71 @@ def LeaveCluster(modify_ssh_setup): raise errors.QuitGanetiException(True, "Shutdown scheduled") -def GetNodeInfo(vgname, hypervisor_type): - """Gives back a hash with different information about the node. +def _GetVgInfo(name): + """Retrieves information about a LVM volume group. - @type vgname: C{string} - @param vgname: the name of the volume group to ask for disk space information - @type hypervisor_type: C{str} - @param hypervisor_type: the name of the hypervisor to ask for - memory information - @rtype: C{dict} - @return: dictionary with the following keys: - - vg_size is the size of the configured volume group in MiB - - vg_free is the free size of the volume group in MiB - - memory_dom0 is the memory allocated for domain0 in MiB - - memory_free is the currently available (free) ram in MiB - - memory_total is the total number of ram in MiB - - hv_version: the hypervisor version, if available + """ + # TODO: GetVGInfo supports returning information for multiple VGs at once + vginfo = bdev.LogicalVolume.GetVGInfo([name]) + if vginfo: + vg_free = int(round(vginfo[0][0], 0)) + vg_size = int(round(vginfo[0][1], 0)) + else: + vg_free = None + vg_size = None + + return { + "name": name, + "vg_free": vg_free, + "vg_size": vg_size, + } + + +def _GetHvInfo(name): + """Retrieves node information from a hypervisor. + + The information returned depends on the hypervisor. Common items: + + - vg_size is the size of the configured volume group in MiB + - vg_free is the free size of the volume group in MiB + - memory_dom0 is the memory allocated for domain0 in MiB + - memory_free is the currently available (free) ram in MiB + - memory_total is the total number of ram in MiB + - hv_version: the hypervisor version, if available + + """ + return hypervisor.GetHypervisor(name).GetNodeInfo() + + +def _GetNamedNodeInfo(names, fn): + """Calls C{fn} for all names in C{names} and returns a dictionary. + + @rtype: None or dict """ - outputarray = {} + if names is None: + return None + else: + return map(fn, names) + - if vgname is not None: - vginfo = bdev.LogicalVolume.GetVGInfo([vgname]) - vg_free = vg_size = None - if vginfo: - vg_free = int(round(vginfo[0][0], 0)) - vg_size = int(round(vginfo[0][1], 0)) - outputarray["vg_size"] = vg_size - outputarray["vg_free"] = vg_free +def GetNodeInfo(vg_names, hv_names): + """Gives back a hash with different information about the node. - if hypervisor_type is not None: - hyper = hypervisor.GetHypervisor(hypervisor_type) - hyp_info = hyper.GetNodeInfo() - if hyp_info is not None: - outputarray.update(hyp_info) + @type vg_names: list of string + @param vg_names: Names of the volume groups to ask for disk space information + @type hv_names: list of string + @param hv_names: Names of the hypervisors to ask for node information + @rtype: tuple; (string, None/dict, None/dict) + @return: Tuple containing boot ID, volume group information and hypervisor + information - outputarray["bootid"] = utils.ReadFile(_BOOT_ID_PATH, size=128).rstrip("\n") + """ + bootid = utils.ReadFile(_BOOT_ID_PATH, size=128).rstrip("\n") + vg_info = _GetNamedNodeInfo(vg_names, _GetVgInfo) + hv_info = _GetNamedNodeInfo(hv_names, _GetHvInfo) - return outputarray + return (bootid, vg_info, hv_info) def VerifyNode(what, cluster_name): @@ -599,6 +700,11 @@ def VerifyNode(what, cluster_name): result[constants.NV_MASTERIP] = netutils.TcpPing(master_ip, port, source=source) + if constants.NV_USERSCRIPTS in what: + result[constants.NV_USERSCRIPTS] = \ + [script for script in what[constants.NV_USERSCRIPTS] + if not (os.path.exists(script) and os.access(script, os.X_OK))] + if constants.NV_OOB_PATHS in what: result[constants.NV_OOB_PATHS] = tmp = [] for path in what[constants.NV_OOB_PATHS]: @@ -1270,6 +1376,27 @@ def InstanceReboot(instance, reboot_type, shutdown_timeout): _Fail("Invalid reboot_type received: %s", reboot_type) +def InstanceBalloonMemory(instance, memory): + """Resize an instance's memory. + + @type instance: L{objects.Instance} + @param instance: the instance object + @type memory: int + @param memory: new memory amount in MB + @rtype: None + + """ + hyper = hypervisor.GetHypervisor(instance.hypervisor) + running = hyper.ListInstances() + if instance.name not in running: + logging.info("Instance %s is not running, cannot balloon", instance.name) + return + try: + hyper.BalloonInstanceMemory(instance, memory) + except errors.HypervisorError, err: + _Fail("Failed to balloon instance memory: %s", err, exc=True) + + def MigrationInfo(instance): """Gather information about an instance to be migrated. @@ -1434,7 +1561,7 @@ def BlockdevCreate(disk, size, owner, on_primary, info): clist.append(crdev) try: - device = bdev.Create(disk.dev_type, disk.physical_id, clist, disk.size) + device = bdev.Create(disk, clist) except errors.BlockDeviceError, err: _Fail("Can't create block device: %s", err) @@ -1443,7 +1570,6 @@ def BlockdevCreate(disk, size, owner, on_primary, info): device.Assemble() except errors.BlockDeviceError, err: _Fail("Can't assemble device after creation, unusual event: %s", err) - device.SetSyncSpeed(constants.SYNC_SPEED) if on_primary or disk.OpenOnSecondary(): try: device.Open(force=True) @@ -1617,8 +1743,7 @@ def _RecursiveAssembleBD(disk, owner, as_primary): children.append(cdev) if as_primary or disk.AssembleOnSecondary(): - r_dev = bdev.Assemble(disk.dev_type, disk.physical_id, children, disk.size) - r_dev.SetSyncSpeed(constants.SYNC_SPEED) + r_dev = bdev.Assemble(disk, children) result = r_dev if as_primary or disk.OpenOnSecondary(): r_dev.Open() @@ -1811,7 +1936,7 @@ def _RecursiveFindBD(disk): for chdisk in disk.children: children.append(_RecursiveFindBD(chdisk)) - return bdev.FindDevice(disk.dev_type, disk.physical_id, children, disk.size) + return bdev.FindDevice(disk, children) def _OpenRealBD(disk): @@ -1997,24 +2122,6 @@ def WriteSsconfFiles(values): ssconf.SimpleStore().WriteFiles(values) -def _ErrnoOrStr(err): - """Format an EnvironmentError exception. - - If the L{err} argument has an errno attribute, it will be looked up - and converted into a textual C{E...} description. Otherwise the - string representation of the error will be returned. - - @type err: L{EnvironmentError} - @param err: the exception to format - - """ - if hasattr(err, "errno"): - detail = errno.errorcode[err.errno] - else: - detail = str(err) - return detail - - def _OSOndiskAPIVersion(os_dir): """Compute and return the API version of a given OS. @@ -2034,7 +2141,7 @@ def _OSOndiskAPIVersion(os_dir): st = os.stat(api_file) except EnvironmentError, err: return False, ("Required file '%s' not found under path %s: %s" % - (constants.OS_API_FILE, os_dir, _ErrnoOrStr(err))) + (constants.OS_API_FILE, os_dir, utils.ErrnoOrStr(err))) if not stat.S_ISREG(stat.S_IFMT(st.st_mode)): return False, ("File '%s' in %s is not a regular file" % @@ -2044,7 +2151,7 @@ def _OSOndiskAPIVersion(os_dir): api_versions = utils.ReadFile(api_file).splitlines() except EnvironmentError, err: return False, ("Error while reading the API version file at %s: %s" % - (api_file, _ErrnoOrStr(err))) + (api_file, utils.ErrnoOrStr(err))) try: api_versions = [int(version.strip()) for version in api_versions] @@ -2157,7 +2264,7 @@ def _TryOSFromDisk(name, base_dir=None): del os_files[filename] continue return False, ("File '%s' under path '%s' is missing (%s)" % - (filename, os_dir, _ErrnoOrStr(err))) + (filename, os_dir, utils.ErrnoOrStr(err))) if not stat.S_ISREG(stat.S_IFMT(st.st_mode)): return False, ("File '%s' under path '%s' is not a regular file" % @@ -2177,7 +2284,7 @@ def _TryOSFromDisk(name, base_dir=None): # we accept missing files, but not other errors if err.errno != errno.ENOENT: return False, ("Error while reading the OS variants file at %s: %s" % - (variants_file, _ErrnoOrStr(err))) + (variants_file, utils.ErrnoOrStr(err))) parameters = [] if constants.OS_PARAMETERS_FILE in os_files: @@ -2186,7 +2293,7 @@ def _TryOSFromDisk(name, base_dir=None): parameters = utils.ReadFile(parameters_file).splitlines() except EnvironmentError, err: return False, ("Error while reading the OS parameters file at %s: %s" % - (parameters_file, _ErrnoOrStr(err))) + (parameters_file, utils.ErrnoOrStr(err))) parameters = [v.split(None, 1) for v in parameters] os_obj = objects.OS(name=name, path=os_dir, @@ -2416,8 +2523,13 @@ def FinalizeExport(instance, snap_disks): config.add_section(constants.INISECT_INS) config.set(constants.INISECT_INS, "name", instance.name) + config.set(constants.INISECT_INS, "maxmem", "%d" % + instance.beparams[constants.BE_MAXMEM]) + config.set(constants.INISECT_INS, "minmem", "%d" % + instance.beparams[constants.BE_MINMEM]) + # "memory" is deprecated, but useful for exporting to old ganeti versions config.set(constants.INISECT_INS, "memory", "%d" % - instance.beparams[constants.BE_MEMORY]) + instance.beparams[constants.BE_MAXMEM]) config.set(constants.INISECT_INS, "vcpus", "%d" % instance.beparams[constants.BE_VCPUS]) config.set(constants.INISECT_INS, "disk_template", instance.disk_template) @@ -2724,7 +2836,10 @@ def JobQueueRename(old, new): _EnsureJobQueueFile(old) _EnsureJobQueueFile(new) - utils.RenameFile(old, new, mkdir=True) + getents = runtime.GetEnts() + + utils.RenameFile(old, new, mkdir=True, mkdir_mode=0700, + dir_uid=getents.masterd_uid, dir_gid=getents.masterd_gid) def BlockdevClose(instance_name, disks): @@ -3424,6 +3539,20 @@ class HooksRunner(object): # constant self._BASE_DIR = hooks_base_dir # pylint: disable=C0103 + def RunLocalHooks(self, node_list, hpath, phase, env): + """Check that the hooks will be run only locally and then run them. + + """ + assert len(node_list) == 1 + node = node_list[0] + _, myself = ssconf.GetMasterAndMyself() + assert node == myself + + results = self.RunHooks(hpath, phase, env) + + # Return values in the form expected by HooksMaster + return {node: (None, False, results)} + def RunHooks(self, hpath, phase, env): """Run the scripts in the hooks directory.