# Copyright 2014 DreamHost, LLC # # Author: DreamHost, LLC # # 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 os import re from astara_router import models from astara_router import settings from astara_router.drivers import (bird, dnsmasq, ip, metadata, iptables, arp, hostname, loadbalancer) class ServiceManagerBase(object): def __init__(self, state_path='.'): self._config = None self.state_path = os.path.abspath(state_path) self._vrrp_ip_mgr = None self._reload_callbacks = [] @property def ip_mgr(self): ip_mgr = ip.IPManager() ip_mgr.ensure_mapping() if not self._config: # we do not yet have config, so use standard ip manager for # ensuring initial intrefaces return ip_mgr if self._config and self._config.ha: if not self._vrrp_ip_mgr: self._vrrp_ip_mgr = ip.VRRPIPManager() self._reload_callbacks.append(self._vrrp_ip_mgr.reload) # peers and prio can change and be updated via config, need to # ensure the vrrp manager is up to date every access. self._vrrp_ip_mgr.set_peers( self._config.ha_config.get('peers', [])) self._vrrp_ip_mgr.set_priority( self._config.ha_config.get('priority', 0)) return self._vrrp_ip_mgr else: # we may not yet have config, so use standard ip manager for # ensuring initial interfaces return ip_mgr @property def config(self): """Make config a read-only property. To update the value, update_config() must called to change the global state of appliance. """ return self._config def update_config(self, config, cache): pass def update_interfaces(self): if self._config is None: return for network in self._config.networks: self.ip_mgr.disable_duplicate_address_detection(network) self.ip_mgr.update_interfaces(self._config.interfaces) def reload_config(self): """Calls any post-config reload callbacks to reload services Required for things like keepalived, which gets its config built by multiple drivers, in order to avoid unncessary restarts. """ [cb() for cb in self._reload_callbacks] class SystemManager(ServiceManagerBase): def __init__(self, state_path='.'): super(SystemManager, self).__init__(state_path) self._config = models.SystemConfiguration() def update_config(self, config, cache): self._config = config self.update_hostname() self.update_interfaces() def update_hostname(self): mgr = hostname.HostnameManager() mgr.update(self._config) class RouterManager(ServiceManagerBase): def update_config(self, config, cache): self._config = config self.update_interfaces() self.update_dhcp() self.update_metadata() self.update_bgp_and_radv() self.update_firewall() self.update_routes(cache) self.update_arp() self.reload_config() def update_dhcp(self): mgr = dnsmasq.DHCPManager() mgr.delete_all_config() for network in self._config.networks: real_ifname = self.ip_mgr.generic_to_host(network.interface.ifname) mgr.update_network_dhcp_config(real_ifname, network) mgr.restart() def update_metadata(self): mgr = metadata.MetadataManager() should_restart = mgr.should_restart(self._config) mgr.save_config(self._config) if should_restart: mgr.restart() else: mgr.ensure_started() def update_bgp_and_radv(self): mgr = bird.BirdManager() mgr.save_config(self._config, self.ip_mgr.generic_mapping) mgr.restart() def update_firewall(self): mgr = iptables.IPTablesManager() mgr.save_config(self._config, self.ip_mgr.generic_mapping) mgr.restart() def update_routes(self, cache): self.ip_mgr.update_default_gateway(self._config) self.ip_mgr.update_host_routes(self._config, cache) def update_arp(self): mgr = arp.ARPManager() mgr.send_gratuitous_arp_for_floating_ips( self._config, self.ip_mgr.generic_to_host ) mgr.remove_stale_entries(self._config) def get_interfaces(self): return self.ip_mgr.get_interfaces() def get_interface(self, ifname): return self.ip_mgr.get_interface(ifname) def _map_virtual_to_real_interfaces(self, virt_data): rules = [] rules.extend( '%s = "%s"' % i for i in self.ip_mgr.generic_mapping.items() ) rules.append(re.sub('([\s!])(ge\d+([\s:]|$))', r'\1$\2', virt_data)) return '\n'.join(rules) def get_config_or_default(self): # This is a hack to provide compatability with the original API, see # Manager.config() if not self._config: return models.RouterConfiguration() else: return self._config class LoadBalancerManager(ServiceManagerBase): def __init__(self, state_path='.'): super(LoadBalancerManager, self).__init__(state_path) self.lb_manager = loadbalancer.get_loadbalancer_driver( # xxx pull from cfg loadbalancer.CONFIGURED_LB_DRIVER)() def update_config(self, config, cache): self._config = config self.lb_manager.update_config(self.config) SERVICE_MANAGER_MAP = { 'router': RouterManager, 'loadbalancer': LoadBalancerManager, } class Manager(object): def __init__(self, state_path='.'): self.state_path = os.path.abspath(state_path) self.ip_mgr = ip.IPManager() self.ip_mgr.ensure_mapping() # Holds the common system config self._system_config = models.SystemConfiguration() # Holds config models for various services (router, loadbalancer) self._service_configs = [] self._service_managers = { 'system': SystemManager() } self._load_managers() def _load_managers(self): for svc in settings.ENABLED_SERVICES: manager = SERVICE_MANAGER_MAP.get(svc) if manager: self._service_managers[svc] = manager() def get_manager(self, service): try: return self._service_managers[service] except: raise Exception('No such service manager loaded for appliance ' 'service %s' % service) def management_address(self, ensure_configuration=False): return self.ip_mgr.get_management_address(ensure_configuration) @property def router(self): """Returns the router manager. This is mostly to keep compat with the existing API. """ return self.get_manager('router') @property def system_config(self): """Make config a read-only property. To update the value, update_config() must called to change the global state of appliance. """ return self._system_config @property def service_configs(self): """Make config a read-only property. To update the value, update_config() must called to change the global state of router. """ return self._service_configs def update_config(self, system_config, service_configs, cache): self._system_config = system_config self._service_configs = service_configs # first update the system config manager = self.get_manager(self.system_config.service_name) manager.update_config(self.system_config, cache) for svc_cfg in self.service_configs: manager = self.get_manager(svc_cfg.service_name) manager.update_config(svc_cfg, cache) @property def config(self): out = {} if 'router' in self._service_managers: # The original appliance API provides router config # in the root 'configuration' key. We want to move that # to the 'services' bucket but provide compat to those who might # still be expecting it in the root. This seeds the root with the # default empty values if no router is associated with the # appliance and allows for # ['configuration']['services']['router'] to be None at the same # time. router_cfg = self.router.get_config_or_default().to_dict() out = router_cfg else: out = {} out['services'] = {} for svc in SERVICE_MANAGER_MAP: try: manager = self.get_manager(svc) except: continue out['services'][svc] = manager.config out['system'] = self.system_config return out class ManagerProxy(object): def __init__(self): self.instance = None def __getattr__(self, name): if not self.instance: self.instance = Manager() return getattr(self.instance, name) manager = ManagerProxy()