# Copyright 2017 Red Hat, Inc. All rights reserved. # # 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 errno import ipaddress import os import shutil import stat import subprocess import distro import jinja2 from oslo_config import cfg from oslo_log import log as logging import webob from werkzeug import exceptions from octavia.common import constants as consts from octavia.common import exceptions as octavia_exceptions from octavia.common import utils CONF = cfg.CONF LOG = logging.getLogger(__name__) j2_env = jinja2.Environment(autoescape=True, loader=jinja2.FileSystemLoader( os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES)) class BaseOS(object): def __init__(self, os_name): self.os_name = os_name self.package_name_map = {} @classmethod def _get_subclasses(cls): for subclass in cls.__subclasses__(): for sc in subclass._get_subclasses(): yield sc yield subclass @classmethod def get_os_util(cls): os_name = distro.id() for subclass in cls._get_subclasses(): if subclass.is_os_name(os_name): return subclass(os_name) raise octavia_exceptions.InvalidAmphoraOperatingSystem(os_name=os_name) def _map_package_name(self, package_name): return self.package_name_map.get(package_name, package_name) def get_network_interface_file(self, interface): if CONF.amphora_agent.agent_server_network_file: return CONF.amphora_agent.agent_server_network_file if CONF.amphora_agent.agent_server_network_dir: return os.path.join(CONF.amphora_agent.agent_server_network_dir, interface) network_dir = consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format( netns=consts.AMPHORA_NAMESPACE) return os.path.join(network_dir, interface) def create_netns_dir(self, network_dir, netns_network_dir, ignore=None): # We need to setup the netns network directory so that the ifup # commands used here and in the startup scripts "sees" the right # interfaces and scripts. try: os.makedirs('/etc/netns/' + consts.AMPHORA_NAMESPACE) shutil.copytree( network_dir, '/etc/netns/{netns}/{net_dir}'.format( netns=consts.AMPHORA_NAMESPACE, net_dir=netns_network_dir), symlinks=True, ignore=ignore) except OSError as e: # Raise the error if it's not "File exists" otherwise pass if e.errno != errno.EEXIST: raise def write_vip_interface_file(self, interface_file_path, primary_interface, vip, ip, broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, template_vip): # write interface file mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH # If we are using a consolidated interfaces file, just append # otherwise clear the per interface file as we are rewriting it # TODO(johnsom): We need a way to clean out old interfaces records if CONF.amphora_agent.agent_server_network_file: flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND else: flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC with os.fdopen(os.open(interface_file_path, flags, mode), 'w') as text_file: text = template_vip.render( consts=consts, interface=primary_interface, vip=vip, vip_ipv6=ip.version == 6, # For ipv6 the netmask is already the prefix prefix=(netmask if ip.version == 6 else utils.netmask_to_prefix(netmask)), broadcast=broadcast, netmask=netmask, gateway=gateway, network=utils.ip_netmask_to_cidr(vip, netmask), mtu=mtu, vrrp_ip=vrrp_ip, vrrp_ipv6=vrrp_version == 6, host_routes=render_host_routes, topology=CONF.controller_worker.loadbalancer_topology, ) text_file.write(text) def write_port_interface_file(self, netns_interface, fixed_ips, mtu, interface_file_path, template_port): # write interface file # If we are using a consolidated interfaces file, just append # otherwise clear the per interface file as we are rewriting it # TODO(johnsom): We need a way to clean out old interfaces records if CONF.amphora_agent.agent_server_network_file: flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND else: flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC # mode 00644 mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH with os.fdopen(os.open(interface_file_path, flags, mode), 'w') as text_file: text = self._generate_network_file_text( netns_interface, fixed_ips, mtu, template_port) text_file.write(text) @classmethod def _generate_network_file_text(cls, netns_interface, fixed_ips, mtu, template_port): text = '' if fixed_ips is None: text = template_port.render(interface=netns_interface) else: for index, fixed_ip in enumerate(fixed_ips, -1): try: ip_addr = fixed_ip['ip_address'] cidr = fixed_ip['subnet_cidr'] ip = ipaddress.ip_address(ip_addr) network = ipaddress.ip_network(cidr) broadcast = network.broadcast_address.exploded netmask = (network.prefixlen if ip.version == 6 else network.netmask.exploded) host_routes = cls.get_host_routes(fixed_ip) except ValueError: return webob.Response( json=dict(message="Invalid network IP"), status=400) new_text = template_port.render(interface=netns_interface, ipv6=ip.version == 6, ip_address=ip.exploded, broadcast=broadcast, netmask=netmask, mtu=mtu, host_routes=host_routes) text = '\n'.join([text, new_text]) return text @classmethod def get_host_routes(cls, fixed_ip): host_routes = [] for hr in fixed_ip.get('host_routes', []): network = ipaddress.ip_network(hr['destination']) host_routes.append({'network': network, 'gw': hr['nexthop']}) return host_routes @classmethod def _bring_if_up(cls, interface, what, flush=True): # Note, we are not using pyroute2 for this as it is not /etc/netns # aware. # Work around for bug: # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=845121 int_up = "ip netns exec {ns} ip link set {int} up".format( ns=consts.AMPHORA_NAMESPACE, int=interface) addr_flush = "ip netns exec {ns} ip addr flush {int}".format( ns=consts.AMPHORA_NAMESPACE, int=interface) cmd = ("ip netns exec {ns} ifup {params}".format( ns=consts.AMPHORA_NAMESPACE, params=interface)) try: out = subprocess.check_output(int_up.split(), stderr=subprocess.STDOUT) LOG.debug(out) if flush: out = subprocess.check_output(addr_flush.split(), stderr=subprocess.STDOUT) LOG.debug(out) out = subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) LOG.debug(out) except subprocess.CalledProcessError as e: LOG.error('Failed to ifup %s due to error: %s %s', interface, e, e.output) raise exceptions.HTTPException( response=webob.Response(json=dict( message='Error plugging {0}'.format(what), details=e.output), status=500)) @classmethod def _bring_if_down(cls, interface): # Note, we are not using pyroute2 for this as it is not /etc/netns # aware. cmd = ("ip netns exec {ns} ifdown {params}".format( ns=consts.AMPHORA_NAMESPACE, params=interface)) try: subprocess.check_output(cmd.split(), stderr=subprocess.STDOUT) except subprocess.CalledProcessError as e: LOG.info('Ignoring failure to ifdown %s due to error: %s %s', interface, e, e.output) @classmethod def bring_interfaces_up(cls, ip, primary_interface, secondary_interface): cls._bring_if_down(primary_interface) if secondary_interface: cls._bring_if_down(secondary_interface) cls._bring_if_up(primary_interface, 'VIP') if secondary_interface: cls._bring_if_up(secondary_interface, 'VIP', flush=False) def has_ifup_all(self): return True class Ubuntu(BaseOS): ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2' ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2' @classmethod def is_os_name(cls, os_name): return os_name in ['ubuntu'] def cmd_get_version_of_installed_package(self, package_name): name = self._map_package_name(package_name) return "dpkg-query -W -f=${{Version}} {name}".format(name=name) def get_network_interface_file(self, interface): if CONF.amphora_agent.agent_server_network_file: return CONF.amphora_agent.agent_server_network_file if CONF.amphora_agent.agent_server_network_dir: return os.path.join(CONF.amphora_agent.agent_server_network_dir, interface + '.cfg') network_dir = consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format( netns=consts.AMPHORA_NAMESPACE) return os.path.join(network_dir, interface + '.cfg') def get_network_path(self): return '/etc/network' def get_netns_network_dir(self): network_dir = self.get_network_path() return os.path.basename(network_dir) def create_netns_dir( self, network_dir=None, netns_network_dir=None, ignore=None): if not netns_network_dir: netns_network_dir = self.get_netns_network_dir() if not network_dir: network_dir = self.get_network_path() if not ignore: ignore = shutil.ignore_patterns('eth0*', 'openssh*') super(Ubuntu, self).create_netns_dir( network_dir, netns_network_dir, ignore) def write_interfaces_file(self): name = '/etc/netns/{}/network/interfaces'.format( consts.AMPHORA_NAMESPACE) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC # mode 00644 mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH with os.fdopen(os.open(name, flags, mode), 'w') as int_file: int_file.write('auto lo\n') int_file.write('iface lo inet loopback\n') if not CONF.amphora_agent.agent_server_network_file: int_file.write('source /etc/netns/{}/network/' 'interfaces.d/*.cfg\n'.format( consts.AMPHORA_NAMESPACE)) def write_vip_interface_file(self, interface_file_path, primary_interface, vip, ip, broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, template_vip=None): if not template_vip: template_vip = j2_env.get_template(self.ETH_X_VIP_CONF) super(Ubuntu, self).write_vip_interface_file( interface_file_path, primary_interface, vip, ip, broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, template_vip) def write_port_interface_file(self, netns_interface, fixed_ips, mtu, interface_file_path=None, template_port=None): if not interface_file_path: interface_file_path = self.get_network_interface_file( netns_interface) if not template_port: template_port = j2_env.get_template(self.ETH_X_PORT_CONF) super(Ubuntu, self).write_port_interface_file( netns_interface, fixed_ips, mtu, interface_file_path, template_port) def has_ifup_all(self): return True class RH(BaseOS): ETH_X_PORT_CONF = 'rh_plug_port_ethX.conf.j2' ETH_X_VIP_CONF = 'rh_plug_vip_ethX.conf.j2' ETH_X_ALIAS_VIP_CONF = 'rh_plug_vip_ethX_alias.conf.j2' ROUTE_ETH_X_CONF = 'rh_route_ethX.conf.j2' RULE_ETH_X_CONF = 'rh_rule_ethX.conf.j2' # The reason of make them as jinja templates is the current scripts force # to add the iptables, so leave it now for future extending if possible. ETH_IFUP_LOCAL_SCRIPT = 'rh_plug_port_eth_ifup_local.conf.j2' ETH_IFDOWN_LOCAL_SCRIPT = 'rh_plug_port_eth_ifdown_local.conf.j2' @classmethod def is_os_name(cls, os_name): return os_name in ['fedora', 'rhel'] def cmd_get_version_of_installed_package(self, package_name): name = self._map_package_name(package_name) return "rpm -q --queryformat %{{VERSION}} {name}".format(name=name) @staticmethod def _get_network_interface_file(prefix, interface): if CONF.amphora_agent.agent_server_network_file: return CONF.amphora_agent.agent_server_network_file if CONF.amphora_agent.agent_server_network_dir: network_dir = CONF.amphora_agent.agent_server_network_dir else: network_dir = consts.RH_AMP_NET_DIR_TEMPLATE.format( netns=consts.AMPHORA_NAMESPACE) return os.path.join(network_dir, prefix + interface) def get_network_interface_file(self, interface): return self._get_network_interface_file('ifcfg-', interface) def get_alias_network_interface_file(self, interface): return self.get_network_interface_file(interface + ':0') def get_static_routes_interface_file(self, interface, version): route = 'route6-' if version == 6 else 'route-' return self._get_network_interface_file(route, interface) def get_route_rules_interface_file(self, interface, version): rule = 'rule6-' if version == 6 else 'rule-' return self._get_network_interface_file(rule, interface) def get_network_path(self): return '/etc/sysconfig/network-scripts' def get_netns_network_dir(self): network_full_path = self.get_network_path() network_basename = os.path.basename(network_full_path) network_dirname = os.path.dirname(network_full_path) network_prefixdir = os.path.basename(network_dirname) return os.path.join(network_prefixdir, network_basename) def create_netns_dir( self, network_dir=None, netns_network_dir=None, ignore=None): if not netns_network_dir: netns_network_dir = self.get_netns_network_dir() if not network_dir: network_dir = self.get_network_path() if not ignore: ignore = shutil.ignore_patterns('ifcfg-eth0*', 'ifcfg-lo*') super(RH, self).create_netns_dir( network_dir, netns_network_dir, ignore) # Copy /etc/sysconfig/network file src = '/etc/sysconfig/network' dst = '/etc/netns/{netns}/sysconfig'.format( netns=consts.AMPHORA_NAMESPACE) shutil.copy2(src, dst) def write_interfaces_file(self): # No interfaces file in RH based flavors return def write_vip_interface_file(self, interface_file_path, primary_interface, vip, ip, broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, template_vip=None): if not template_vip: template_vip = j2_env.get_template(self.ETH_X_VIP_CONF) super(RH, self).write_vip_interface_file( interface_file_path, primary_interface, vip, ip, broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, template_vip) # keepalived will handle the VIP if we are on active/standby if (ip.version == 4 and CONF.controller_worker.loadbalancer_topology == consts.TOPOLOGY_SINGLE): # Create an IPv4 alias interface, needed in RH based flavors alias_interface_file_path = self.get_alias_network_interface_file( primary_interface) template_vip_alias = j2_env.get_template(self.ETH_X_ALIAS_VIP_CONF) super(RH, self).write_vip_interface_file( alias_interface_file_path, primary_interface, vip, ip, broadcast, netmask, gateway, mtu, vrrp_ip, vrrp_version, render_host_routes, template_vip_alias) routes_interface_file_path = ( self.get_static_routes_interface_file(primary_interface, ip.version)) template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF) self.write_static_routes_interface_file( routes_interface_file_path, primary_interface, render_host_routes, template_routes, gateway, vip, netmask) # keepalived will handle the rule(s) if we are on actvie/standby if (CONF.controller_worker.loadbalancer_topology == consts.TOPOLOGY_SINGLE): route_rules_interface_file_path = ( self.get_route_rules_interface_file(primary_interface, ip.version)) template_rules = j2_env.get_template(self.RULE_ETH_X_CONF) self.write_static_routes_interface_file( route_rules_interface_file_path, primary_interface, render_host_routes, template_rules, gateway, vip, netmask) self._write_ifup_ifdown_local_scripts_if_possible() def write_static_routes_interface_file(self, interface_file_path, interface, host_routes, template_routes, gateway, vip, netmask): # write static routes interface file mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH # TODO(johnsom): We need a way to clean out old interfaces records if CONF.amphora_agent.agent_server_network_file: flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND else: flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC with os.fdopen(os.open(interface_file_path, flags, mode), 'w') as text_file: text = template_routes.render( consts=consts, interface=interface, host_routes=host_routes, gateway=gateway, network=utils.ip_netmask_to_cidr(vip, netmask), vip=vip, topology=CONF.controller_worker.loadbalancer_topology, ) text_file.write(text) def write_port_interface_file(self, netns_interface, fixed_ips, mtu, interface_file_path=None, template_port=None): if not interface_file_path: interface_file_path = self.get_network_interface_file( netns_interface) if not template_port: template_port = j2_env.get_template(self.ETH_X_PORT_CONF) super(RH, self).write_port_interface_file( netns_interface, fixed_ips, mtu, interface_file_path, template_port) if fixed_ips: host_routes = [] host_routes_ipv6 = [] for fixed_ip in fixed_ips: ip_addr = fixed_ip['ip_address'] ip = ipaddress.ip_address(ip_addr) if ip.version == 6: host_routes_ipv6.extend(self.get_host_routes(fixed_ip)) else: host_routes.extend(self.get_host_routes(fixed_ip)) routes_interface_file_path = ( self.get_static_routes_interface_file(netns_interface, 4)) template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF) self.write_static_routes_interface_file( routes_interface_file_path, netns_interface, host_routes, template_routes, None, None, None) routes_interface_file_path_ipv6 = ( self.get_static_routes_interface_file(netns_interface, 6)) template_routes = j2_env.get_template(self.ROUTE_ETH_X_CONF) self.write_static_routes_interface_file( routes_interface_file_path_ipv6, netns_interface, host_routes_ipv6, template_routes, None, None, None) self._write_ifup_ifdown_local_scripts_if_possible() @classmethod def bring_interfaces_up(cls, ip, primary_interface, secondary_interface): if ip.version == 4: super(RH, cls).bring_interfaces_up( ip, primary_interface, secondary_interface) else: # Secondary interface is not present in IPv6 configuration cls._bring_if_down(primary_interface) cls._bring_if_up(primary_interface, 'VIP') def has_ifup_all(self): return False def _write_ifup_ifdown_local_scripts_if_possible(self): if self._check_ifup_ifdown_local_scripts_exists(): template_ifup_local = j2_env.get_template( self.ETH_IFUP_LOCAL_SCRIPT) self.write_port_interface_if_local_scripts(template_ifup_local) template_ifdown_local = j2_env.get_template( self.ETH_IFDOWN_LOCAL_SCRIPT) self.write_port_interface_if_local_scripts(template_ifdown_local, ifup=False) def _check_ifup_ifdown_local_scripts_exists(self): file_names = ['ifup-local', 'ifdown-local'] target_dir = '/sbin/' res = [] for file_name in file_names: if os.path.exists(os.path.join(target_dir, file_name)): res.append(True) else: res.append(False) # This means we only add the scripts when both of them are non-exists return not any(res) def write_port_interface_if_local_scripts( self, template_script, ifup=True): file_name = 'ifup' + '-local' mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC if not ifup: file_name = 'ifdown' + '-local' with os.fdopen( os.open(os.path.join( '/sbin/', file_name), flags, mode), 'w') as text_file: text = template_script.render() text_file.write(text) os.chmod(os.path.join('/sbin/', file_name), stat.S_IEXEC) class CentOS(RH): def __init__(self, os_name): super(CentOS, self).__init__(os_name) if distro.version() == '7': self.package_name_map.update({'haproxy': 'haproxy18'}) @classmethod def is_os_name(cls, os_name): return os_name in ['centos']