X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/218f4c3de706aca7e4521d7e1975f517cf5ecb9b..fac489a56410b927f19f81ef72755cab9f2b9d29:/lib/config.py diff --git a/lib/config.py b/lib/config.py index e77bb34..de06e6e 100644 --- a/lib/config.py +++ b/lib/config.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc. +# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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 @@ -31,9 +31,10 @@ much memory. """ -# pylint: disable-msg=R0904 +# pylint: disable=R0904 # R0904: Too many public methods +import copy import os import random import logging @@ -50,6 +51,8 @@ from ganeti import serializer from ganeti import uidpool from ganeti import netutils from ganeti import runtime +from ganeti import pathutils +from ganeti import network _config_lock = locking.SharedLock("ConfigWriter") @@ -106,6 +109,17 @@ class TemporaryReservationManager: all_reserved.update(holder_reserved) return all_reserved + def GetECReserved(self, ec_id): + """ Used when you want to retrieve all reservations for a specific + execution context. E.g when commiting reserved IPs for a specific + network. + + """ + ec_reserved = set() + if ec_id in self._ec_reserved: + ec_reserved.update(self._ec_reserved[ec_id]) + return ec_reserved + def Generate(self, existing, generate_one_fn, ec_id): """Generate a new resource of this type @@ -126,6 +140,33 @@ class TemporaryReservationManager: return new_resource +def _MatchNameComponentIgnoreCase(short_name, names): + """Wrapper around L{utils.text.MatchNameComponent}. + + """ + return utils.MatchNameComponent(short_name, names, case_sensitive=False) + + +def _CheckInstanceDiskIvNames(disks): + """Checks if instance's disks' C{iv_name} attributes are in order. + + @type disks: list of L{objects.Disk} + @param disks: List of disks + @rtype: list of tuples; (int, string, string) + @return: List of wrongly named disks, each tuple contains disk index, + expected and actual name + + """ + result = [] + + for (idx, disk) in enumerate(disks): + exp_iv_name = "disk/%s" % idx + if disk.iv_name != exp_iv_name: + result.append((idx, exp_iv_name, disk.iv_name)) + + return result + + class ConfigWriter: """The interface to the cluster configuration. @@ -140,7 +181,7 @@ class ConfigWriter: self._config_data = None self._offline = offline if cfg_file is None: - self._cfg_file = constants.CLUSTER_CONF_FILE + self._cfg_file = pathutils.CLUSTER_CONF_FILE else: self._cfg_file = cfg_file self._getents = _getents @@ -149,8 +190,10 @@ class ConfigWriter: self._temporary_macs = TemporaryReservationManager() self._temporary_secrets = TemporaryReservationManager() self._temporary_lvs = TemporaryReservationManager() + self._temporary_ips = TemporaryReservationManager() self._all_rms = [self._temporary_ids, self._temporary_macs, - self._temporary_secrets, self._temporary_lvs] + self._temporary_secrets, self._temporary_lvs, + self._temporary_ips] # Note: in order to prevent errors when resolving our name in # _DistributeConfig, we compute it here once and reuse it; it's # better to raise an error before starting to modify the config @@ -158,32 +201,34 @@ class ConfigWriter: self._my_hostname = netutils.Hostname.GetSysName() self._last_cluster_serial = -1 self._cfg_id = None + self._context = None self._OpenConfig(accept_foreign) + def _GetRpc(self, address_list): + """Returns RPC runner for configuration. + + """ + return rpc.ConfigRunner(self._context, address_list) + + def SetContext(self, context): + """Sets Ganeti context. + + """ + self._context = context + # this method needs to be static, so that we can call it on the class @staticmethod def IsCluster(): """Check if the cluster is configured. """ - return os.path.exists(constants.CLUSTER_CONF_FILE) - - def _GenerateOneMAC(self): - """Generate one mac address - - """ - prefix = self._config_data.cluster.mac_prefix - byte1 = random.randrange(0, 256) - byte2 = random.randrange(0, 256) - byte3 = random.randrange(0, 256) - mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3) - return mac + return os.path.exists(pathutils.CLUSTER_CONF_FILE) @locking.ssynchronized(_config_lock, shared=1) def GetNdParams(self, node): """Get the node params populated with cluster defaults. - @type node: L{object.Node} + @type node: L{objects.Node} @param node: The node we want to know the params for @return: A dict with the filled in node params @@ -192,14 +237,80 @@ class ConfigWriter: return self._config_data.cluster.FillND(node, nodegroup) @locking.ssynchronized(_config_lock, shared=1) - def GenerateMAC(self, ec_id): + def GetInstanceDiskParams(self, instance): + """Get the disk params populated with inherit chain. + + @type instance: L{objects.Instance} + @param instance: The instance we want to know the params for + @return: A dict with the filled in disk params + + """ + node = self._UnlockedGetNodeInfo(instance.primary_node) + nodegroup = self._UnlockedGetNodeGroup(node.group) + return self._UnlockedGetGroupDiskParams(nodegroup) + + @locking.ssynchronized(_config_lock, shared=1) + def GetGroupDiskParams(self, group): + """Get the disk params populated with inherit chain. + + @type group: L{objects.NodeGroup} + @param group: The group we want to know the params for + @return: A dict with the filled in disk params + + """ + return self._UnlockedGetGroupDiskParams(group) + + def _UnlockedGetGroupDiskParams(self, group): + """Get the disk params populated with inherit chain down to node-group. + + @type group: L{objects.NodeGroup} + @param group: The group we want to know the params for + @return: A dict with the filled in disk params + + """ + return self._config_data.cluster.SimpleFillDP(group.diskparams) + + def _UnlockedGetNetworkMACPrefix(self, net_uuid): + """Return the network mac prefix if it exists or the cluster level default. + + """ + prefix = None + if net_uuid: + nobj = self._UnlockedGetNetwork(net_uuid) + if nobj.mac_prefix: + prefix = nobj.mac_prefix + + return prefix + + def _GenerateOneMAC(self, prefix=None): + """Return a function that randomly generates a MAC suffic + and appends it to the given prefix. If prefix is not given get + the cluster level default. + + """ + if not prefix: + prefix = self._config_data.cluster.mac_prefix + + def GenMac(): + byte1 = random.randrange(0, 256) + byte2 = random.randrange(0, 256) + byte3 = random.randrange(0, 256) + mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3) + return mac + + return GenMac + + @locking.ssynchronized(_config_lock, shared=1) + def GenerateMAC(self, net_uuid, ec_id): """Generate a MAC for an instance. This should check the current instances for duplicates. """ existing = self._AllMACs() - return self._temporary_ids.Generate(existing, self._GenerateOneMAC, ec_id) + prefix = self._UnlockedGetNetworkMACPrefix(net_uuid) + gen_mac = self._GenerateOneMAC(prefix) + return self._temporary_ids.Generate(existing, gen_mac, ec_id) @locking.ssynchronized(_config_lock, shared=1) def ReserveMAC(self, mac, ec_id): @@ -215,6 +326,89 @@ class ConfigWriter: else: self._temporary_macs.Reserve(ec_id, mac) + def _UnlockedCommitTemporaryIps(self, ec_id): + """Commit all reserved IP address to their respective pools + + """ + for action, address, net_uuid in self._temporary_ips.GetECReserved(ec_id): + self._UnlockedCommitIp(action, net_uuid, address) + + def _UnlockedCommitIp(self, action, net_uuid, address): + """Commit a reserved IP address to an IP pool. + + The IP address is taken from the network's IP pool and marked as reserved. + + """ + nobj = self._UnlockedGetNetwork(net_uuid) + pool = network.AddressPool(nobj) + if action == constants.RESERVE_ACTION: + pool.Reserve(address) + elif action == constants.RELEASE_ACTION: + pool.Release(address) + + def _UnlockedReleaseIp(self, net_uuid, address, ec_id): + """Give a specific IP address back to an IP pool. + + The IP address is returned to the IP pool designated by pool_id and marked + as reserved. + + """ + self._temporary_ips.Reserve(ec_id, + (constants.RELEASE_ACTION, address, net_uuid)) + + @locking.ssynchronized(_config_lock, shared=1) + def ReleaseIp(self, net_uuid, address, ec_id): + """Give a specified IP address back to an IP pool. + + This is just a wrapper around _UnlockedReleaseIp. + + """ + if net_uuid: + self._UnlockedReleaseIp(net_uuid, address, ec_id) + + @locking.ssynchronized(_config_lock, shared=1) + def GenerateIp(self, net_uuid, ec_id): + """Find a free IPv4 address for an instance. + + """ + nobj = self._UnlockedGetNetwork(net_uuid) + pool = network.AddressPool(nobj) + + def gen_one(): + try: + ip = pool.GenerateFree() + except errors.AddressPoolError: + raise errors.ReservationError("Cannot generate IP. Network is full") + return (constants.RESERVE_ACTION, ip, net_uuid) + + _, address, _ = self._temporary_ips.Generate([], gen_one, ec_id) + return address + + def _UnlockedReserveIp(self, net_uuid, address, ec_id): + """Reserve a given IPv4 address for use by an instance. + + """ + nobj = self._UnlockedGetNetwork(net_uuid) + pool = network.AddressPool(nobj) + try: + isreserved = pool.IsReserved(address) + except errors.AddressPoolError: + raise errors.ReservationError("IP address not in network") + if isreserved: + raise errors.ReservationError("IP address already in use") + + return self._temporary_ips.Reserve(ec_id, + (constants.RESERVE_ACTION, + address, net_uuid)) + + @locking.ssynchronized(_config_lock, shared=1) + def ReserveIp(self, net_uuid, address, ec_id): + """Reserve a given IPv4 address for use by an instance. + + """ + if net_uuid: + return self._UnlockedReserveIp(net_uuid, address, ec_id) + @locking.ssynchronized(_config_lock, shared=1) def ReserveLV(self, lv_name, ec_id): """Reserve an VG/LV pair for an instance. @@ -251,6 +445,24 @@ class ConfigWriter: lvnames.update(lv_list) return lvnames + def _AllDisks(self): + """Compute the list of all Disks. + + """ + disks = [] + for instance in self._config_data.instances.values(): + disks.extend(instance.disks) + return disks + + def _AllNICs(self): + """Compute the list of all NICs. + + """ + nics = [] + for instance in self._config_data.instances.values(): + nics.extend(instance.nics) + return nics + def _AllIDs(self, include_temporary): """Compute the list of all UUIDs and names we have. @@ -368,7 +580,7 @@ class ConfigWriter: configuration errors """ - # pylint: disable-msg=R0914 + # pylint: disable=R0914 result = [] seen_macs = [] ports = {} @@ -383,13 +595,21 @@ class ConfigWriter: invalid_hvs = set(cluster.enabled_hypervisors) - constants.HYPER_TYPES if invalid_hvs: result.append("enabled hypervisors contains invalid entries: %s" % - invalid_hvs) + utils.CommaJoin(invalid_hvs)) missing_hvp = (set(cluster.enabled_hypervisors) - set(cluster.hvparams.keys())) if missing_hvp: result.append("hypervisor parameters missing for the enabled" " hypervisor(s) %s" % utils.CommaJoin(missing_hvp)) + if not cluster.enabled_disk_templates: + result.append("enabled disk templates list doesn't have any entries") + invalid_disk_templates = set(cluster.enabled_disk_templates) \ + - constants.DISK_TEMPLATES + if invalid_disk_templates: + result.append("enabled disk templates list contains invalid entries:" + " %s" % utils.CommaJoin(invalid_disk_templates)) + if cluster.master_node not in data.nodes: result.append("cluster has invalid primary node '%s'" % cluster.master_node) @@ -406,6 +626,34 @@ class ConfigWriter: except errors.ConfigurationError, err: result.append("%s has invalid nicparams: %s" % (owner, err)) + def _helper_ipolicy(owner, ipolicy, iscluster): + try: + objects.InstancePolicy.CheckParameterSyntax(ipolicy, iscluster) + except errors.ConfigurationError, err: + result.append("%s has invalid instance policy: %s" % (owner, err)) + for key, value in ipolicy.items(): + if key == constants.ISPECS_MINMAX: + for k in range(len(value)): + _helper_ispecs(owner, "ipolicy/%s[%s]" % (key, k), value[k]) + elif key == constants.ISPECS_STD: + _helper(owner, "ipolicy/" + key, value, + constants.ISPECS_PARAMETER_TYPES) + else: + # FIXME: assuming list type + if key in constants.IPOLICY_PARAMETERS: + exp_type = float + else: + exp_type = list + if not isinstance(value, exp_type): + result.append("%s has invalid instance policy: for %s," + " expecting %s, got %s" % + (owner, key, exp_type.__name__, type(value))) + + def _helper_ispecs(owner, parentkey, params): + for (key, value) in params.items(): + fullkey = "/".join([parentkey, key]) + _helper(owner, fullkey, value, constants.ISPECS_PARAMETER_TYPES) + # check cluster parameters _helper("cluster", "beparams", cluster.SimpleFillBE({}), constants.BES_PARAMETER_TYPES) @@ -414,6 +662,7 @@ class ConfigWriter: _helper_nic("cluster", cluster.SimpleFillNIC({})) _helper("cluster", "ndparams", cluster.SimpleFillND({}), constants.NDS_PARAMETER_TYPES) + _helper_ipolicy("cluster", cluster.ipolicy, True) # per-instance checks for instance_name in data.instances: @@ -441,18 +690,23 @@ class ConfigWriter: filled, constants.NICS_PARAMETER_TYPES) _helper_nic(owner, filled) + # disk template checks + if not instance.disk_template in data.cluster.enabled_disk_templates: + result.append("instance '%s' uses the disabled disk template '%s'." % + (instance_name, instance.disk_template)) + # parameter checks if instance.beparams: _helper("instance %s" % instance.name, "beparams", cluster.FillBE(instance), constants.BES_PARAMETER_TYPES) # gather the drbd ports for duplicate checks - for dsk in instance.disks: + for (idx, dsk) in enumerate(instance.disks): if dsk.dev_type in constants.LDS_DRBD: tcp_port = dsk.logical_id[2] if tcp_port not in ports: ports[tcp_port] = [] - ports[tcp_port].append((instance.name, "drbd disk %s" % dsk.iv_name)) + ports[tcp_port].append((instance.name, "drbd disk %s" % idx)) # gather network port reservation net_port = getattr(instance, "network_port", None) if net_port is not None: @@ -466,6 +720,15 @@ class ConfigWriter: (instance.name, idx, msg) for msg in disk.Verify()]) result.extend(self._CheckDiskIDs(disk, seen_lids, seen_pids)) + wrong_names = _CheckInstanceDiskIvNames(instance.disks) + if wrong_names: + tmp = "; ".join(("name of disk %s should be '%s', but is '%s'" % + (idx, exp_name, actual_name)) + for (idx, exp_name, actual_name) in wrong_names) + + result.append("Instance '%s' has wrongly named disks: %s" % + (instance.name, tmp)) + # cluster-wide pool of free ports for free_port in cluster.tcpudp_port_pool: if free_port not in ports: @@ -513,6 +776,10 @@ class ConfigWriter: _helper("node %s" % node.name, "ndparams", cluster.FillND(node, data.nodegroups[node.group]), constants.NDS_PARAMETER_TYPES) + used_globals = constants.NDC_GLOBALS.intersection(node.ndparams) + if used_globals: + result.append("Node '%s' has some global parameters set: %s" % + (node.name, utils.CommaJoin(used_globals))) # nodegroups checks nodegroups_names = set() @@ -528,12 +795,14 @@ class ConfigWriter: result.append("duplicate node group name '%s'" % nodegroup.name) else: nodegroups_names.add(nodegroup.name) + group_name = "group %s" % nodegroup.name + _helper_ipolicy(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy), + False) if nodegroup.ndparams: - _helper("group %s" % nodegroup.name, "ndparams", + _helper(group_name, "ndparams", cluster.SimpleFillND(nodegroup.ndparams), constants.NDS_PARAMETER_TYPES) - # drbd minors check _, duplicates = self._UnlockedComputeDRBDMap() for node, minor, instance_a, instance_b in duplicates: @@ -570,7 +839,7 @@ class ConfigWriter: else: raise errors.ProgrammerError("NIC mode '%s' not handled" % nic_mode) - _AddIpAddress("%s/%s" % (link, nic.ip), + _AddIpAddress("%s/%s/%s" % (link, nic.ip, nic.network), "instance:%s/nic:%d" % (instance.name, idx)) for ip, owners in ips.items(): @@ -648,12 +917,15 @@ class ConfigWriter: def AddTcpUdpPort(self, port): """Adds a new port to the available port pool. + @warning: this method does not "flush" the configuration (via + L{_WriteConfig}); callers should do that themselves once the + configuration is stable + """ if not isinstance(port, int): raise errors.ProgrammerError("Invalid type passed for port") self._config_data.cluster.tcpudp_port_pool.add(port) - self._WriteConfig() @locking.ssynchronized(_config_lock, shared=1) def GetPortList(self): @@ -872,6 +1144,20 @@ class ConfigWriter: return self._config_data.cluster.master_netdev @locking.ssynchronized(_config_lock, shared=1) + def GetMasterNetmask(self): + """Get the netmask of the master node for this cluster. + + """ + return self._config_data.cluster.master_netmask + + @locking.ssynchronized(_config_lock, shared=1) + def GetUseExternalMipScript(self): + """Get flag representing whether to use the external master IP setup script. + + """ + return self._config_data.cluster.use_external_mip_script + + @locking.ssynchronized(_config_lock, shared=1) def GetFileStorageDir(self): """Get the file storage dir for this cluster. @@ -879,6 +1165,13 @@ class ConfigWriter: return self._config_data.cluster.file_storage_dir @locking.ssynchronized(_config_lock, shared=1) + def GetSharedFileStorageDir(self): + """Get the shared file storage dir for this cluster. + + """ + return self._config_data.cluster.shared_file_storage_dir + + @locking.ssynchronized(_config_lock, shared=1) def GetHypervisorType(self): """Get the hypervisor type for this cluster. @@ -911,6 +1204,22 @@ class ConfigWriter: """ return self._config_data.cluster.primary_ip_family + @locking.ssynchronized(_config_lock, shared=1) + def GetMasterNetworkParameters(self): + """Get network parameters of the master node. + + @rtype: L{object.MasterNetworkParameters} + @return: network parameters of the master node + + """ + cluster = self._config_data.cluster + result = objects.MasterNetworkParameters( + name=cluster.master_node, ip=cluster.master_ip, + netmask=cluster.master_netmask, netdev=cluster.master_netdev, + ip_family=cluster.primary_ip_family) + + return result + @locking.ssynchronized(_config_lock) def AddNodeGroup(self, group, ec_id, check_uuid=True): """Add a node group to the configuration. @@ -993,7 +1302,7 @@ class ConfigWriter: if target is None: if len(self._config_data.nodegroups) != 1: raise errors.OpPrereqError("More than one node group exists. Target" - " group must be specified explicitely.") + " group must be specified explicitly.") else: return self._config_data.nodegroups.keys()[0] if target in self._config_data.nodegroups: @@ -1058,6 +1367,28 @@ class ConfigWriter: """ return self._config_data.nodegroups.keys() + @locking.ssynchronized(_config_lock, shared=1) + def GetNodeGroupMembersByNodes(self, nodes): + """Get nodes which are member in the same nodegroups as the given nodes. + + """ + ngfn = lambda node_name: self._UnlockedGetNodeInfo(node_name).group + return frozenset(member_name + for node_name in nodes + for member_name in + self._UnlockedGetNodeGroup(ngfn(node_name)).members) + + @locking.ssynchronized(_config_lock, shared=1) + def GetMultiNodeGroupInfo(self, group_uuids): + """Get the configuration of multiple node groups. + + @param group_uuids: List of node group UUIDs + @rtype: list + @return: List of tuples of (group_uuid, group_info) + + """ + return [(uuid, self._UnlockedGetNodeGroup(uuid)) for uuid in group_uuids] + @locking.ssynchronized(_config_lock) def AddInstance(self, instance, ec_id): """Add an instance to the config. @@ -1089,6 +1420,7 @@ class ConfigWriter: self._config_data.instances[instance.name] = instance self._config_data.cluster.serial_no += 1 self._UnlockedReleaseDRBDMinors(instance.name) + self._UnlockedCommitTemporaryIps(ec_id) self._WriteConfig() def _EnsureUUID(self, item, ec_id): @@ -1104,19 +1436,27 @@ class ConfigWriter: raise errors.ConfigurationError("Cannot add '%s': UUID %s already" " in use" % (item.name, item.uuid)) - def _SetInstanceStatus(self, instance_name, status): + def _SetInstanceStatus(self, instance_name, status, disks_active): """Set the instance's status to a given value. """ - assert isinstance(status, bool), \ - "Invalid status '%s' passed to SetInstanceStatus" % (status,) - if instance_name not in self._config_data.instances: raise errors.ConfigurationError("Unknown instance '%s'" % instance_name) instance = self._config_data.instances[instance_name] - if instance.admin_up != status: - instance.admin_up = status + + if status is None: + status = instance.admin_state + if disks_active is None: + disks_active = instance.disks_active + + assert status in constants.ADMINST_ALL, \ + "Invalid status '%s' passed to SetInstanceStatus" % (status,) + + if instance.admin_state != status or \ + instance.disks_active != disks_active: + instance.admin_state = status + instance.disks_active = disks_active instance.serial_no += 1 instance.mtime = time.time() self._WriteConfig() @@ -1125,8 +1465,19 @@ class ConfigWriter: def MarkInstanceUp(self, instance_name): """Mark the instance status to up in the config. + This also sets the instance disks active flag. + """ - self._SetInstanceStatus(instance_name, True) + self._SetInstanceStatus(instance_name, constants.ADMINST_UP, True) + + @locking.ssynchronized(_config_lock) + def MarkInstanceOffline(self, instance_name): + """Mark the instance status to down in the config. + + This also clears the instance disks active flag. + + """ + self._SetInstanceStatus(instance_name, constants.ADMINST_OFFLINE, False) @locking.ssynchronized(_config_lock) def RemoveInstance(self, instance_name): @@ -1143,6 +1494,13 @@ class ConfigWriter: if network_port is not None: self._config_data.cluster.tcpudp_port_pool.add(network_port) + instance = self._UnlockedGetInstanceInfo(instance_name) + + for nic in instance.nics: + if nic.network and nic.ip: + # Return all IP addresses to the respective address pools + self._UnlockedCommitIp(constants.RELEASE_ACTION, nic.network, nic.ip) + del self._config_data.instances[instance_name] self._config_data.cluster.serial_no += 1 self._WriteConfig() @@ -1158,32 +1516,52 @@ class ConfigWriter: """ if old_name not in self._config_data.instances: raise errors.ConfigurationError("Unknown instance '%s'" % old_name) - inst = self._config_data.instances[old_name] - del self._config_data.instances[old_name] + + # Operate on a copy to not loose instance object in case of a failure + inst = self._config_data.instances[old_name].Copy() inst.name = new_name - for disk in inst.disks: + for (idx, disk) in enumerate(inst.disks): if disk.dev_type == constants.LD_FILE: # rename the file paths in logical and physical id file_storage_dir = os.path.dirname(os.path.dirname(disk.logical_id[1])) - disk_fname = "disk%s" % disk.iv_name.split("/")[1] - disk.physical_id = disk.logical_id = (disk.logical_id[0], - utils.PathJoin(file_storage_dir, - inst.name, - disk_fname)) + disk.logical_id = (disk.logical_id[0], + utils.PathJoin(file_storage_dir, inst.name, + "disk%s" % idx)) + disk.physical_id = disk.logical_id + + # Actually replace instance object + del self._config_data.instances[old_name] + self._config_data.instances[inst.name] = inst # Force update of ssconf files self._config_data.cluster.serial_no += 1 - self._config_data.instances[inst.name] = inst self._WriteConfig() @locking.ssynchronized(_config_lock) def MarkInstanceDown(self, instance_name): """Mark the status of an instance to down in the configuration. + This does not touch the instance disks active flag, as shut down instances + can still have active disks. + + """ + self._SetInstanceStatus(instance_name, constants.ADMINST_DOWN, None) + + @locking.ssynchronized(_config_lock) + def MarkInstanceDisksActive(self, instance_name): + """Mark the status of instance disks active. + + """ + self._SetInstanceStatus(instance_name, None, True) + + @locking.ssynchronized(_config_lock) + def MarkInstanceDisksInactive(self, instance_name): + """Mark the status of instance disks inactive. + """ - self._SetInstanceStatus(instance_name, False) + self._SetInstanceStatus(instance_name, None, False) def _UnlockedGetInstanceList(self): """Get the list of instances. @@ -1203,14 +1581,12 @@ class ConfigWriter: """ return self._UnlockedGetInstanceList() - @locking.ssynchronized(_config_lock, shared=1) def ExpandInstanceName(self, short_name): """Attempt to expand an incomplete instance name. """ - return utils.MatchNameComponent(short_name, - self._config_data.instances.keys(), - case_sensitive=False) + # Locking is done in L{ConfigWriter.GetInstanceList} + return _MatchNameComponentIgnoreCase(short_name, self.GetInstanceList()) def _UnlockedGetInstanceInfo(self, instance_name): """Returns information about an instance. @@ -1240,6 +1616,56 @@ class ConfigWriter: return self._UnlockedGetInstanceInfo(instance_name) @locking.ssynchronized(_config_lock, shared=1) + def GetInstanceNodeGroups(self, instance_name, primary_only=False): + """Returns set of node group UUIDs for instance's nodes. + + @rtype: frozenset + + """ + instance = self._UnlockedGetInstanceInfo(instance_name) + if not instance: + raise errors.ConfigurationError("Unknown instance '%s'" % instance_name) + + if primary_only: + nodes = [instance.primary_node] + else: + nodes = instance.all_nodes + + return frozenset(self._UnlockedGetNodeInfo(node_name).group + for node_name in nodes) + + @locking.ssynchronized(_config_lock, shared=1) + def GetInstanceNetworks(self, instance_name): + """Returns set of network UUIDs for instance's nics. + + @rtype: frozenset + + """ + instance = self._UnlockedGetInstanceInfo(instance_name) + if not instance: + raise errors.ConfigurationError("Unknown instance '%s'" % instance_name) + + networks = set() + for nic in instance.nics: + if nic.network: + networks.add(nic.network) + + return frozenset(networks) + + @locking.ssynchronized(_config_lock, shared=1) + def GetMultiInstanceInfo(self, instances): + """Get the configuration of multiple instances. + + @param instances: list of instance names + @rtype: list + @return: list of tuples (instance, instance_info), where + instance_info is what would GetInstanceInfo return for the + node, while keeping the original order + + """ + return [(name, self._UnlockedGetInstanceInfo(name)) for name in instances] + + @locking.ssynchronized(_config_lock, shared=1) def GetAllInstancesInfo(self): """Get the configuration of all instances. @@ -1252,6 +1678,22 @@ class ConfigWriter: for instance in self._UnlockedGetInstanceList()]) return my_dict + @locking.ssynchronized(_config_lock, shared=1) + def GetInstancesInfoByFilter(self, filter_fn): + """Get instance configuration with a filter. + + @type filter_fn: callable + @param filter_fn: Filter function receiving instance object as parameter, + returning boolean. Important: this function is called while the + configuration locks is held. It must not do any complex work or call + functions potentially leading to a deadlock. Ideally it doesn't call any + other functions and just compares instance attributes. + + """ + return dict((name, inst) + for (name, inst) in self._config_data.instances.items() + if filter_fn(inst)) + @locking.ssynchronized(_config_lock) def AddNode(self, node, ec_id): """Add a node to the configuration. @@ -1286,14 +1728,12 @@ class ConfigWriter: self._config_data.cluster.serial_no += 1 self._WriteConfig() - @locking.ssynchronized(_config_lock, shared=1) def ExpandNodeName(self, short_name): - """Attempt to expand an incomplete instance name. + """Attempt to expand an incomplete node name. """ - return utils.MatchNameComponent(short_name, - self._config_data.nodes.keys(), - case_sensitive=False) + # Locking is done in L{ConfigWriter.GetNodeList} + return _MatchNameComponentIgnoreCase(short_name, self.GetNodeList()) def _UnlockedGetNodeInfo(self, node_name): """Get the configuration of a node, as stored in the config. @@ -1345,6 +1785,52 @@ class ConfigWriter: sec.append(inst.name) return (pri, sec) + @locking.ssynchronized(_config_lock, shared=1) + def GetNodeGroupInstances(self, uuid, primary_only=False): + """Get the instances of a node group. + + @param uuid: Node group UUID + @param primary_only: Whether to only consider primary nodes + @rtype: frozenset + @return: List of instance names in node group + + """ + if primary_only: + nodes_fn = lambda inst: [inst.primary_node] + else: + nodes_fn = lambda inst: inst.all_nodes + + return frozenset(inst.name + for inst in self._config_data.instances.values() + for node_name in nodes_fn(inst) + if self._UnlockedGetNodeInfo(node_name).group == uuid) + + def _UnlockedGetHvparamsString(self, hvname): + """Return the string representation of the list of hyervisor parameters of + the given hypervisor. + + @see: C{GetHvparams} + + """ + result = "" + hvparams = self._config_data.cluster.hvparams[hvname] + for key in hvparams: + result += "%s=%s\n" % (key, hvparams[key]) + return result + + @locking.ssynchronized(_config_lock, shared=1) + def GetHvparamsString(self, hvname): + """Return the hypervisor parameters of the given hypervisor. + + @type hvname: string + @param hvname: name of a hypervisor + @rtype: string + @return: string containing key-value-pairs, one pair on each line; + format: KEY=VALUE + + """ + return self._UnlockedGetHvparamsString(hvname) + def _UnlockedGetNodeList(self): """Return the list of nodes which are in the configuration. @@ -1397,6 +1883,19 @@ class ConfigWriter: return [node.name for node in all_nodes if not node.vm_capable] @locking.ssynchronized(_config_lock, shared=1) + def GetMultiNodeInfo(self, nodes): + """Get the configuration of multiple nodes. + + @param nodes: list of node names + @rtype: list + @return: list of tuples of (node, node_info), where node_info is + what would GetNodeInfo return for the node, in the original + order + + """ + return [(name, self._UnlockedGetNodeInfo(name)) for name in nodes] + + @locking.ssynchronized(_config_lock, shared=1) def GetAllNodesInfo(self): """Get the configuration of all nodes. @@ -1405,9 +1904,16 @@ class ConfigWriter: would GetNodeInfo return for the node """ - my_dict = dict([(node, self._UnlockedGetNodeInfo(node)) - for node in self._UnlockedGetNodeList()]) - return my_dict + return self._UnlockedGetAllNodesInfo() + + def _UnlockedGetAllNodesInfo(self): + """Gets configuration of all nodes. + + @note: See L{GetAllNodesInfo} + + """ + return dict([(node, self._UnlockedGetNodeInfo(node)) + for node in self._UnlockedGetNodeList()]) @locking.ssynchronized(_config_lock, shared=1) def GetNodeGroupsFromNodes(self, nodes): @@ -1523,7 +2029,7 @@ class ConfigWriter: """Changes the group of a number of nodes. @type mods: list of tuples; (node name, new group UUID) - @param modes: Node membership modifications + @param mods: Node membership modifications """ groups = self._config_data.nodegroups @@ -1582,7 +2088,7 @@ class ConfigWriter: # Update timestamps and serials (only once per node/group object) now = time.time() - for obj in frozenset(itertools.chain(*resmod)): # pylint: disable-msg=W0142 + for obj in frozenset(itertools.chain(*resmod)): # pylint: disable=W0142 obj.serial_no += 1 obj.mtime = now @@ -1605,6 +2111,9 @@ class ConfigWriter: return (self._config_data.instances.values() + self._config_data.nodes.values() + self._config_data.nodegroups.values() + + self._config_data.networks.values() + + self._AllDisks() + + self._AllNICs() + [self._config_data.cluster]) def _OpenConfig(self, accept_foreign): @@ -1621,8 +2130,8 @@ class ConfigWriter: # Make sure the configuration has the right version _ValidateConfig(data) - if (not hasattr(data, 'cluster') or - not hasattr(data.cluster, 'rsahostkeypub')): + if (not hasattr(data, "cluster") or + not hasattr(data.cluster, "rsahostkeypub")): raise errors.ConfigurationError("Incomplete configuration" " (missing cluster.rsahostkeypub)") @@ -1633,24 +2142,22 @@ class ConfigWriter: (data.cluster.master_node, self._my_hostname)) raise errors.ConfigurationError(msg) - # Upgrade configuration if needed - data.UpgradeConfig() - self._config_data = data # reset the last serial as -1 so that the next write will cause # ssconf update self._last_cluster_serial = -1 - # And finally run our (custom) config upgrade sequence + # Upgrade configuration if needed self._UpgradeConfig() self._cfg_id = utils.GetFileID(path=self._cfg_file) def _UpgradeConfig(self): - """Run upgrade steps that cannot be done purely in the objects. + """Run any upgrade steps. - This is because some data elements need uniqueness across the - whole configuration, etc. + This method performs both in-object upgrades and also update some data + elements that need uniqueness across the whole configuration or interact + with other objects. @warning: this function will call L{_WriteConfig()}, but also L{DropECReservations} so it needs to be called only from a @@ -1659,31 +2166,42 @@ class ConfigWriter: created first, to avoid causing deadlock. """ - modified = False + # Keep a copy of the persistent part of _config_data to check for changes + # Serialization doesn't guarantee order in dictionaries + oldconf = copy.deepcopy(self._config_data.ToDict()) + + # In-object upgrades + self._config_data.UpgradeConfig() + for item in self._AllUUIDObjects(): if item.uuid is None: item.uuid = self._GenerateUniqueID(_UPGRADE_CONFIG_JID) - modified = True if not self._config_data.nodegroups: default_nodegroup_name = constants.INITIAL_NODE_GROUP_NAME default_nodegroup = objects.NodeGroup(name=default_nodegroup_name, members=[]) self._UnlockedAddNodeGroup(default_nodegroup, _UPGRADE_CONFIG_JID, True) - modified = True for node in self._config_data.nodes.values(): if not node.group: node.group = self.LookupNodeGroup(None) - modified = True # This is technically *not* an upgrade, but needs to be done both when # nodegroups are being added, and upon normally loading the config, # because the members list of a node group is discarded upon # serializing/deserializing the object. self._UnlockedAddNodeToGroup(node.name, node.group) + + modified = (oldconf != self._config_data.ToDict()) if modified: self._WriteConfig() # This is ok even if it acquires the internal lock, as _UpgradeConfig is # only called at config init time, without the lock held self.DropECReservations(_UPGRADE_CONFIG_JID) + else: + config_errors = self._UnlockedVerifyConfig() + if config_errors: + errmsg = ("Loaded configuration data is not consistent: %s" % + (utils.CommaJoin(config_errors))) + logging.critical(errmsg) def _DistributeConfig(self, feedback_fn): """Distribute the configuration to the other nodes. @@ -1713,8 +2231,9 @@ class ConfigWriter: node_list.append(node_info.name) addr_list.append(node_info.primary_ip) - result = rpc.RpcRunner.call_upload_file(node_list, self._cfg_file, - address_list=addr_list) + # TODO: Use dedicated resolver talking to config writer for name resolution + result = \ + self._GetRpc(addr_list).call_upload_file(node_list, self._cfg_file) for to_node, to_result in result.items(): msg = to_result.fail_msg if msg: @@ -1773,7 +2292,7 @@ class ConfigWriter: # Write ssconf files on all nodes (including locally) if self._last_cluster_serial < self._config_data.cluster.serial_no: if not self._offline: - result = rpc.RpcRunner.call_write_ssconf_files( + result = self._GetRpc(None).call_write_ssconf_files( self._UnlockedGetOnlineNodeList(), self._UnlockedGetSsconfValues()) @@ -1789,6 +2308,40 @@ class ConfigWriter: self._last_cluster_serial = self._config_data.cluster.serial_no + def _GetAllHvparamsStrings(self, hypervisors): + """Get the hvparams of all given hypervisors from the config. + + @type hypervisors: list of string + @param hypervisors: list of hypervisor names + @rtype: dict of strings + @returns: dictionary mapping the hypervisor name to a string representation + of the hypervisor's hvparams + + """ + hvparams = {} + for hv in hypervisors: + hvparams[hv] = self._UnlockedGetHvparamsString(hv) + return hvparams + + @staticmethod + def _ExtendByAllHvparamsStrings(ssconf_values, all_hvparams): + """Extends the ssconf_values dictionary by hvparams. + + @type ssconf_values: dict of strings + @param ssconf_values: dictionary mapping ssconf_keys to strings + representing the content of ssconf files + @type all_hvparams: dict of strings + @param all_hvparams: dictionary mapping hypervisor names to a string + representation of their hvparams + @rtype: same as ssconf_values + @returns: the ssconf_values dictionary extended by hvparams + + """ + for hv in all_hvparams: + ssconf_key = constants.SS_HVPARAMS_PREF + hv + ssconf_values[ssconf_key] = all_hvparams[hv] + return ssconf_values + def _UnlockedGetSsconfValues(self): """Return the values needed by ssconf. @@ -1820,21 +2373,27 @@ class ConfigWriter: cluster_tags = fn(cluster.GetTags()) hypervisor_list = fn(cluster.enabled_hypervisors) + all_hvparams = self._GetAllHvparamsStrings(constants.HYPER_TYPES) uid_pool = uidpool.FormatUidPool(cluster.uid_pool, separator="\n") nodegroups = ["%s %s" % (nodegroup.uuid, nodegroup.name) for nodegroup in self._config_data.nodegroups.values()] nodegroups_data = fn(utils.NiceSort(nodegroups)) + networks = ["%s %s" % (net.uuid, net.name) for net in + self._config_data.networks.values()] + networks_data = fn(utils.NiceSort(networks)) - return { + ssconf_values = { constants.SS_CLUSTER_NAME: cluster.cluster_name, constants.SS_CLUSTER_TAGS: cluster_tags, constants.SS_FILE_STORAGE_DIR: cluster.file_storage_dir, + constants.SS_SHARED_FILE_STORAGE_DIR: cluster.shared_file_storage_dir, constants.SS_MASTER_CANDIDATES: mc_data, constants.SS_MASTER_CANDIDATES_IPS: mc_ips_data, constants.SS_MASTER_IP: cluster.master_ip, constants.SS_MASTER_NETDEV: cluster.master_netdev, + constants.SS_MASTER_NETMASK: str(cluster.master_netmask), constants.SS_MASTER_NODE: cluster.master_node, constants.SS_NODE_LIST: node_data, constants.SS_NODE_PRIMARY_IPS: node_pri_ips_data, @@ -1848,7 +2407,17 @@ class ConfigWriter: constants.SS_MAINTAIN_NODE_HEALTH: str(cluster.maintain_node_health), constants.SS_UID_POOL: uid_pool, constants.SS_NODEGROUPS: nodegroups_data, + constants.SS_NETWORKS: networks_data, } + ssconf_values = self._ExtendByAllHvparamsStrings(ssconf_values, + all_hvparams) + bad_values = [(k, v) for k, v in ssconf_values.items() + if not isinstance(v, (str, basestring))] + if bad_values: + err = utils.CommaJoin("%s=%s" % (k, v) for k, v in bad_values) + raise errors.ConfigurationError("Some ssconf key(s) have non-string" + " values: %s" % err) + return ssconf_values @locking.ssynchronized(_config_lock, shared=1) def GetSsconfValues(self): @@ -1914,7 +2483,7 @@ class ConfigWriter: return self._config_data.HasAnyDiskOfType(dev_type) @locking.ssynchronized(_config_lock) - def Update(self, target, feedback_fn): + def Update(self, target, feedback_fn, ec_id=None): """Notify function to be called after updates. This function must be called when an object (as returned by @@ -1942,6 +2511,8 @@ class ConfigWriter: test = target in self._config_data.instances.values() elif isinstance(target, objects.NodeGroup): test = target in self._config_data.nodegroups.values() + elif isinstance(target, objects.Network): + test = target in self._config_data.networks.values() else: raise errors.ProgrammerError("Invalid object type (%s) passed to" " ConfigWriter.Update" % type(target)) @@ -1959,6 +2530,10 @@ class ConfigWriter: if isinstance(target, objects.Instance): self._UnlockedReleaseDRBDMinors(target.name) + if ec_id is not None: + # Commit all ips reserved by OpInstanceSetParams and OpGroupSetParams + self._UnlockedCommitTemporaryIps(ec_id) + self._WriteConfig(feedback_fn=feedback_fn) @locking.ssynchronized(_config_lock) @@ -1968,3 +2543,192 @@ class ConfigWriter: """ for rm in self._all_rms: rm.DropECReservations(ec_id) + + @locking.ssynchronized(_config_lock, shared=1) + def GetAllNetworksInfo(self): + """Get configuration info of all the networks. + + """ + return dict(self._config_data.networks) + + def _UnlockedGetNetworkList(self): + """Get the list of networks. + + This function is for internal use, when the config lock is already held. + + """ + return self._config_data.networks.keys() + + @locking.ssynchronized(_config_lock, shared=1) + def GetNetworkList(self): + """Get the list of networks. + + @return: array of networks, ex. ["main", "vlan100", "200] + + """ + return self._UnlockedGetNetworkList() + + @locking.ssynchronized(_config_lock, shared=1) + def GetNetworkNames(self): + """Get a list of network names + + """ + names = [net.name + for net in self._config_data.networks.values()] + return names + + def _UnlockedGetNetwork(self, uuid): + """Returns information about a network. + + This function is for internal use, when the config lock is already held. + + """ + if uuid not in self._config_data.networks: + return None + + return self._config_data.networks[uuid] + + @locking.ssynchronized(_config_lock, shared=1) + def GetNetwork(self, uuid): + """Returns information about a network. + + It takes the information from the configuration file. + + @param uuid: UUID of the network + + @rtype: L{objects.Network} + @return: the network object + + """ + return self._UnlockedGetNetwork(uuid) + + @locking.ssynchronized(_config_lock) + def AddNetwork(self, net, ec_id, check_uuid=True): + """Add a network to the configuration. + + @type net: L{objects.Network} + @param net: the Network object to add + @type ec_id: string + @param ec_id: unique id for the job to use when creating a missing UUID + + """ + self._UnlockedAddNetwork(net, ec_id, check_uuid) + self._WriteConfig() + + def _UnlockedAddNetwork(self, net, ec_id, check_uuid): + """Add a network to the configuration. + + """ + logging.info("Adding network %s to configuration", net.name) + + if check_uuid: + self._EnsureUUID(net, ec_id) + + net.serial_no = 1 + self._config_data.networks[net.uuid] = net + self._config_data.cluster.serial_no += 1 + + def _UnlockedLookupNetwork(self, target): + """Lookup a network's UUID. + + @type target: string + @param target: network name or UUID + @rtype: string + @return: network UUID + @raises errors.OpPrereqError: when the target network cannot be found + + """ + if target is None: + return None + if target in self._config_data.networks: + return target + for net in self._config_data.networks.values(): + if net.name == target: + return net.uuid + raise errors.OpPrereqError("Network '%s' not found" % target, + errors.ECODE_NOENT) + + @locking.ssynchronized(_config_lock, shared=1) + def LookupNetwork(self, target): + """Lookup a network's UUID. + + This function is just a wrapper over L{_UnlockedLookupNetwork}. + + @type target: string + @param target: network name or UUID + @rtype: string + @return: network UUID + + """ + return self._UnlockedLookupNetwork(target) + + @locking.ssynchronized(_config_lock) + def RemoveNetwork(self, network_uuid): + """Remove a network from the configuration. + + @type network_uuid: string + @param network_uuid: the UUID of the network to remove + + """ + logging.info("Removing network %s from configuration", network_uuid) + + if network_uuid not in self._config_data.networks: + raise errors.ConfigurationError("Unknown network '%s'" % network_uuid) + + del self._config_data.networks[network_uuid] + self._config_data.cluster.serial_no += 1 + self._WriteConfig() + + def _UnlockedGetGroupNetParams(self, net_uuid, node): + """Get the netparams (mode, link) of a network. + + Get a network's netparams for a given node. + + @type net_uuid: string + @param net_uuid: network uuid + @type node: string + @param node: node name + @rtype: dict or None + @return: netparams + + """ + node_info = self._UnlockedGetNodeInfo(node) + nodegroup_info = self._UnlockedGetNodeGroup(node_info.group) + netparams = nodegroup_info.networks.get(net_uuid, None) + + return netparams + + @locking.ssynchronized(_config_lock, shared=1) + def GetGroupNetParams(self, net_uuid, node): + """Locking wrapper of _UnlockedGetGroupNetParams() + + """ + return self._UnlockedGetGroupNetParams(net_uuid, node) + + @locking.ssynchronized(_config_lock, shared=1) + def CheckIPInNodeGroup(self, ip, node): + """Check IP uniqueness in nodegroup. + + Check networks that are connected in the node's node group + if ip is contained in any of them. Used when creating/adding + a NIC to ensure uniqueness among nodegroups. + + @type ip: string + @param ip: ip address + @type node: string + @param node: node name + @rtype: (string, dict) or (None, None) + @return: (network name, netparams) + + """ + if ip is None: + return (None, None) + node_info = self._UnlockedGetNodeInfo(node) + nodegroup_info = self._UnlockedGetNodeGroup(node_info.group) + for net_uuid in nodegroup_info.networks.keys(): + net_info = self._UnlockedGetNetwork(net_uuid) + pool = network.AddressPool(net_info) + if pool.Contains(ip): + return (net_info.name, nodegroup_info.networks[net_uuid]) + + return (None, None)