X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/3ecf6786a731c0a467f0efcbf6346914e3824ec8..fac489a56410b927f19f81ef72755cab9f2b9d29:/lib/ssconf.py diff --git a/lib/ssconf.py b/lib/ssconf.py index de9cdbc..11af2a2 100644 --- a/lib/ssconf.py +++ b/lib/ssconf.py @@ -1,7 +1,7 @@ -#!/usr/bin/python +# # -# Copyright (C) 2006, 2007 Google Inc. +# Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012 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 @@ -26,167 +26,442 @@ configuration data, which is mostly static and available to all nodes. """ -import os -import tempfile +import sys import errno -import socket +import logging +from ganeti import compat from ganeti import errors from ganeti import constants +from ganeti import utils +from ganeti import netutils +from ganeti import pathutils + + +SSCONF_LOCK_TIMEOUT = 10 + +#: Valid ssconf keys +_VALID_KEYS = compat.UniqueFrozenset([ + constants.SS_CLUSTER_NAME, + constants.SS_CLUSTER_TAGS, + constants.SS_FILE_STORAGE_DIR, + constants.SS_SHARED_FILE_STORAGE_DIR, + constants.SS_MASTER_CANDIDATES, + constants.SS_MASTER_CANDIDATES_IPS, + constants.SS_MASTER_IP, + constants.SS_MASTER_NETDEV, + constants.SS_MASTER_NETMASK, + constants.SS_MASTER_NODE, + constants.SS_NODE_LIST, + constants.SS_NODE_PRIMARY_IPS, + constants.SS_NODE_SECONDARY_IPS, + constants.SS_OFFLINE_NODES, + constants.SS_ONLINE_NODES, + constants.SS_PRIMARY_IP_FAMILY, + constants.SS_INSTANCE_LIST, + constants.SS_RELEASE_VERSION, + constants.SS_HYPERVISOR_LIST, + constants.SS_MAINTAIN_NODE_HEALTH, + constants.SS_UID_POOL, + constants.SS_NODEGROUPS, + constants.SS_NETWORKS, + constants.SS_HVPARAMS_XEN_PVM, + constants.SS_HVPARAMS_XEN_FAKE, + constants.SS_HVPARAMS_XEN_HVM, + constants.SS_HVPARAMS_XEN_KVM, + constants.SS_HVPARAMS_XEN_CHROOT, + constants.SS_HVPARAMS_XEN_LXC, + ]) + +#: Maximum size for ssconf files +_MAX_SIZE = 128 * 1024 + + +def ReadSsconfFile(filename): + """Reads an ssconf file and verifies its size. + + @type filename: string + @param filename: Path to file + @rtype: string + @return: File contents without newlines at the end + @raise RuntimeError: When the file size exceeds L{_MAX_SIZE} + + """ + statcb = utils.FileStatHelper() + + data = utils.ReadFile(filename, size=_MAX_SIZE, preread=statcb) + if statcb.st.st_size > _MAX_SIZE: + msg = ("File '%s' has a size of %s bytes (up to %s allowed)" % + (filename, statcb.st.st_size, _MAX_SIZE)) + raise RuntimeError(msg) -class SimpleStore: + return data.rstrip("\n") + + +class SimpleStore(object): """Interface to static cluster data. - This is different that the config.ConfigWriter class in that it - holds data that is (mostly) constant after the cluster - initialization. Its purpose is to allow limited customization of - things which would otherwise normally live in constants.py. Note - that this data cannot live in ConfigWriter as that is available only - on the master node, and our data must be readable by both the master - and the nodes. + This is different that the config.ConfigWriter and + SimpleConfigReader classes in that it holds data that will always be + present, even on nodes which don't have all the cluster data. Other particularities of the datastore: - keys are restricted to predefined values - - values are small (<4k) - - since the data is practically static, read keys are cached in memory - - some keys are handled specially (read from the system, so - we can't update them) """ - _SS_FILEPREFIX = "ssconf_" - SS_HYPERVISOR = "hypervisor" - SS_NODED_PASS = "node_pass" - SS_MASTER_NODE = "master_node" - SS_MASTER_IP = "master_ip" - SS_MASTER_NETDEV = "master_netdev" - SS_CLUSTER_NAME = "cluster_name" - _VALID_KEYS = (SS_HYPERVISOR, SS_NODED_PASS, SS_MASTER_NODE, SS_MASTER_IP, - SS_MASTER_NETDEV, SS_CLUSTER_NAME) - _MAX_SIZE = 4096 - - def __init__(self, cfg_location=None): + def __init__(self, cfg_location=None, _lockfile=pathutils.SSCONF_LOCK_FILE): if cfg_location is None: - self._cfg_dir = constants.DATA_DIR + self._cfg_dir = pathutils.DATA_DIR else: self._cfg_dir = cfg_location - self._cache = {} + + self._lockfile = _lockfile def KeyToFilename(self, key): """Convert a given key into filename. """ - if key not in self._VALID_KEYS: + if key not in _VALID_KEYS: raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'" % str(key)) - filename = self._cfg_dir + '/' + self._SS_FILEPREFIX + key + filename = self._cfg_dir + "/" + constants.SSCONF_FILEPREFIX + key return filename - def _ReadFile(self, key): + def _ReadFile(self, key, default=None): """Generic routine to read keys. This will read the file which holds the value requested. Errors will be changed into ConfigurationErrors. """ - if key in self._cache: - return self._cache[key] filename = self.KeyToFilename(key) try: - fh = file(filename, 'r') - try: - data = fh.readline(self._MAX_SIZE) - data = data.rstrip('\n') - finally: - fh.close() + return ReadSsconfFile(filename) except EnvironmentError, err: - raise errors.ConfigurationError("Can't read from the ssconf file:" - " '%s'" % str(err)) - self._cache[key] = data - return data + if err.errno == errno.ENOENT and default is not None: + return default + raise errors.ConfigurationError("Can't read ssconf file %s: %s" % + (filename, str(err))) + + def ReadAll(self): + """Reads all keys and returns their values. + + @rtype: dict + @return: Dictionary, ssconf key as key, value as value + + """ + result = [] + + for key in _VALID_KEYS: + try: + value = self._ReadFile(key) + except errors.ConfigurationError: + # Ignore non-existing files + pass + else: + result.append((key, value)) + + return dict(result) - def GetNodeDaemonPort(self): - """Get the node daemon port for this cluster. + def WriteFiles(self, values, dry_run=False): + """Writes ssconf files used by external scripts. - Note that this routine does not read a ganeti-specific file, but - instead uses socket.getservbyname to allow pre-customization of - this parameter outside of ganeti. + @type values: dict + @param values: Dictionary of (name, value) + @type dry_run boolean + @param dry_run: Whether to perform a dry run """ + ssconf_lock = utils.FileLock.Open(self._lockfile) + + # Get lock while writing files + ssconf_lock.Exclusive(blocking=True, timeout=SSCONF_LOCK_TIMEOUT) try: - port = socket.getservbyname("ganeti-noded", "tcp") - except socket.error: - port = constants.DEFAULT_NODED_PORT + for name, value in values.iteritems(): + if value and not value.endswith("\n"): + value += "\n" + + if len(value) > _MAX_SIZE: + msg = ("Value '%s' has a length of %s bytes, but only up to %s are" + " allowed" % (name, len(value), _MAX_SIZE)) + raise errors.ConfigurationError(msg) + + utils.WriteFile(self.KeyToFilename(name), data=value, + mode=constants.SS_FILE_PERMS, + dry_run=dry_run) + finally: + ssconf_lock.Unlock() + + def GetFileList(self): + """Return the list of all config files. + + This is used for computing node replication data. - return port + """ + return [self.KeyToFilename(key) for key in _VALID_KEYS] - def GetHypervisorType(self): - """Get the hypervisor type for this cluster. + def GetClusterName(self): + """Get the cluster name. """ - return self._ReadFile(self.SS_HYPERVISOR) + return self._ReadFile(constants.SS_CLUSTER_NAME) - def GetNodeDaemonPassword(self): - """Get the node password for this cluster. + def GetFileStorageDir(self): + """Get the file storage dir. """ - return self._ReadFile(self.SS_NODED_PASS) + return self._ReadFile(constants.SS_FILE_STORAGE_DIR) - def GetMasterNode(self): - """Get the hostname of the master node for this cluster. + def GetSharedFileStorageDir(self): + """Get the shared file storage dir. """ - return self._ReadFile(self.SS_MASTER_NODE) + return self._ReadFile(constants.SS_SHARED_FILE_STORAGE_DIR) + + def GetMasterCandidates(self): + """Return the list of master candidates. + + """ + data = self._ReadFile(constants.SS_MASTER_CANDIDATES) + nl = data.splitlines(False) + return nl + + def GetMasterCandidatesIPList(self): + """Return the list of master candidates' primary IP. + + """ + data = self._ReadFile(constants.SS_MASTER_CANDIDATES_IPS) + nl = data.splitlines(False) + return nl def GetMasterIP(self): """Get the IP of the master node for this cluster. """ - return self._ReadFile(self.SS_MASTER_IP) + return self._ReadFile(constants.SS_MASTER_IP) def GetMasterNetdev(self): """Get the netdev to which we'll add the master ip. """ - return self._ReadFile(self.SS_MASTER_NETDEV) + return self._ReadFile(constants.SS_MASTER_NETDEV) - def GetClusterName(self): - """Get the cluster name. + def GetMasterNetmask(self): + """Get the master netmask. """ - return self._ReadFile(self.SS_CLUSTER_NAME) + try: + return self._ReadFile(constants.SS_MASTER_NETMASK) + except errors.ConfigurationError: + family = self.GetPrimaryIPFamily() + ipcls = netutils.IPAddress.GetClassFromIpFamily(family) + return ipcls.iplen - def SetKey(self, key, value): - """Set the value of a key. + def GetMasterNode(self): + """Get the hostname of the master node for this cluster. - This should be used only when adding a node to a cluster. + """ + return self._ReadFile(constants.SS_MASTER_NODE) + + def GetNodeList(self): + """Return the list of cluster nodes. """ - file_name = self.KeyToFilename(key) - dir_name, small_name = os.path.split(file_name) - fd, new_name = tempfile.mkstemp('.new', small_name, dir_name) - # here we need to make sure we remove the temp file, if any error - # leaves it in place - try: - os.chown(new_name, 0, 0) - os.chmod(new_name, 0400) - os.write(fd, "%s\n" % str(value)) - os.fsync(fd) - os.rename(new_name, file_name) - self._cache[key] = value - finally: - os.close(fd) - try: - os.unlink(new_name) - except OSError, err: - if err.errno != errno.ENOENT: - raise + data = self._ReadFile(constants.SS_NODE_LIST) + nl = data.splitlines(False) + return nl - def GetFileList(self): - """Return the lis of all config files. + def GetNodePrimaryIPList(self): + """Return the list of cluster nodes' primary IP. - This is used for computing node replication data. + """ + data = self._ReadFile(constants.SS_NODE_PRIMARY_IPS) + nl = data.splitlines(False) + return nl + + def GetNodeSecondaryIPList(self): + """Return the list of cluster nodes' secondary IP. + + """ + data = self._ReadFile(constants.SS_NODE_SECONDARY_IPS) + nl = data.splitlines(False) + return nl + + def GetNodegroupList(self): + """Return the list of nodegroups. + + """ + data = self._ReadFile(constants.SS_NODEGROUPS) + nl = data.splitlines(False) + return nl + + def GetNetworkList(self): + """Return the list of networks. + + """ + data = self._ReadFile(constants.SS_NETWORKS) + nl = data.splitlines(False) + return nl + + def GetClusterTags(self): + """Return the cluster tags. + + """ + data = self._ReadFile(constants.SS_CLUSTER_TAGS) + nl = data.splitlines(False) + return nl + + def GetHypervisorList(self): + """Return the list of enabled hypervisors. + + """ + data = self._ReadFile(constants.SS_HYPERVISOR_LIST) + nl = data.splitlines(False) + return nl + + def GetHvparamsForHypervisor(self, hvname): + """Return the hypervisor parameters of the given hypervisor. + + @type hvname: string + @param hvname: name of the hypervisor, must be in C{constants.HYPER_TYPES} + @rtype: dict of strings + @returns: dictionary with hypervisor parameters + + """ + data = self._ReadFile(constants.SS_HVPARAMS_PREF + hvname) + lines = data.splitlines(False) + hvparams = {} + for line in lines: + (key, value) = line.split("=") + hvparams[key] = value + return hvparams + + def GetHvparams(self): + """Return the hypervisor parameters of all hypervisors. + + @rtype: dict of dict of strings + @returns: dictionary mapping hypervisor names to hvparams + + """ + all_hvparams = {} + for hv in constants.HYPER_TYPES: + all_hvparams[hv] = self.GetHvparamsForHypervisor(hv) + return all_hvparams + + def GetMaintainNodeHealth(self): + """Return the value of the maintain_node_health option. + + """ + data = self._ReadFile(constants.SS_MAINTAIN_NODE_HEALTH) + # we rely on the bool serialization here + return data == "True" + + def GetUidPool(self): + """Return the user-id pool definition string. + + The separator character is a newline. + + The return value can be parsed using uidpool.ParseUidPool():: + + ss = ssconf.SimpleStore() + uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\\n") """ - return [self.KeyToFilename(key) for key in self._VALID_KEYS] + data = self._ReadFile(constants.SS_UID_POOL) + return data + + def GetPrimaryIPFamily(self): + """Return the cluster-wide primary address family. + + """ + try: + return int(self._ReadFile(constants.SS_PRIMARY_IP_FAMILY, + default=netutils.IP4Address.family)) + except (ValueError, TypeError), err: + raise errors.ConfigurationError("Error while trying to parse primary IP" + " family: %s" % err) + + +def WriteSsconfFiles(values, dry_run=False): + """Update all ssconf files. + + Wrapper around L{SimpleStore.WriteFiles}. + + """ + SimpleStore().WriteFiles(values, dry_run=dry_run) + + +def GetMasterAndMyself(ss=None): + """Get the master node and my own hostname. + + This can be either used for a 'soft' check (compared to CheckMaster, + which exits) or just for computing both at the same time. + + The function does not handle any errors, these should be handled in + the caller (errors.ConfigurationError, errors.ResolverError). + + @param ss: either a sstore.SimpleConfigReader or a + sstore.SimpleStore instance + @rtype: tuple + @return: a tuple (master node name, my own name) + + """ + if ss is None: + ss = SimpleStore() + return ss.GetMasterNode(), netutils.Hostname.GetSysName() + + +def CheckMaster(debug, ss=None): + """Checks the node setup. + + If this is the master, the function will return. Otherwise it will + exit with an exit code based on the node status. + + """ + try: + master_name, myself = GetMasterAndMyself(ss) + except errors.ConfigurationError, err: + print "Cluster configuration incomplete: '%s'" % str(err) + sys.exit(constants.EXIT_NODESETUP_ERROR) + except errors.ResolverError, err: + sys.stderr.write("Cannot resolve my own name (%s)\n" % err.args[0]) + sys.exit(constants.EXIT_NODESETUP_ERROR) + + if myself != master_name: + if debug: + sys.stderr.write("Not master, exiting.\n") + sys.exit(constants.EXIT_NOTMASTER) + + +def VerifyClusterName(name, _cfg_location=None): + """Verifies cluster name against a local cluster name. + + @type name: string + @param name: Cluster name + + """ + sstore = SimpleStore(cfg_location=_cfg_location) + + try: + local_name = sstore.GetClusterName() + except errors.ConfigurationError, err: + logging.debug("Can't get local cluster name: %s", err) + else: + if name != local_name: + raise errors.GenericError("Current cluster name is '%s'" % local_name) + + +def VerifyKeys(keys): + """Raises an exception if unknown ssconf keys are given. + + @type keys: sequence + @param keys: Key names to verify + @raise errors.GenericError: When invalid keys were found + + """ + invalid = frozenset(keys) - _VALID_KEYS + if invalid: + raise errors.GenericError("Invalid ssconf keys: %s" % + utils.CommaJoin(sorted(invalid)))