# Copyright (C) 2011-2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # # See the License for the specific language governing permissions and # limitations under the License. import functools import ipaddress import math import os import time import yaml from nodepool.driver import ConfigValue from nodepool.driver import Drivers try: from yaml import CSafeLoader as SafeLoader except ImportError: from yaml import SafeLoader class ZooKeeperConnectionConfig(object): ''' Represents the connection parameters for a ZooKeeper server. ''' def __eq__(self, other): if isinstance(other, ZooKeeperConnectionConfig): if other.__dict__ == self.__dict__: return True return False def __init__(self, host, port=2181, chroot=None): '''Initialize the ZooKeeperConnectionConfig object. :param str host: The hostname of the ZooKeeper server. :param int port: The port on which ZooKeeper is listening. Optional, default: 2181. :param str chroot: A chroot for this connection. All ZooKeeper nodes will be underneath this root path. Optional, default: None. (one per server) defining the ZooKeeper cluster servers. Only the 'host' attribute is required.'. ''' self.host = host self.port = port self.chroot = chroot or '' def __repr__(self): return "host=%s port=%s chroot=%s" % \ (self.host, self.port, self.chroot) def buildZooKeeperHosts(host_list): ''' Build the ZK cluster host list for client connections. :param list host_list: A list of :py:class:`~nodepool.zk.ZooKeeperConnectionConfig` objects (one per server) defining the ZooKeeper cluster servers. ''' if not isinstance(host_list, list): raise Exception("'host_list' must be a list") hosts = [] for host_def in host_list: h = host_def.host # If this looks like a ipv6 literal address, make sure it's # quoted in []'s try: addr = ipaddress.ip_address(host_def.host) if addr.version == 6: h = '[%s]' % addr except ValueError: pass host = '%s:%s%s' % (h, host_def.port, host_def.chroot) hosts.append(host) return ",".join(hosts) class Config(ConfigValue): ''' Class representing the nodepool configuration. This class implements methods to read each of the top-level configuration items found in the YAML config file, and set attributes accordingly. ''' def __init__(self): self.diskimages = {} self.labels = {} self.providers = {} self.provider_managers = {} self.zookeeper_servers = {} self.zookeeper_timeout = 10.0 self.zookeeper_tls_cert = None self.zookeeper_tls_key = None self.zookeeper_tls_ca = None self.elements_dir = None self.images_dir = None self.build_log_dir = None self.build_log_retention = None self.max_hold_age = None self.webapp = None self.tenant_resource_limits = {} # Last modified timestamps of loaded config files self.config_mtimes = {} def __eq__(self, other): if isinstance(other, Config): return (self.diskimages == other.diskimages and self.labels == other.labels and self.providers == other.providers and self.provider_managers == other.provider_managers and self.zookeeper_servers == other.zookeeper_servers and self.zookeeper_timeout == other.zookeeper_timeout and self.elements_dir == other.elements_dir and self.images_dir == other.images_dir and self.build_log_dir == other.build_log_dir and self.build_log_retention == other.build_log_retention and self.max_hold_age == other.max_hold_age and self.webapp == other.webapp and self.tenant_resource_limits == other.tenant_resource_limits ) return False def setConfigPathMtime(self, path, mtime): self.config_mtimes[path] = mtime def setElementsDir(self, value): self.elements_dir = value def setImagesDir(self, value): self.images_dir = value def setBuildLog(self, directory, retention): if retention is None: retention = 7 self.build_log_dir = directory self.build_log_retention = retention def setMaxHoldAge(self, value): if value is None or value <= 0: value = math.inf self.max_hold_age = value def setWebApp(self, webapp_cfg): if webapp_cfg is None: webapp_cfg = {} self.webapp = { 'port': webapp_cfg.get('port', 8005), 'listen_address': webapp_cfg.get('listen_address', '0.0.0.0') } def setZooKeeperTLS(self, zk_tls): if not zk_tls: return self.zookeeper_tls_cert = zk_tls.get('cert') self.zookeeper_tls_key = zk_tls.get('key') self.zookeeper_tls_ca = zk_tls.get('ca') def setZooKeeperServers(self, zk_cfg): if not zk_cfg: return hosts = [] for server in zk_cfg: z = ZooKeeperConnectionConfig(server['host'], server.get('port', 2281), server.get('chroot', None)) hosts.append(z) self.zookeeper_servers = buildZooKeeperHosts(hosts) def setZooKeeperTimeout(self, timeout): self.zookeeper_timeout = float(timeout) def setDiskImages(self, diskimages_cfg): if not diskimages_cfg: return all_diskimages = {} non_abstract_diskimages = [] # create a dict and split out the abstract images which don't # become final images, but can still be referenced as parent: for diskimage in diskimages_cfg: name = diskimage['name'] all_diskimages[name] = diskimage if not diskimage.get('abstract', False): non_abstract_diskimages.append(diskimage) def _merge_image_cfg(diskimage, parent): parent_cfg = all_diskimages[parent] if parent_cfg.get('parent', None): _merge_image_cfg(diskimage, parent_cfg['parent']) diskimage.setFromConfig(parent_cfg) for cfg in non_abstract_diskimages: d = DiskImage(cfg['name']) # Walk the parents, if any, and set their values if cfg.get('parent', None): _merge_image_cfg(d, cfg.get('parent')) # Now set our config, which overrides any values from # parents. d.setFromConfig(cfg) # must be a string, as it's passed as env-var to # d-i-b, but might be untyped in the yaml and # interpreted as a number (e.g. "21" for fedora) d.release = str(d.release) # This is expected as a space-separated string d.elements = u' '.join(d.elements) self.diskimages[d.name] = d def setSecureDiskimageEnv(self, diskimages, secure_config_path): for diskimage in diskimages: if diskimage['name'] not in self.diskimages: raise Exception('%s: unknown diskimage %s' % (secure_config_path, diskimage['name'])) self.diskimages[diskimage['name']].env_vars.update( diskimage['env-vars']) def setLabels(self, labels_cfg): if not labels_cfg: return for label in labels_cfg: l = Label() l.name = label['name'] l.max_ready_age = label.get('max-ready-age', 0) l.min_ready = label.get('min-ready', 0) l.pools = [] self.labels[l.name] = l def setProviders(self, providers_cfg): if not providers_cfg: return for provider in providers_cfg: p = get_provider_config(provider) p.load(self) self.providers[p.name] = p def setTenantResourceLimits(self, tenant_resource_limits_cfg): if not tenant_resource_limits_cfg: return for resource_limit in tenant_resource_limits_cfg: resource_limit = resource_limit.copy() tenant_name = resource_limit.pop('tenant-name') limits = {} limits['cores'] = resource_limit.pop('max-cores', math.inf) limits['instances'] = resource_limit.pop('max-servers', math.inf) limits['ram'] = resource_limit.pop('max-ram', math.inf) if 'max-volumes' in resource_limit: limits['volumes'] = resource_limit.pop('max-volumes', math.inf) if 'max-volume-gb' in resource_limit: limits['volume-gb'] = resource_limit.pop( 'max-volume-gb', math.inf) for k, v in resource_limit.items(): limits[k] = v self.tenant_resource_limits[tenant_name] = limits class Label(ConfigValue): def __init__(self): self.name = None self.max_ready_age = None self.min_ready = None self.pools = None def __eq__(self, other): if isinstance(other, Label): return (self.name == other.name and self.max_ready_age == other.max_ready_age and self.min_ready == other.min_ready and self.pools == other.pools) return False def __repr__(self): return "