diff --git a/elements/amphora-agent/install.d/amphora-agent-source-install/75-amphora-agent-install b/elements/amphora-agent/install.d/amphora-agent-source-install/75-amphora-agent-install index d196557c9d..c7debf1d38 100755 --- a/elements/amphora-agent/install.d/amphora-agent-source-install/75-amphora-agent-install +++ b/elements/amphora-agent/install.d/amphora-agent-source-install/75-amphora-agent-install @@ -27,6 +27,9 @@ ln -s $AMP_VENV/bin/haproxy-vrrp-* /usr/local/bin/ || true # Link heath checker script ln -s $AMP_VENV/bin/amphora-health-checker /usr/local/bin/amphora-health-checker || true +# Link amphora interface script +ln -s $AMP_VENV/bin/amphora-interface /usr/local/bin/amphora-interface || true + mkdir /etc/octavia # we assume certs, etc will come in through the config drive mkdir /etc/octavia/certs diff --git a/etc/octavia.conf b/etc/octavia.conf index 472d4cd18d..65fc1d9a01 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -462,7 +462,6 @@ # # agent_server_network_dir = -# agent_server_network_file = # agent_request_read_timeout = 180 # Minimum TLS protocol, eg: TLS, TLSv1.1, TLSv1.2, TLSv1.3 (if available) diff --git a/octavia/amphorae/backends/agent/agent_jinja_cfg.py b/octavia/amphorae/backends/agent/agent_jinja_cfg.py index fc5e2d68dd..210c5f5c15 100644 --- a/octavia/amphorae/backends/agent/agent_jinja_cfg.py +++ b/octavia/amphorae/backends/agent/agent_jinja_cfg.py @@ -40,8 +40,6 @@ class AgentJinjaTemplater(object): 'agent_server_cert': CONF.amphora_agent.agent_server_cert, 'agent_server_network_dir': CONF.amphora_agent.agent_server_network_dir, - 'agent_server_network_file': - CONF.amphora_agent.agent_server_network_file, 'agent_request_read_timeout': CONF.amphora_agent.agent_request_read_timeout, 'amphora_id': amphora_id, diff --git a/octavia/amphorae/backends/agent/api_server/loadbalancer.py b/octavia/amphorae/backends/agent/api_server/loadbalancer.py index b223e21435..76ea4fe9d2 100644 --- a/octavia/amphorae/backends/agent/api_server/loadbalancer.py +++ b/octavia/amphorae/backends/agent/api_server/loadbalancer.py @@ -28,7 +28,6 @@ import webob from werkzeug import exceptions from octavia.amphorae.backends.agent.api_server import haproxy_compatibility -from octavia.amphorae.backends.agent.api_server import osutils from octavia.amphorae.backends.agent.api_server import util from octavia.common import constants as consts from octavia.common import utils as octavia_utils @@ -73,9 +72,6 @@ class Wrapped(object): class Loadbalancer(object): - def __init__(self): - self._osutils = osutils.BaseOS.get_os_util() - def get_haproxy_config(self, lb_id): """Gets the haproxy config @@ -198,7 +194,6 @@ class Loadbalancer(object): respawn_interval), amphora_netns=consts.AMP_NETNS_SVC_PREFIX, amphora_nsname=consts.AMPHORA_NAMESPACE, - HasIFUPAll=self._osutils.has_ifup_all(), haproxy_major_version=hap_major, haproxy_minor_version=hap_minor ) diff --git a/octavia/amphorae/backends/agent/api_server/osutils.py b/octavia/amphorae/backends/agent/api_server/osutils.py index 27a72d45f9..008ed59dfa 100644 --- a/octavia/amphorae/backends/agent/api_server/osutils.py +++ b/octavia/amphorae/backends/agent/api_server/osutils.py @@ -12,31 +12,22 @@ # 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.amphorae.backends.utils import interface_file 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): @@ -62,160 +53,51 @@ class BaseOS(object): 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 write_interface_file(self, interface, ip_address, prefixlen): + interface = interface_file.InterfaceFile( + name=interface, + addresses=[{ + "address": ip_address, + "prefixlen": prefixlen + }] + ) + interface.write() - 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) + def write_vip_interface_file(self, interface, vip, ip_version, + prefixlen, gateway, + mtu, vrrp_ip, + host_routes): + vip_interface = interface_file.VIPInterfaceFile( + name=interface, + mtu=mtu, + vip=vip, + ip_version=ip_version, + prefixlen=prefixlen, + gateway=gateway, + vrrp_ip=vrrp_ip, + host_routes=host_routes, + topology=CONF.controller_worker.loadbalancer_topology) + vip_interface.write() - 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) + def write_port_interface_file(self, interface, fixed_ips, mtu): + port_interface = interface_file.PortInterfaceFile( + name=interface, + mtu=mtu, + fixed_ips=fixed_ips) + port_interface.write() @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( + def _bring_if_up(cls, interface, what): + cmd = ("ip netns exec {ns} amphora-interface up {params}".format( ns=consts.AMPHORA_NAMESPACE, params=interface)) + LOG.debug("Executing: %s", cmd) 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) + LOG.error('Failed to set up %s due to error: %s %s', interface, + e, e.output) raise exceptions.HTTPException( response=webob.Response(json=dict( message='Error plugging {0}'.format(what), @@ -223,34 +105,23 @@ class BaseOS(object): @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( + cmd = ("ip netns exec {ns} amphora-interface down {params}".format( ns=consts.AMPHORA_NAMESPACE, params=interface)) + LOG.debug("Executing: %s", cmd) 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', + LOG.info('Ignoring failure to set %s down due to error: %s %s', interface, e, e.output) @classmethod - def bring_interfaces_up(cls, ip, primary_interface, secondary_interface): + def bring_interfaces_up(cls, ip, primary_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'] @@ -259,87 +130,9 @@ class Ubuntu(BaseOS): 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().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().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().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'] @@ -348,227 +141,6 @@ class RH(BaseOS): 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*') - super().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().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().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().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): diff --git a/octavia/amphorae/backends/agent/api_server/plug.py b/octavia/amphorae/backends/agent/api_server/plug.py index 78fe839d00..3864359e04 100644 --- a/octavia/amphorae/backends/agent/api_server/plug.py +++ b/octavia/amphorae/backends/agent/api_server/plug.py @@ -30,9 +30,6 @@ from octavia.common import constants as consts CONF = cfg.CONF -ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2' -ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2' - LOG = logging.getLogger(__name__) @@ -40,26 +37,20 @@ class Plug(object): def __init__(self, osutils): self._osutils = osutils + def plug_lo(self): + self._osutils.write_interface_file( + interface="lo", + ip_address="127.0.0.1", + prefixlen=8) + def plug_vip(self, vip, subnet_cidr, gateway, mac_address, mtu=None, vrrp_ip=None, host_routes=None): # Validate vip and subnet_cidr, calculate broadcast address and netmask try: - render_host_routes = [] ip = ipaddress.ip_address(vip) network = ipaddress.ip_network(subnet_cidr) vip = ip.exploded - broadcast = network.broadcast_address.exploded - netmask = (network.prefixlen if ip.version == 6 - else network.netmask.exploded) - vrrp_version = None - if vrrp_ip: - vrrp_ip_obj = ipaddress.ip_address(vrrp_ip) - vrrp_version = vrrp_ip_obj.version - if host_routes: - for hr in host_routes: - network = ipaddress.ip_network(hr['destination']) - render_host_routes.append({'network': network, - 'gw': hr['nexthop']}) + prefixlen = network.prefixlen except ValueError: return webob.Response(json=dict(message="Invalid VIP"), status=400) @@ -76,27 +67,16 @@ class Plug(object): # Always put the VIP interface as eth1 primary_interface = consts.NETNS_PRIMARY_INTERFACE - secondary_interface = "{interface}:0".format( - interface=primary_interface) - interface_file_path = self._osutils.get_network_interface_file( - primary_interface) - - self._osutils.create_netns_dir() - - self._osutils.write_interfaces_file() self._osutils.write_vip_interface_file( - interface_file_path=interface_file_path, - primary_interface=primary_interface, + interface=primary_interface, vip=vip, - ip=ip, - broadcast=broadcast, - netmask=netmask, + ip_version=ip.version, + prefixlen=prefixlen, gateway=gateway, mtu=mtu, vrrp_ip=vrrp_ip, - vrrp_version=vrrp_version, - render_host_routes=render_host_routes) + host_routes=host_routes) # Update the list of interfaces to add to the namespace # This is used in the amphora reboot case to re-establish the namespace @@ -136,13 +116,8 @@ class Plug(object): ipr.link('set', index=idx, net_ns_fd=consts.AMPHORA_NAMESPACE, IFLA_IFNAME=primary_interface) - # In an ha amphora, keepalived should bring the VIP interface up - if (CONF.controller_worker.loadbalancer_topology == - consts.TOPOLOGY_ACTIVE_STANDBY): - secondary_interface = None # bring interfaces up - self._osutils.bring_interfaces_up( - ip, primary_interface, secondary_interface) + self._osutils.bring_interfaces_up(ip, primary_interface) return webob.Response(json=dict( message="OK", @@ -189,13 +164,10 @@ class Plug(object): LOG.info('Plugged interface %s will become %s in the namespace %s', default_netns_interface, netns_interface, consts.AMPHORA_NAMESPACE) - interface_file_path = self._osutils.get_network_interface_file( - netns_interface) self._osutils.write_port_interface_file( - netns_interface=netns_interface, + interface=netns_interface, fixed_ips=fixed_ips, - mtu=mtu, - interface_file_path=interface_file_path) + mtu=mtu) # Update the list of interfaces to add to the namespace self._update_plugged_interfaces_file(netns_interface, mac_address) diff --git a/octavia/amphorae/backends/agent/api_server/server.py b/octavia/amphorae/backends/agent/api_server/server.py index ca46377e54..45b2cc0caf 100644 --- a/octavia/amphorae/backends/agent/api_server/server.py +++ b/octavia/amphorae/backends/agent/api_server/server.py @@ -64,6 +64,8 @@ class Server(object): register_app_error_handler(self.app) + self._plug.plug_lo() + self.app.add_url_rule(rule='/', view_func=self.version_discovery, methods=['GET']) self.app.add_url_rule(rule=PATH_PREFIX + diff --git a/octavia/amphorae/backends/agent/api_server/templates/amphora-netns.systemd.j2 b/octavia/amphorae/backends/agent/api_server/templates/amphora-netns.systemd.j2 index ca5dafccf1..501c2fa660 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/amphora-netns.systemd.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/amphora-netns.systemd.j2 @@ -25,12 +25,4 @@ ExecStart=-/bin/sh -c '/usr/bin/sort -k 1 /var/lib/octavia/plugged_interfaces > # Assign the interfaces into the namespace with the appropriate name ExecStart=-/bin/sh -c '/sbin/ip link | awk \'{getline n; print $0,n}\' | awk \'{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }\' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk \'{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}\'' # Bring up all of the namespace interfaces -{%- if HasIFUPAll %} -# Ubuntu seems to not correctly set up the lo iface when calling ifup -a -# Disable it first, before setting it up. -ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} ifdown lo -ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} ifup -a -{%- else %} -ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} ifup lo -ExecStart=-/bin/awk '{system("/sbin/ip netns exec {{ amphora_nsname }} ifup " $2)}' /var/lib/octavia/plugged_interfaces -{%- endif %} +ExecStart=-/sbin/ip netns exec {{ amphora_nsname }} amphora-interface up all diff --git a/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 deleted file mode 100644 index 61e82e1325..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/plug_port_ethX.conf.j2 +++ /dev/null @@ -1,43 +0,0 @@ -{# -# Copyright 2015 Hewlett-Packard Development Company, L.P. -# -# 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. -#} -# Generated by Octavia agent -auto {{ interface }} -{%- if ip_address %} -iface {{ interface }} inet{{ '6' if ipv6 }} static -address {{ ip_address }} -broadcast {{ broadcast }} -netmask {{ netmask }} -{%- if mtu %} -mtu {{ mtu }} -{%- endif %} -{%- for hr in host_routes %} -{%- if ((hr.network.version == 4 and hr.network.prefixlen == 32) or - (hr.network.version == 6 and hr.network.prefixlen == 128)) %} -up route add -host {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} -down route del -host {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} -{%- else %} -up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} -down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} -{%- endif %} -{%- endfor %} -post-up /usr/local/bin/lvs-masquerade.sh add {{ 'ipv6' if ipv6 else 'ipv4' }} {{ interface }} -post-down /usr/local/bin/lvs-masquerade.sh delete {{ 'ipv6' if ipv6 else 'ipv4' }} {{ interface }} -{%- else %} -iface {{ interface }} inet dhcp -auto {{ interface }}:0 -iface {{ interface }}:0 inet6 auto -{%- endif %} - diff --git a/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 deleted file mode 100644 index 2a11c9e3ce..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 +++ /dev/null @@ -1,82 +0,0 @@ -{# -# Copyright 2015 Hewlett-Packard Development Company, L.P. -# Copyright 2016 Rackspace -# -# 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. -#} -# Generated by Octavia agent -{%- if topology == consts.TOPOLOGY_SINGLE %} -auto {{ interface }} {{ interface }}:0 -{%- else %} -auto {{ interface }} -{%- endif %} -{%- if vrrp_ip %} -iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static -address {{ vrrp_ip }} -broadcast {{ broadcast }} -netmask {{ netmask }} -{%- if gateway %} -gateway {{ gateway }} -{%- endif %} -{%- if mtu %} -mtu {{ mtu }} -{%- endif %} -{%- for hr in host_routes %} -{%- if ((hr.network.version == 4 and hr.network.prefixlen == 32) or - (hr.network.version == 6 and hr.network.prefixlen == 128)) %} -up route add -host {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} -down route del -host {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} -{%- else %} -up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} -down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }} -{%- endif %} -{%- endfor %} -{%- else %} - -iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }} -{%- endif %} - -{%- if topology == consts.TOPOLOGY_SINGLE %} -iface {{ interface }}:0 inet{{ '6' if vip_ipv6 }} static -address {{ vip }} -broadcast {{ broadcast }} -netmask {{ netmask }} -{%- endif %} - -# Add a source routing table to allow members to access the VIP -{%- if gateway %} - -post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add default via {{ gateway }} dev {{ interface }} onlink table 1 -post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del default via {{ gateway }} dev {{ interface }} onlink table 1 - -{# Keepalived will insert and remove this route in active/standby #} -{%- if topology == consts.TOPOLOGY_SINGLE %} -post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add {{ network }} dev {{ interface }} src {{ vip }} scope link table 1 -post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del {{ network }} dev {{ interface }} src {{ vip }} scope link table 1 -{%- endif %} - -{%- endif %} - -{%- for hr in host_routes %} -post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}route add {{ hr.network }} via {{ hr.gw }} dev {{ interface }} onlink table 1 -post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}route del {{ hr.network }} via {{ hr.gw }} dev {{ interface }} onlink table 1 -{%- endfor %} - -{# Keepalived will insert and remove this rule in active/standby #} -{%- if topology == consts.TOPOLOGY_SINGLE %} -post-up /sbin/ip {{ '-6 ' if vip_ipv6 }}rule add from {{ vip }}/{{ '128' if vip_ipv6 else '32' }} table 1 priority 100 -post-down /sbin/ip {{ '-6 ' if vip_ipv6 }}rule del from {{ vip }}/{{ '128' if vip_ipv6 else '32' }} table 1 priority 100 -{%- endif %} - -post-up /usr/local/bin/lvs-masquerade.sh add {{ 'ipv6' if vip_ipv6 else 'ipv4' }} {{ interface }} -post-down /usr/local/bin/lvs-masquerade.sh delete {{ 'ipv6' if vip_ipv6 else 'ipv4' }} {{ interface }} diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_ethX.conf.j2 deleted file mode 100644 index 1b47b2a868..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_ethX.conf.j2 +++ /dev/null @@ -1,47 +0,0 @@ -{# -# 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. -#} -# Generated by Octavia agent -NM_CONTROLLED="no" -DEVICE="{{ interface }}" -ONBOOT="yes" -TYPE="Ethernet" -USERCTL="yes" -{%- if ipv6 %} -IPV6INIT="yes" -{%- if mtu %} -IPV6_MTU="{{ mtu }}" -{%- endif %} -{%- if ip_address %} -IPV6_AUTOCONF="no" -IPV6ADDR="{{ ip_address }}" -{%- else %} -IPV6_AUTOCONF="yes" -{%- endif %} -{%- else %} -IPV6INIT="no" -{%- if mtu %} -MTU="{{ mtu }}" -{%- endif %} -{%- if ip_address %} -BOOTPROTO="static" -IPADDR="{{ ip_address }}" -NETMASK="{{ netmask }}" -{%- else %} -BOOTPROTO="dhcp" -PERSISTENT_DHCLIENT="1" -{%- endif %} -{%- endif %} - diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifdown_local.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifdown_local.conf.j2 deleted file mode 100644 index a2fd8835aa..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifdown_local.conf.j2 +++ /dev/null @@ -1,19 +0,0 @@ -{# 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. -#} -# Generated by Octavia agent -#!/bin/bash -if [[ "$1" != "lo" ]] - then - /usr/local/bin/lvs-masquerade.sh delete ipv4 $1 - /usr/local/bin/lvs-masquerade.sh delete ipv6 $1 -fi diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifup_local.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifup_local.conf.j2 deleted file mode 100644 index 82fddeeba6..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_port_eth_ifup_local.conf.j2 +++ /dev/null @@ -1,19 +0,0 @@ -{# 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. -#} -# Generated by Octavia agent -#!/bin/bash -if [[ "$1" != "lo" ]] - then - /usr/local/bin/lvs-masquerade.sh add ipv4 $1 - /usr/local/bin/lvs-masquerade.sh add ipv6 $1 -fi diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX.conf.j2 deleted file mode 100644 index c2a66343c0..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX.conf.j2 +++ /dev/null @@ -1,60 +0,0 @@ -{# -# 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. -#} -# Generated by Octavia agent -NM_CONTROLLED="no" -DEVICE="{{ interface }}" -ONBOOT="yes" -TYPE="Ethernet" -USERCTL="yes" - -{%- if vrrp_ip %} -{%- if vrrp_ipv6 %} -IPV6INIT="yes" -IPV6_DEFROUTE="yes" -IPV6_AUTOCONF="no" -IPV6ADDR="{{ vrrp_ip }}/{{ prefix }}" -{%- if gateway %} -IPV6_DEFAULTGW="{{ gateway }}" -{%- endif %} -{%- if mtu %} -IPV6_MTU="{{ mtu }}" -{%- endif %} -{%- else %} {# not vrrp_ipv6 #} -BOOTPROTO="static" -IPADDR="{{ vrrp_ip }}" -NETMASK="{{ netmask }}" -{%- if gateway %} -GATEWAY="{{ gateway }}" -{%- endif %} -MTU="{{ mtu }}" -{%- endif %} {# end if vrrp_ipv6 #} -{%- else %} {# not vrrp_ip #} -{%- if vip_ipv6 %} -IPV6INIT="yes" -IPV6_DEFROUTE="yes" -IPV6_AUTOCONF="yes" -{%- else %} -BOOTPROTO="dhcp" -PERSISTENT_DHCLIENT="1" -{%- endif %} {# end if vip_ipv6 #} -{%- endif %} {# end if vrrp_ip #} - -{%- if topology == consts.TOPOLOGY_SINGLE -%} -{%- if vip_ipv6 %} -IPV6ADDR_SECONDARIES="{{ vip }}/{{ prefix }}" -{%- endif %} -{%- endif %} - diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX_alias.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX_alias.conf.j2 deleted file mode 100644 index 42430f0a26..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/rh_plug_vip_ethX_alias.conf.j2 +++ /dev/null @@ -1,29 +0,0 @@ -{# -# 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. -#} -# Generated by Octavia agent -NM_CONTROLLED="no" -DEVICE="{{ interface }}:0" -NAME="{{ interface }}:0" -ONBOOT="yes" -ARPCHECK="no" -IPV6INIT="no" -{%- if mtu %} -MTU="{{ mtu }}" -{%- endif %} -BOOTPROTO="static" -IPADDR="{{ vip }}" -NETMASK="{{ netmask }}" - diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 deleted file mode 100644 index 3ce6777c1d..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/rh_route_ethX.conf.j2 +++ /dev/null @@ -1,29 +0,0 @@ -{# -# 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. -#} -# Generated by Octavia agent -{%- for hr in host_routes %} -{{ hr.network }} via {{ hr.gw }} dev {{ interface }} -{%- endfor %} -# Add a source routing table to allow members to access the VIP -{%- if gateway %} -{%- if topology == consts.TOPOLOGY_SINGLE %} -{{ network }} dev {{ interface }} src {{ vip }} scope link table 1 -{%- endif %} -default table 1 via {{ gateway }} dev {{ interface }} -{%- endif %} -{%- for hr in host_routes %} -{{ hr.network }} table 1 via {{ hr.gw }} dev {{ interface }} -{%- endfor %} diff --git a/octavia/amphorae/backends/agent/api_server/templates/rh_rule_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/rh_rule_ethX.conf.j2 deleted file mode 100644 index 0164f191d1..0000000000 --- a/octavia/amphorae/backends/agent/api_server/templates/rh_rule_ethX.conf.j2 +++ /dev/null @@ -1,17 +0,0 @@ -{# -# Copyright 2017 Rackspace, US Inc. -# -# 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. -#} -# Generated by Octavia agent -from {{ vip }} table 1 diff --git a/octavia/amphorae/backends/agent/api_server/templates/sysvinit.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/sysvinit.conf.j2 index 4aa16b5142..f2db01eb48 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/sysvinit.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/sysvinit.conf.j2 @@ -62,11 +62,7 @@ haproxy_start() # Assign the interfaces into the namespace with the appropriate name ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true # Bring up all of the namespace interfaces - {%- if HasIFUPAll %} - ip netns exec {{ amphora_nsname }} ifup -a || true - {%- else %} - awk '{system("/sbin/ip netns exec {{ amphora_nsname }} ifup " $2)}' /var/lib/octavia/plugged_interfaces || true - {%- endif %} + ip netns exec {{ amphora_nsname }} amphora-interface up all || true start-stop-daemon --start --pidfile "$PIDFILE" \ --exec $HAPROXY -- -f "$CONFIG" -f "$USER_GROUP_CONF_PATH" -L "$PEER_NAME" -D -p "$PIDFILE" \ diff --git a/octavia/amphorae/backends/agent/api_server/templates/upstart.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/upstart.conf.j2 index 25a89811fd..c8fcd5c050 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/upstart.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/upstart.conf.j2 @@ -42,11 +42,7 @@ pre-start script # Assign the interfaces into the namespace with the appropriate name ip link | awk '{getline n; print $0,n}' | awk '{sub(":","",$2)} { for(i=1;i<=NF;i++) if ($i == "link/ether") {print $(i+1) " " $2} }' | sort -k 1 | join -j 1 - /var/lib/octavia/plugged_interfaces.sorted | awk '{system("ip link set "$2" netns {{ amphora_nsname }} name "$3"")}' || true # Bring up all of the namespace interfaces - {%- if HasIFUPAll %} - ip netns exec {{ amphora_nsname }} ifup -a || true - {%- else %} - awk '{system("/sbin/ip netns exec {{ amphora_nsname }} ifup " $2)}' /var/lib/octavia/plugged_interfaces || true - {%- endif %} + ip netns exec {{ amphora_nsname }} amphora-interface up all || true end script diff --git a/octavia/amphorae/backends/agent/api_server/util.py b/octavia/amphorae/backends/agent/api_server/util.py index 5043ea12b6..45d76a6e13 100644 --- a/octavia/amphorae/backends/agent/api_server/util.py +++ b/octavia/amphorae/backends/agent/api_server/util.py @@ -22,7 +22,6 @@ import jinja2 from oslo_config import cfg from oslo_log import log as logging -from octavia.amphorae.backends.agent.api_server import osutils from octavia.amphorae.backends.utils import ip_advertisement from octavia.amphorae.backends.utils import network_utils from octavia.common import constants as consts @@ -240,8 +239,6 @@ def get_os_init_system(): def install_netns_systemd_service(): - os_utils = osutils.BaseOS.get_os_util() - 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) @@ -260,8 +257,7 @@ def install_netns_systemd_service(): with os.fdopen(os.open(netns_path, flags, mode), 'w') as text_file: text = jinja_env.get_template( consts.AMP_NETNS_SVC_PREFIX + '.systemd.j2').render( - amphora_nsname=consts.AMPHORA_NAMESPACE, - HasIFUPAll=os_utils.has_ifup_all()) + amphora_nsname=consts.AMPHORA_NAMESPACE) text_file.write(text) diff --git a/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template b/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template index 0e3c02743a..97294a1f76 100644 --- a/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template +++ b/octavia/amphorae/backends/agent/templates/amphora_agent_conf.template @@ -40,9 +40,6 @@ agent_server_cert = {{ agent_server_cert }} {% if agent_server_network_dir -%} agent_server_network_dir = {{ agent_server_network_dir }} {% endif -%} -{% if agent_server_network_file -%} -agent_server_network_file = {{ agent_server_network_file }} -{% endif -%} agent_request_read_timeout = {{ agent_request_read_timeout }} amphora_id = {{ amphora_id }} amphora_udp_driver = {{ amphora_udp_driver }} diff --git a/octavia/amphorae/backends/utils/interface.py b/octavia/amphorae/backends/utils/interface.py new file mode 100644 index 0000000000..04090dbdef --- /dev/null +++ b/octavia/amphorae/backends/utils/interface.py @@ -0,0 +1,237 @@ +# Copyright 2020 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 socket +import subprocess +import time + +from oslo_config import cfg +from oslo_log import log as logging +import pyroute2 + +from octavia.amphorae.backends.utils import interface_file +from octavia.common import constants as consts +from octavia.common import exceptions + +CONF = cfg.CONF + +LOG = logging.getLogger(__name__) + + +class InterfaceController(object): + ADD = 'add' + DELETE = 'delete' + SET = 'set' + + def interface_file_list(self): + net_dir = interface_file.InterfaceFile.get_directory() + + for f in os.listdir(net_dir): + for ext in interface_file.InterfaceFile.get_extensions(): + if f.endswith(ext): + yield os.path.join(net_dir, f) + + def list(self): + interfaces = {} + for f in self.interface_file_list(): + iface = interface_file.InterfaceFile.from_file(f) + interfaces[iface.name] = iface + return interfaces + + def _family(self, address): + return (socket.AF_INET6 + if ipaddress.ip_network(address, strict=False).version == 6 + else socket.AF_INET) + + def _ipr_command(self, method, command, + retry_on_invalid_argument=False, + retry_interval=.2, + raise_on_error=True, + max_retries=20, + **kwargs): + + for dummy in range(max_retries + 1): + try: + method(command, **kwargs) + break + except pyroute2.NetlinkError as e: + if e.code == errno.EINVAL and retry_on_invalid_argument: + LOG.debug("Retrying after %d sec.", retry_interval) + time.sleep(retry_interval) + continue + + if command == self.ADD and e.code != errno.EEXIST: + msg = "Cannot call {} {} (with {}): {}".format( + method.__name__, command, kwargs, e) + if raise_on_error: + raise exceptions.AmphoraNetworkConfigException(msg) + LOG.error(msg) + return + else: + msg = "Cannot call {} {} (with {}) after {} retries.".format( + method.__name__, command, kwargs, max_retries) + if raise_on_error: + raise exceptions.AmphoraNetworkConfigException(msg) + LOG.error(msg) + + def _dhclient_up(self, interface_name): + cmd = ["/sbin/dhclient", + "-lf", + "/var/lib/dhclient/dhclient-{}.leases".format( + interface_name), + "-pf", + "/run/dhclient-{}.pid".format(interface_name), + interface_name] + LOG.debug("Running '%s'", cmd) + subprocess.check_output(cmd, stderr=subprocess.STDOUT) + + def _dhclient_down(self, interface_name): + cmd = ["/sbin/dhclient", + "-r", + "-lf", + "/var/lib/dhclient/dhclient-{}.leases".format( + interface_name), + "-pf", + "/run/dhclient-{}.pid".format(interface_name), + interface_name] + LOG.debug("Running '%s'", cmd) + subprocess.check_output(cmd, stderr=subprocess.STDOUT) + + def _ipv6auto_up(self, interface_name): + # Set values to enable SLAAC on interface_name + # accept_ra is set to 2 to accept router advertisements if forwarding + # is enabled on the interface + for key, value in (('accept_ra', 2), + ('autoconf', 1)): + cmd = ["/sbin/sysctl", + "-w", + "net.ipv6.conf.{}.{}={}".format(interface_name, + key, value)] + LOG.debug("Running '%s'", cmd) + subprocess.check_output(cmd, stderr=subprocess.STDOUT) + + def _ipv6auto_down(self, interface_name): + for key, value in (('accept_ra', 0), + ('autoconf', 0)): + cmd = ["/sbin/sysctl", + "-w", + "net.ipv6.conf.{}.{}={}".format(interface_name, + key, value)] + LOG.debug("Running '%s'", cmd) + subprocess.check_output(cmd, stderr=subprocess.STDOUT) + + def up(self, interface): + LOG.info("Setting interface %s up", interface.name) + + for address in interface.addresses: + if address.get(consts.DHCP): + self._dhclient_up(interface.name) + if address.get(consts.IPV6AUTO): + self._ipv6auto_up(interface.name) + + with pyroute2.IPRoute() as ipr: + idx = ipr.link_lookup(ifname=interface.name)[0] + + self._ipr_command(ipr.link, self.SET, index=idx, + state=consts.IFACE_UP, mtu=interface.mtu) + + for address in interface.addresses: + if (consts.ADDRESS not in address or + address.get(consts.DHCP) or + address.get(consts.IPV6AUTO)): + continue + address[consts.FAMILY] = self._family(address[consts.ADDRESS]) + LOG.debug("%s: Adding address %s", interface.name, address) + self._ipr_command(ipr.addr, self.ADD, index=idx, **address) + + for route in interface.routes: + route[consts.FAMILY] = self._family(route[consts.DST]) + LOG.debug("%s: Adding route %s", interface.name, route) + # Set retry_on_invalid_argument=True because the interface + # might not be ready after setting its addresses + # Note: can we use 'replace' instead of 'add' here? + # Set raise_on_error to False, possible invalid (user-defined) + # routes from the subnet's host_routes will not break the + # script. + self._ipr_command(ipr.route, self.ADD, + retry_on_invalid_argument=True, + raise_on_error=False, + oif=idx, **route) + + for rule in interface.rules: + rule[consts.FAMILY] = self._family(rule[consts.SRC]) + LOG.debug("%s: Adding rule %s", interface.name, rule) + self._ipr_command(ipr.rule, self.ADD, + retry_on_invalid_argument=True, + **rule) + + for script in interface.scripts[consts.IFACE_UP]: + LOG.debug("%s: Running command '%s'", + interface.name, script[consts.COMMAND]) + subprocess.check_output(script[consts.COMMAND].split()) + + def down(self, interface): + LOG.info("Setting interface %s down", interface.name) + + for address in interface.addresses: + if address.get(consts.DHCP): + self._dhclient_down(interface.name) + if address.get(consts.IPV6AUTO): + self._ipv6auto_down(interface.name) + + with pyroute2.IPRoute() as ipr: + idx = ipr.link_lookup(ifname=interface.name)[0] + + link = ipr.get_links(idx)[0] + current_state = link.get(consts.STATE) + + if current_state == consts.IFACE_UP: + for rule in interface.rules: + rule[consts.FAMILY] = self._family(rule[consts.SRC]) + LOG.debug("%s: Deleting rule %s", interface.name, rule) + self._ipr_command(ipr.rule, self.DELETE, + raise_on_error=False, **rule) + + for route in interface.routes: + route[consts.FAMILY] = self._family(route[consts.DST]) + LOG.debug("%s: Deleting route %s", interface.name, route) + self._ipr_command(ipr.route, self.DELETE, + raise_on_error=False, oif=idx, **route) + + for address in interface.addresses: + if consts.ADDRESS not in address: + continue + address[consts.FAMILY] = self._family( + address[consts.ADDRESS]) + LOG.debug("%s: Deleting address %s", + interface.name, address) + self._ipr_command(ipr.addr, self.DELETE, + raise_on_error=False, + index=idx, **address) + + self._ipr_command(ipr.link, self.SET, raise_on_error=False, + index=idx, state=consts.IFACE_DOWN) + + if current_state == consts.IFACE_UP: + for script in interface.scripts[consts.IFACE_DOWN]: + LOG.debug("%s: Running command '%s'", + interface.name, script[consts.COMMAND]) + try: + subprocess.check_output(script[consts.COMMAND].split()) + except Exception as e: + LOG.error("Error while running command '%s' on %s: %s", + script[consts.COMMAND], interface.name, e) diff --git a/octavia/amphorae/backends/utils/interface_file.py b/octavia/amphorae/backends/utils/interface_file.py new file mode 100644 index 0000000000..bb650ecc5c --- /dev/null +++ b/octavia/amphorae/backends/utils/interface_file.py @@ -0,0 +1,218 @@ +# Copyright 2020 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 ipaddress +import os +import stat + +from oslo_config import cfg +import simplejson + +from octavia.common import constants as consts + +CONF = cfg.CONF + + +class InterfaceFile(object): + def __init__(self, name, mtu=None, addresses=None, + routes=None, rules=None, scripts=None): + self.name = name + self.mtu = mtu + self.addresses = addresses or [] + self.routes = routes or [] + self.rules = rules or [] + self.scripts = scripts or { + consts.IFACE_UP: [], + consts.IFACE_DOWN: [] + } + + @classmethod + def get_extensions(cls): + return [".json"] + + @classmethod + def load(cls, fp): + return simplejson.load(fp) + + @classmethod + def dump(cls, obj): + return simplejson.dumps(obj) + + @classmethod + def from_file(cls, filename): + with open(filename, encoding='utf-8') as fp: + config = cls.load(fp) + + return InterfaceFile(**config) + + @classmethod + def get_directory(cls): + return (CONF.amphora_agent.agent_server_network_dir or + consts.AMP_NET_DIR_TEMPLATE) + + @classmethod + def get_host_routes(cls, routes, **kwargs): + host_routes = [] + if routes: + for hr in routes: + route = { + consts.DST: hr['destination'], + consts.GATEWAY: hr['nexthop'], + consts.FLAGS: [consts.ONLINK] + } + route.update(kwargs) + host_routes.append(route) + return host_routes + + def write(self): + mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH + flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC + + net_dir = self.get_directory() + + try: + os.makedirs(net_dir) + except OSError: + pass + + interface_file = "{}.json".format(self.name) + + with os.fdopen(os.open(os.path.join(net_dir, interface_file), + flags, mode), 'w') as fp: + interface = { + consts.NAME: self.name, + consts.ADDRESSES: self.addresses, + consts.ROUTES: self.routes, + consts.RULES: self.rules, + consts.SCRIPTS: self.scripts + } + if self.mtu: + interface[consts.MTU] = self.mtu + fp.write(self.dump(interface)) + + +class VIPInterfaceFile(InterfaceFile): + def __init__(self, name, mtu, + vip, ip_version, prefixlen, + gateway, vrrp_ip, host_routes, + topology): + + super().__init__(name, mtu=mtu) + + if vrrp_ip: + self.addresses.append({ + consts.ADDRESS: vrrp_ip, + consts.PREFIXLEN: prefixlen + }) + else: + key = consts.DHCP if ip_version == 4 else consts.IPV6AUTO + self.addresses.append({ + key: True + }) + + if gateway: + # Add default routes if there's a gateway + self.routes.append({ + consts.DST: ( + "::/0" if ip_version == 6 else "0.0.0.0/0"), + consts.GATEWAY: gateway, + consts.FLAGS: [consts.ONLINK] + }) + self.routes.append({ + consts.DST: ( + "::/0" if ip_version == 6 else "0.0.0.0/0"), + consts.GATEWAY: gateway, + consts.FLAGS: [consts.ONLINK], + consts.TABLE: 1, + }) + + # In ACTIVE_STANDBY topology, keepalived sets these addresses, routes + # and rules + if topology == consts.TOPOLOGY_SINGLE: + self.addresses.append({ + consts.ADDRESS: vip, + consts.PREFIXLEN: prefixlen + }) + vip_cidr = ipaddress.ip_network( + "{}/{}".format(vip, prefixlen), strict=False) + self.routes.append({ + consts.DST: vip_cidr.exploded, + consts.PREFSRC: vip, + consts.SCOPE: 'link', + consts.TABLE: 1, + }) + self.rules.append({ + consts.SRC: vip, + consts.SRC_LEN: 128 if ip_version == 6 else 32, + consts.TABLE: 1, + }) + + self.routes.extend(self.get_host_routes(host_routes)) + self.routes.extend(self.get_host_routes(host_routes, + table=1)) + + self.scripts[consts.IFACE_UP].append({ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add {} {}".format( + 'ipv6' if ip_version == 6 else 'ipv4', name)) + }) + self.scripts[consts.IFACE_DOWN].append({ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete {} {}".format( + 'ipv6' if ip_version == 6 else 'ipv4', name)) + }) + + +class PortInterfaceFile(InterfaceFile): + def __init__(self, name, mtu, fixed_ips): + super().__init__(name, mtu=mtu) + + if fixed_ips: + ip_versions = set() + + for fixed_ip in fixed_ips: + ip_addr = fixed_ip['ip_address'] + cidr = fixed_ip['subnet_cidr'] + ip = ipaddress.ip_address(ip_addr) + network = ipaddress.ip_network(cidr) + prefixlen = network.prefixlen + self.addresses.append({ + consts.ADDRESS: fixed_ip['ip_address'], + consts.PREFIXLEN: prefixlen, + }) + + ip_versions.add(ip.version) + + host_routes = self.get_host_routes( + fixed_ip.get('host_routes', [])) + self.routes.extend(host_routes) + else: + ip_versions = {4, 6} + + self.addresses.append({ + consts.DHCP: True, + consts.IPV6AUTO: True + }) + + for ip_version in ip_versions: + self.scripts[consts.IFACE_UP].append({ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add {} {}".format( + 'ipv6' if ip_version == 6 else 'ipv4', name)) + }) + self.scripts[consts.IFACE_DOWN].append({ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete {} {}".format( + 'ipv6' if ip_version == 6 else 'ipv4', name)) + }) diff --git a/octavia/cmd/interface.py b/octavia/cmd/interface.py new file mode 100644 index 0000000000..51be56e0a9 --- /dev/null +++ b/octavia/cmd/interface.py @@ -0,0 +1,90 @@ +# Copyright 2020 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 sys + +from oslo_config import cfg + +from octavia.amphorae.backends.utils import interface +from octavia.common import config +from octavia.common import exceptions + + +class InterfaceException(exceptions.OctaviaException): + message = "Could not configure interface: %(msg)s" + + +def interfaces_find(interface_controller, name): + all_interfaces = interface_controller.list() + + if name == "all": + return all_interfaces.values() + + if name in all_interfaces: + return [all_interfaces[name]] + + msg = "Could not find interface '{}'.".format(name) + raise InterfaceException(msg=msg) + + +def interfaces_update(interfaces, action_fn, action_str): + errors = [] + + for iface in interfaces: + try: + action_fn(iface) + except Exception as e: + errors.append("Error on action '{}' for interface {}: {}.".format( + action_str, iface.name, e)) + + if errors: + raise InterfaceException(msg=", ".join(errors)) + + +def interface_cmd(interface_name, action): + interface_controller = interface.InterfaceController() + + if action == "up": + action_fn = interface_controller.up + elif action == "down": + action_fn = interface_controller.down + else: + raise InterfaceException( + msg="Unknown action '{}'".format(action)) + + interfaces = interfaces_find(interface_controller, + interface_name) + interfaces_update(interfaces, action_fn, action) + + +def main(): + config.init(sys.argv[1:-2]) + config.setup_logging(cfg.CONF) + + try: + action = sys.argv[-2] + interface_name = sys.argv[-1] + except IndexError: + print("usage: {} [up|down] ".format(sys.argv[0])) + sys.exit(1) + + try: + interface_cmd(interface_name, action) + except Exception as e: + print("Error: {}".format(e)) + sys.exit(2) + + +if __name__ == "__main__": + main() diff --git a/octavia/common/config.py b/octavia/common/config.py index 6197b9d039..f36a1f0fb3 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -160,7 +160,11 @@ amphora_agent_opts = [ cfg.StrOpt('agent_server_network_file', help=_("The file where the network interfaces are located. " "Specifying this will override any value set for " - "agent_server_network_dir.")), + "agent_server_network_dir."), + deprecated_for_removal=True, + deprecated_reason=_('New amphora interface management ' + 'does not support single interface file.'), + deprecated_since='Xena'), cfg.IntOpt('agent_request_read_timeout', default=180, help=_("The time in seconds to allow a request from the " "controller to run before terminating the socket.")), diff --git a/octavia/common/constants.py b/octavia/common/constants.py index dbcda3c006..b5effb877e 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -714,8 +714,6 @@ NOAUTH = 'noauth' TESTING = 'testing' # Amphora distro-specific data -UBUNTU_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/network/interfaces.d/' -RH_AMP_NET_DIR_TEMPLATE = '/etc/netns/{netns}/sysconfig/network-scripts/' UBUNTU = 'ubuntu' CENTOS = 'centos' @@ -894,3 +892,38 @@ SUPPORTED_ALPN_PROTOCOLS = [lib_consts.ALPN_PROTOCOL_HTTP_2, AMPHORA_SUPPORTED_ALPN_PROTOCOLS = [lib_consts.ALPN_PROTOCOL_HTTP_2, lib_consts.ALPN_PROTOCOL_HTTP_1_1, lib_consts.ALPN_PROTOCOL_HTTP_1_0] + +# Amphora interface fields +MTU = 'mtu' +ADDRESSES = 'addresses' +ROUTES = 'routes' +RULES = 'rules' +SCRIPTS = 'scripts' + +# pyroute2 fields +STATE = 'state' + +FAMILY = 'family' +ADDRESS = 'address' +PREFIXLEN = 'prefixlen' +DHCP = 'dhcp' +IPV6AUTO = 'ipv6auto' + +DST = 'dst' +PREFSRC = 'prefsrc' +GATEWAY = 'gateway' +FLAGS = 'flags' +ONLINK = 'onlink' +TABLE = 'table' +SCOPE = 'scope' + +SRC = 'src' +SRC_LEN = 'src_len' + +IFACE_UP = 'up' +IFACE_DOWN = 'down' + +COMMAND = 'command' + +# Amphora network directory +AMP_NET_DIR_TEMPLATE = '/etc/octavia/interfaces/' diff --git a/octavia/common/exceptions.py b/octavia/common/exceptions.py index fca49bf300..43f68df053 100644 --- a/octavia/common/exceptions.py +++ b/octavia/common/exceptions.py @@ -414,3 +414,8 @@ class NetworkServiceError(OctaviaException): class InvalidIPAddress(APIException): msg = _('The IP Address %(ip_addr)s is invalid.') code = 400 + + +class AmphoraNetworkConfigException(OctaviaException): + message = _('Cannot configure network resource in the amphora: ' + '%(detail)s') diff --git a/octavia/tests/common/utils.py b/octavia/tests/common/utils.py index 2c729c3f08..61f2efd21c 100644 --- a/octavia/tests/common/utils.py +++ b/octavia/tests/common/utils.py @@ -12,10 +12,13 @@ # 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 ipaddress from unittest import mock import fixtures +from octavia.common import constants as consts + # Borrowed from neutron # https://review.opendev.org/#/c/232716/ @@ -41,3 +44,67 @@ class OpenFixture(fixtures.Fixture): self._patch = mock.patch('builtins.open', new=replacement_open) self._patch.start() self.addCleanup(self._patch.stop) + + +def assert_address_lists_equal(obj, l1, l2): + obj.assertEqual(len(l1), len(l2), + "Address lists don't match: {} vs {}".format(l1, l2)) + for a1, a2 in zip(l1, l2): + if consts.ADDRESS in a1 and consts.ADDRESS in a2: + obj.assertEqual( + ipaddress.ip_address(a1[consts.ADDRESS]), + ipaddress.ip_address(a2[consts.ADDRESS])) + obj.assertEqual(a1[consts.PREFIXLEN], + a2[consts.PREFIXLEN]) + else: + obj.assertEqual(a1, a2) + + +def assert_route_lists_equal(obj, l1, l2): + obj.assertEqual(len(l1), len(l2), + "Routes don't match: {} vs {}".format(l1, l2)) + for r1, r2 in zip(l1, l2): + obj.assertEqual( + ipaddress.ip_network(r1[consts.DST]), + ipaddress.ip_network(r2[consts.DST])) + if consts.GATEWAY in r1 and consts.GATEWAY in r2: + obj.assertEqual( + ipaddress.ip_address(r1[consts.GATEWAY]), + ipaddress.ip_address(r2[consts.GATEWAY])) + if consts.PREFSRC in r1 and consts.PREFSRC in r2: + obj.assertEqual( + ipaddress.ip_address(r1[consts.PREFSRC]), + ipaddress.ip_address(r2[consts.PREFSRC])) + for attr in (consts.ONLINK, consts.TABLE, consts.SCOPE): + obj.assertEqual(r1.get(attr), r2.get(attr)) + + +def assert_rule_lists_equal(obj, l1, l2): + obj.assertEqual(len(l1), len(l2)) + for r1, r2 in zip(l1, l2): + obj.assertEqual( + ipaddress.ip_address(r1[consts.SRC]), + ipaddress.ip_address(r2[consts.SRC])) + obj.assertEqual(r1[consts.SRC_LEN], r2[consts.SRC_LEN]) + obj.assertEqual(r1[consts.TABLE], r2[consts.TABLE]) + + +def assert_script_lists_equal(obj, l1, l2): + obj.assertEqual(l1, l2) + + +def assert_interface_files_equal(obj, i1, i2): + obj.assertEqual(i1[consts.NAME], i2[consts.NAME]) + obj.assertEqual(i1.get(consts.MTU), i2.get(consts.MTU)) + assert_address_lists_equal(obj, + i1[consts.ADDRESSES], + i2[consts.ADDRESSES]) + assert_route_lists_equal(obj, + i1[consts.ROUTES], + i2[consts.ROUTES]) + assert_rule_lists_equal(obj, + i1[consts.RULES], + i2[consts.RULES]) + assert_script_lists_equal(obj, + i1[consts.SCRIPTS], + i2[consts.SCRIPTS]) diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py index ac15bf19ab..d33e3e3771 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py @@ -56,13 +56,15 @@ class TestServerTestCase(base.TestCase): self.useFixture(fixtures.MockPatch( 'oslo_config.cfg.find_config_files', return_value=[AMP_AGENT_CONF_PATH])) - with mock.patch('distro.id', - return_value='ubuntu'): + with mock.patch('distro.id', return_value='ubuntu'), mock.patch( + 'octavia.amphorae.backends.agent.api_server.plug.' + 'Plug.plug_lo'): self.ubuntu_test_server = server.Server() self.ubuntu_app = self.ubuntu_test_server.app.test_client() - with mock.patch('distro.id', - return_value='centos'): + with mock.patch('distro.id', return_value='centos'), mock.patch( + 'octavia.amphorae.backends.agent.api_server.plug.' + 'Plug.plug_lo'): self.centos_test_server = server.Server() self.centos_app = self.centos_test_server.app.test_client() @@ -955,15 +957,9 @@ class TestServerTestCase(base.TestCase): def test_ubuntu_plug_network(self): self._test_plug_network(consts.UBUNTU) - self.conf.config(group="amphora_agent", - agent_server_network_file="/path/to/interfaces_file") - self._test_plug_network(consts.UBUNTU) def test_centos_plug_network(self): self._test_plug_network(consts.CENTOS) - self.conf.config(group="amphora_agent", - agent_server_network_file="/path/to/interfaces_file") - self._test_plug_network(consts.CENTOS) @mock.patch('os.chmod') @mock.patch('pyroute2.IPRoute', create=True) @@ -1038,7 +1034,9 @@ class TestServerTestCase(base.TestCase): # No interface down m().reset_mock() with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -1064,21 +1062,16 @@ class TestServerTestCase(base.TestCase): file_name = self.conf.conf.amphora_agent.agent_server_network_file flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND - elif distro == consts.UBUNTU: - file_name = ('/etc/netns/{0}/network/interfaces.d/' - 'eth{1}.cfg'.format(consts.AMPHORA_NAMESPACE, - test_int_num)) - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC - - elif distro == consts.CENTOS: - file_name = ('/etc/netns/{0}/sysconfig/network-scripts/' - 'ifcfg-eth{1}'.format(consts.AMPHORA_NAMESPACE, - test_int_num)) + else: + file_name = ('/etc/octavia/interfaces/' + 'eth{}.json'.format(test_int_num)) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -1101,28 +1094,48 @@ class TestServerTestCase(base.TestCase): mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') - handle = m() - if distro == consts.UBUNTU: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto eth{int}\n' - 'iface eth{int} inet dhcp\n' - 'auto eth{int}:0\n' - 'iface eth{int}:0 inet6 auto\n'.format(int=test_int_num)) - elif distro == consts.CENTOS: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'NM_CONTROLLED="no"\n' - 'DEVICE="eth{int}"\n' - 'ONBOOT="yes"\n' - 'TYPE="Ethernet"\n' - 'USERCTL="yes"\n' - 'IPV6INIT="no"\n' - 'BOOTPROTO="dhcp"\n' - 'PERSISTENT_DHCLIENT="1"\n'.format(int=test_int_num)) + expected_dict = { + consts.NAME: "eth{}".format(test_int_num), + consts.ADDRESSES: [ + { + consts.DHCP: True, + consts.IPV6AUTO: True + } + ], + consts.ROUTES: [ + ], + consts.RULES: [ + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "eth{}".format(test_int_num)) + }, { + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv6 " + "eth{}".format(test_int_num)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "eth{}".format(test_int_num)) + }, { + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv6 " + "eth{}".format(test_int_num)) + }] + } + } + + mock_dump.assert_called_once() + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, args[0], expected_dict) + mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', 'eth' + test_int_num], stderr=-2) + 'amphora-interface', 'up', 'eth' + test_int_num], stderr=-2) # fixed IPs happy path port_info = {'mac_address': '123', 'mtu': 1450, 'fixed_ips': [ @@ -1133,20 +1146,16 @@ class TestServerTestCase(base.TestCase): if self.conf.conf.amphora_agent.agent_server_network_file: file_name = self.conf.conf.amphora_agent.agent_server_network_file flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND - elif distro == consts.UBUNTU: - file_name = ('/etc/netns/{0}/network/interfaces.d/' - 'eth{1}.cfg'.format(consts.AMPHORA_NAMESPACE, - test_int_num)) - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC - elif distro == consts.CENTOS: - file_name = ('/etc/netns/{0}/sysconfig/network-scripts/' - 'ifcfg-eth{1}'.format(consts.AMPHORA_NAMESPACE, - test_int_num)) + else: + file_name = ('/etc/octavia/interfaces/' + 'eth{}.json'.format(test_int_num)) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -1169,35 +1178,34 @@ class TestServerTestCase(base.TestCase): mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') - handle = m() - if distro == consts.UBUNTU: - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'auto eth{int}\n' - 'iface eth{int} inet static\n' - 'address 10.0.0.5\nbroadcast 10.0.0.255\n' - 'netmask 255.255.255.0\n' - 'mtu 1450\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv4 ' - 'eth{int}\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv4 ' - 'eth{int}\n'.format(int=test_int_num)) - elif distro == consts.CENTOS: - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'NM_CONTROLLED="no"\n' - 'DEVICE="eth{int}"\n' - 'ONBOOT="yes"\n' - 'TYPE="Ethernet"\n' - 'USERCTL="yes"\n' - 'IPV6INIT="no"\n' - 'MTU="1450"\n' - 'BOOTPROTO="static"\n' - 'IPADDR="10.0.0.5"\n' - 'NETMASK="255.255.255.0"\n'.format(int=test_int_num)) + expected_dict = { + consts.NAME: "eth{}".format(test_int_num), + consts.MTU: 1450, + consts.ADDRESSES: [ + {consts.ADDRESS: '10.0.0.5', consts.PREFIXLEN: 24} + ], + consts.ROUTES: [], + consts.RULES: [], + consts.SCRIPTS: { + consts.IFACE_UP: [ + {consts.COMMAND: + '/usr/local/bin/lvs-masquerade.sh add ipv4 ' + 'eth{}'.format(test_int_num)}], + consts.IFACE_DOWN: [ + {consts.COMMAND: + '/usr/local/bin/lvs-masquerade.sh delete ipv4 ' + 'eth{}'.format(test_int_num)}] + } + } + + mock_dump.assert_called_once() + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, args[0], expected_dict) + mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', 'eth' + test_int_num], stderr=-2) + 'amphora-interface', 'up', 'eth' + test_int_num], stderr=-2) # fixed IPs happy path IPv6 port_info = {'mac_address': '123', 'mtu': 1450, 'fixed_ips': [ @@ -1208,20 +1216,16 @@ class TestServerTestCase(base.TestCase): if self.conf.conf.amphora_agent.agent_server_network_file: file_name = self.conf.conf.amphora_agent.agent_server_network_file flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND - elif distro == consts.UBUNTU: - file_name = ('/etc/netns/{0}/network/interfaces.d/' - 'eth{1}.cfg'.format(consts.AMPHORA_NAMESPACE, - test_int_num)) - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC - elif distro == consts.CENTOS: - file_name = ('/etc/netns/{0}/sysconfig/network-scripts/' - 'ifcfg-eth{1}'.format(consts.AMPHORA_NAMESPACE, - test_int_num)) + else: + file_name = ('/etc/octavia/interfaces/' + 'eth{}.json'.format(test_int_num)) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -1244,31 +1248,34 @@ class TestServerTestCase(base.TestCase): mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') - handle = m() - if distro == consts.UBUNTU: - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'auto eth{int}\n' - 'iface eth{int} inet6 static\n' - 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' - 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32\nmtu 1450\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv6 ' - 'eth{int}\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv6 ' - 'eth{int}\n'.format(int=test_int_num)) - elif distro == consts.CENTOS: - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'NM_CONTROLLED="no"\nDEVICE="eth{int}"\n' - 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes"\n' - 'IPV6INIT="yes"\nIPV6_MTU="1450"\n' - 'IPV6_AUTOCONF="no"\n' - 'IPV6ADDR="2001:0db8:0000:0000:0000:0000:' - '0000:0002"\n'.format(int=test_int_num)) + expected_dict = { + consts.NAME: "eth{}".format(test_int_num), + consts.MTU: 1450, + consts.ADDRESSES: [ + {consts.ADDRESS: '2001:0db8::2', + consts.PREFIXLEN: 32}], + consts.ROUTES: [], + consts.RULES: [], + consts.SCRIPTS: { + consts.IFACE_UP: [ + {consts.COMMAND: + '/usr/local/bin/lvs-masquerade.sh add ipv6 ' + 'eth{}'.format(test_int_num)}], + consts.IFACE_DOWN: [ + {consts.COMMAND: + '/usr/local/bin/lvs-masquerade.sh delete ipv6 ' + 'eth{}'.format(test_int_num)}] + } + } + + mock_dump.assert_called_once() + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, args[0], expected_dict) + mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', 'eth' + test_int_num], stderr=-2) + 'amphora-interface', 'up', 'eth' + test_int_num], stderr=-2) # fixed IPs, bogus IP port_info = {'mac_address': '123', 'fixed_ips': [ @@ -1276,8 +1283,8 @@ class TestServerTestCase(base.TestCase): flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - file_name = '/etc/netns/{0}/network/interfaces.d/eth{1}.cfg'.format( - consts.AMPHORA_NAMESPACE, test_int_num) + file_name = ('/etc/octavia/interfaces/' + 'eth{}.json'.format(test_int_num)) m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( os, 'fdopen', m) as mock_fdopen: @@ -1373,8 +1380,7 @@ class TestServerTestCase(base.TestCase): self.assertIn(distro, [consts.UBUNTU, consts.CENTOS]) SUBNET_CIDR = '192.0.2.0/24' - BROADCAST = '192.0.2.255' - NETMASK = '255.255.255.0' + PREFIXLEN = 24 IP = '192.0.1.5' MAC = '123' DEST1 = '198.51.100.0/24' @@ -1392,16 +1398,14 @@ class TestServerTestCase(base.TestCase): flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - if distro == consts.UBUNTU: - file_name = '/etc/netns/{0}/network/interfaces.d/{1}.cfg'.format( - consts.AMPHORA_NAMESPACE, consts.NETNS_PRIMARY_INTERFACE) - elif distro == consts.CENTOS: - file_name = ('/etc/netns/{0}/sysconfig/network-scripts/' - 'ifcfg-{1}'.format(consts.AMPHORA_NAMESPACE, - consts.NETNS_PRIMARY_INTERFACE)) + file_name = '/etc/octavia/interfaces/{}.json'.format( + consts.NETNS_PRIMARY_INTERFACE) + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -1424,58 +1428,54 @@ class TestServerTestCase(base.TestCase): mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') - handle = m() - if distro == consts.UBUNTU: - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'auto ' + consts.NETNS_PRIMARY_INTERFACE + - '\niface ' + consts.NETNS_PRIMARY_INTERFACE + - ' inet static\n' + - 'address ' + IP + '\nbroadcast ' + BROADCAST + '\n' + - 'netmask ' + NETMASK + '\n' + 'mtu 1450\n' + - 'up route add -net ' + DEST1 + ' gw ' + NEXTHOP + - ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' - 'down route del -net ' + DEST1 + ' gw ' + NEXTHOP + - ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' - 'up route add -host ' + DEST2 + ' gw ' + NEXTHOP + - ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' - 'down route del -host ' + DEST2 + ' gw ' + NEXTHOP + - ' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n' + - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv4 ' + - consts.NETNS_PRIMARY_INTERFACE + '\n' + - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv4 ' + - consts.NETNS_PRIMARY_INTERFACE + '\n') - elif distro == consts.CENTOS: - handle.write.assert_any_call( - '\n\n# Generated by Octavia agent\n' - 'NM_CONTROLLED="no"\nDEVICE="{int}"\n' - 'ONBOOT="yes"\nTYPE="Ethernet"\n' - 'USERCTL="yes"\nIPV6INIT="no"\nMTU="1450"\n' - 'BOOTPROTO="static"\nIPADDR="{ip}"\n' - 'NETMASK="{mask}"\n'.format( - int=consts.NETNS_PRIMARY_INTERFACE, - ip=IP, - mask=NETMASK)) - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC - mock_open.assert_any_call('/sbin/ifup-local', flags, mode) - mock_open.assert_any_call('/sbin/ifdown-local', flags, mode) - calls = [mock.call('/sbin/ifup-local', stat.S_IEXEC), - mock.call('/sbin/ifdown-local', stat.S_IEXEC)] - mock_os_chmod.assert_has_calls(calls) + expected_dict = { + consts.NAME: consts.NETNS_PRIMARY_INTERFACE, + consts.MTU: 1450, + consts.ADDRESSES: [ + { + consts.ADDRESS: IP, + consts.PREFIXLEN: PREFIXLEN + } + ], + consts.ROUTES: [ + { + consts.DST: DEST1, + consts.GATEWAY: NEXTHOP + }, { + consts.DST: DEST2, + consts.GATEWAY: NEXTHOP + } + ], + consts.RULES: [ + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }] + } + } + + mock_dump.assert_called_once() + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, args[0], expected_dict) + mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', consts.NETNS_PRIMARY_INTERFACE], stderr=-2) + 'amphora-interface', 'up', + consts.NETNS_PRIMARY_INTERFACE], stderr=-2) def test_ubuntu_plug_VIP4(self): self._test_plug_VIP4(consts.UBUNTU) - self.conf.config(group="amphora_agent", - agent_server_network_file="/path/to/interfaces_file") - self._test_plug_VIP4(consts.UBUNTU) self._test_plug_VIP4(consts.CENTOS) - self.conf.config(group="amphora_agent", - agent_server_network_file="/path/to/interfaces_file") - self._test_plug_VIP4(consts.CENTOS) @mock.patch('os.chmod') @mock.patch('shutil.copy2') @@ -1616,23 +1616,17 @@ class TestServerTestCase(base.TestCase): if self.conf.conf.amphora_agent.agent_server_network_file: file_name = self.conf.conf.amphora_agent.agent_server_network_file flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND - elif distro == consts.UBUNTU: - file_name = ('/etc/netns/{netns}/network/interfaces.d/' - '{netns_int}.cfg'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC - elif distro == consts.CENTOS: - file_name = ('/etc/netns/{netns}/sysconfig/network-scripts/' - 'ifcfg-{netns_int}'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + else: + file_name = ('/etc/octavia/interfaces/{netns_int}.json'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -1656,73 +1650,79 @@ class TestServerTestCase(base.TestCase): mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') - handle = m() - if distro == consts.UBUNTU: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto {netns_int} {netns_int}:0\n' - 'iface {netns_int} inet static\n' - 'address 203.0.113.4\n' - 'broadcast 203.0.113.255\n' - 'netmask 255.255.255.0\n' - 'gateway 203.0.113.1\n' - 'mtu 1450\n' - 'up route add -net 203.0.114.0/24 gw 203.0.113.5 ' - 'dev {netns_int}\n' - 'down route del -net 203.0.114.0/24 gw 203.0.113.5 ' - 'dev {netns_int}\n' - 'up route add -host 203.0.115.1/32 gw 203.0.113.5 ' - 'dev {netns_int}\n' - 'down route del -host 203.0.115.1/32 gw 203.0.113.5 ' - 'dev {netns_int}\n' - 'iface {netns_int}:0 inet static\n' - 'address 203.0.113.2\n' - 'broadcast 203.0.113.255\n' - 'netmask 255.255.255.0\n\n' - '# Add a source routing table to allow members to ' - 'access the VIP\n\n' - 'post-up /sbin/ip route add default via 203.0.113.1 ' - 'dev eth1 onlink table 1\n' - 'post-down /sbin/ip route del default via 203.0.113.1 ' - 'dev eth1 onlink table 1\n\n\n' - 'post-up /sbin/ip route add 203.0.113.0/24 ' - 'dev eth1 src 203.0.113.2 scope link table 1\n' - 'post-down /sbin/ip route del 203.0.113.0/24 ' - 'dev eth1 src 203.0.113.2 scope link table 1\n' - 'post-up /sbin/ip route add 203.0.114.0/24 ' - 'via 203.0.113.5 dev eth1 onlink table 1\n' - 'post-down /sbin/ip route del 203.0.114.0/24 ' - 'via 203.0.113.5 dev eth1 onlink table 1\n' - 'post-up /sbin/ip route add 203.0.115.1/32 ' - 'via 203.0.113.5 dev eth1 onlink table 1\n' - 'post-down /sbin/ip route del 203.0.115.1/32 ' - 'via 203.0.113.5 dev eth1 onlink table 1\n\n\n' - 'post-up /sbin/ip rule add from 203.0.113.2/32 table 1 ' - 'priority 100\n' - 'post-down /sbin/ip rule del from 203.0.113.2/32 table 1 ' - 'priority 100\n\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv4 eth1\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv4 ' - 'eth1'.format(netns_int=consts.NETNS_PRIMARY_INTERFACE)) - elif distro == consts.CENTOS: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'NM_CONTROLLED="no"\nDEVICE="{netns_int}"\n' - 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes" \n' - 'BOOTPROTO="static"\nIPADDR="203.0.113.4"\n' - 'NETMASK="255.255.255.0"\nGATEWAY="203.0.113.1"\n' - 'MTU="1450" \n'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)) - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC - mock_open.assert_any_call('/sbin/ifup-local', flags, mode) - mock_open.assert_any_call('/sbin/ifdown-local', flags, mode) - calls = [mock.call('/sbin/ifup-local', stat.S_IEXEC), - mock.call('/sbin/ifdown-local', stat.S_IEXEC)] - mock_os_chmod.assert_has_calls(calls) + expected_dict = { + consts.NAME: consts.NETNS_PRIMARY_INTERFACE, + consts.MTU: 1450, + consts.ADDRESSES: [ + { + consts.ADDRESS: "203.0.113.4", + consts.PREFIXLEN: 24 + }, { + consts.ADDRESS: "203.0.113.2", + consts.PREFIXLEN: 24 + } + ], + consts.ROUTES: [ + { + consts.DST: '0.0.0.0/0', + consts.GATEWAY: '203.0.113.1', + consts.FLAGS: [consts.ONLINK] + }, { + consts.DST: '0.0.0.0/0', + consts.GATEWAY: '203.0.113.1', + consts.TABLE: 1, + consts.FLAGS: [consts.ONLINK] + }, { + consts.DST: '203.0.113.0/24', + consts.PREFSRC: '203.0.113.2', + consts.SCOPE: 'link', + consts.TABLE: 1 + }, { + consts.DST: '203.0.114.0/24', + consts.GATEWAY: '203.0.113.5' + }, { + consts.DST: '203.0.115.1/32', + consts.GATEWAY: '203.0.113.5' + }, { + consts.DST: '203.0.114.0/24', + consts.GATEWAY: '203.0.113.5', + consts.TABLE: 1 + }, { + consts.DST: '203.0.115.1/32', + consts.GATEWAY: '203.0.113.5', + consts.TABLE: 1 + } + ], + consts.RULES: [ + { + consts.SRC: '203.0.113.2', + consts.SRC_LEN: 32, + consts.TABLE: 1 + } + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }] + } + } + + mock_dump.assert_called_once() + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, args[0], expected_dict) + mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', '{netns_int}:0'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) + 'amphora-interface', 'up', + consts.NETNS_PRIMARY_INTERFACE], stderr=-2) # Verify sysctl was loaded calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'], @@ -1744,23 +1744,17 @@ class TestServerTestCase(base.TestCase): file_name = self.conf.conf.amphora_agent.agent_server_network_file flags = os.O_WRONLY | os.O_CREAT | os.O_APPEND - elif distro == consts.UBUNTU: - file_name = ('/etc/netns/{netns}/network/interfaces.d/' - '{netns_int}.cfg'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) - flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC - elif distro == consts.CENTOS: - file_name = ('/etc/netns/{netns}/sysconfig/network-scripts/' - 'ifcfg-{netns_int}'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + else: + file_name = ('/etc/octavia/interfaces/' + '{}.json'.format(consts.NETNS_PRIMARY_INTERFACE)) flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -1782,44 +1776,64 @@ class TestServerTestCase(base.TestCase): mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') - handle = m() - if distro == consts.UBUNTU: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto {netns_int} {netns_int}:0\n\n' - 'iface {netns_int} inet dhcp\n' - 'iface {netns_int}:0 inet static\n' - 'address 203.0.113.2\n' - 'broadcast 203.0.113.255\n' - 'netmask 255.255.255.0\n\n' - '# Add a source routing table to allow members to ' - 'access the VIP\n\n' - 'post-up /sbin/ip route add default via 203.0.113.1 ' - 'dev eth1 onlink table 1\n' - 'post-down /sbin/ip route del default via 203.0.113.1 ' - 'dev eth1 onlink table 1\n\n\n' - 'post-up /sbin/ip route add 203.0.113.0/24 ' - 'dev eth1 src 203.0.113.2 scope link table 1\n' - 'post-down /sbin/ip route del 203.0.113.0/24 ' - 'dev eth1 src 203.0.113.2 scope link table 1\n\n\n' - 'post-up /sbin/ip rule add from 203.0.113.2/32 table 1 ' - 'priority 100\n' - 'post-down /sbin/ip rule del from 203.0.113.2/32 table 1 ' - 'priority 100\n\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv4 eth1\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv4 ' - 'eth1'.format(netns_int=consts.NETNS_PRIMARY_INTERFACE)) - elif distro == consts.CENTOS: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'NM_CONTROLLED="no"\nDEVICE="{netns_int}"\n' - 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes" \n' - 'BOOTPROTO="dhcp"\nPERSISTENT_DHCLIENT="1" \n'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + expected_dict = { + consts.NAME: consts.NETNS_PRIMARY_INTERFACE, + consts.ADDRESSES: [ + { + consts.DHCP: True + }, { + consts.ADDRESS: "203.0.113.2", + consts.PREFIXLEN: 24 + } + ], + consts.ROUTES: [ + { + consts.DST: '0.0.0.0/0', + consts.GATEWAY: '203.0.113.1', + consts.FLAGS: [consts.ONLINK] + }, { + consts.DST: '0.0.0.0/0', + consts.GATEWAY: '203.0.113.1', + consts.FLAGS: [consts.ONLINK], + consts.TABLE: 1 + }, { + consts.DST: '203.0.113.0/24', + consts.PREFSRC: '203.0.113.2', + consts.SCOPE: 'link', + consts.TABLE: 1 + } + ], + consts.RULES: [ + { + consts.SRC: '203.0.113.2', + consts.SRC_LEN: 32, + consts.TABLE: 1 + } + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }] + } + } + + mock_dump.assert_called_once() + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, args[0], expected_dict) + mock_check_output.assert_called_with( ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', '{netns_int}:0'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) + 'amphora-interface', 'up', + consts.NETNS_PRIMARY_INTERFACE], stderr=-2) + mock_check_output.side_effect = [ 'unplug1', subprocess.CalledProcessError( @@ -1971,20 +1985,14 @@ class TestServerTestCase(base.TestCase): flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - if distro == consts.UBUNTU: - file_name = ('/etc/netns/{netns}/network/interfaces.d/' - '{netns_int}.cfg'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) - elif distro == consts.CENTOS: - file_name = ('/etc/netns/{netns}/sysconfig/network-scripts/' - 'ifcfg-{netns_int}'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + file_name = ('/etc/octavia/interfaces/{netns_int}.json'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -2007,80 +2015,79 @@ class TestServerTestCase(base.TestCase): mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') - handle = m() - if distro == consts.UBUNTU: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto {netns_int} {netns_int}:0\n' - 'iface {netns_int} inet6 static\n' - 'address 2001:db8::4\n' - 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32\n' - 'gateway 2001:db8::1\n' - 'mtu 1450\n' - 'up route add -net 2001:db9::/32 gw 2001:db8::5 ' - 'dev {netns_int}\n' - 'down route del -net 2001:db9::/32 gw 2001:db8::5 ' - 'dev {netns_int}\n' - 'up route add -host 2001:db9::1/128 gw 2001:db8::5 ' - 'dev {netns_int}\n' - 'down route del -host 2001:db9::1/128 gw 2001:db8::5 ' - 'dev {netns_int}\n' - 'iface {netns_int}:0 inet6 static\n' - 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' - 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32\n\n' - '# Add a source routing table to allow members to access ' - 'the VIP\n\n' - 'post-up /sbin/ip -6 route add default via 2001:db8::1 ' - 'dev eth1 onlink table 1\n' - 'post-down /sbin/ip -6 route del default via 2001:db8::1 ' - 'dev eth1 onlink table 1\n\n\n' - 'post-up /sbin/ip -6 route add 2001:db8::/32 ' - 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' - 'scope link table 1\n' - 'post-down /sbin/ip -6 route del 2001:db8::/32 ' - 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' - 'scope link table 1\n' - 'post-up /sbin/ip -6 route add 2001:db9::/32 via ' - '2001:db8::5 dev eth1 onlink table 1\n' - 'post-down /sbin/ip -6 route del 2001:db9::/32 ' - 'via 2001:db8::5 dev eth1 onlink table 1\n' - 'post-up /sbin/ip -6 route add 2001:db9::1/128 via ' - '2001:db8::5 dev eth1 onlink table 1\n' - 'post-down /sbin/ip -6 route del 2001:db9::1/128 ' - 'via 2001:db8::5 dev eth1 onlink table 1\n\n\n' - 'post-up /sbin/ip -6 rule add from ' - '2001:0db8:0000:0000:0000:0000:0000:0002/128 table 1 ' - 'priority 100\n' - 'post-down /sbin/ip -6 rule del from ' - '2001:0db8:0000:0000:0000:0000:0000:0002/128 table 1 ' - 'priority 100\n\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv6 eth1\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv6 ' - 'eth1'.format(netns_int=consts.NETNS_PRIMARY_INTERFACE)) - elif distro == consts.CENTOS: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'NM_CONTROLLED="no"\nDEVICE="{netns_int}"\n' - 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes"\n' - 'IPV6INIT="yes"\nIPV6_DEFROUTE="yes"\n' - 'IPV6_AUTOCONF="no"\nIPV6ADDR="2001:db8::4/32"\n' - 'IPV6_DEFAULTGW="2001:db8::1"\nIPV6_MTU="1450" \n' - 'IPV6ADDR_SECONDARIES="2001:0db8:0000:0000:0000:0000:' - '0000:0002/32"\n'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + expected_dict = { + consts.NAME: consts.NETNS_PRIMARY_INTERFACE, + consts.MTU: 1450, + consts.ADDRESSES: [ + { + consts.ADDRESS: '2001:db8::4', + consts.PREFIXLEN: 32 + }, { + consts.ADDRESS: '2001:0db8::2', + consts.PREFIXLEN: 32 + } + ], + consts.ROUTES: [ + { + consts.DST: '::/0', + consts.GATEWAY: '2001:db8::1', + consts.FLAGS: [consts.ONLINK] + }, { + consts.DST: '::/0', + consts.GATEWAY: '2001:db8::1', + consts.FLAGS: [consts.ONLINK], + consts.TABLE: 1 + }, { + consts.DST: '2001:0db8::/32', + consts.PREFSRC: '2001:0db8::2', + consts.SCOPE: 'link', + consts.TABLE: 1 + }, { + consts.DST: '2001:db9::/32', + consts.GATEWAY: '2001:db8::5' + }, { + consts.DST: '2001:db9::1/128', + consts.GATEWAY: '2001:db8::5' + }, { + consts.DST: '2001:db9::/32', + consts.GATEWAY: '2001:db8::5', + consts.TABLE: 1 + }, { + consts.DST: '2001:db9::1/128', + consts.GATEWAY: '2001:db8::5', + consts.TABLE: 1 + } + ], + consts.RULES: [ + { + consts.SRC: '2001:0db8::2', + consts.SRC_LEN: 128, + consts.TABLE: 1 + } + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv6 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv6 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }] + } + } - if distro == consts.UBUNTU: - mock_check_output.assert_called_with( - ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', '{netns_int}:0'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) - elif distro == consts.CENTOS: - mock_check_output.assert_called_with( - ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', '{netns_int}'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) + mock_dump.assert_called_once() + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, args[0], expected_dict) + + mock_check_output.assert_called_with( + ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, + 'amphora-interface', 'up', '{netns_int}'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) # Verify sysctl was loaded calls = [mock.call('amphora-haproxy', ['/sbin/sysctl', '--system'], @@ -2099,20 +2106,15 @@ class TestServerTestCase(base.TestCase): # One Interface down, Happy Path IPv6 flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH - if distro == consts.UBUNTU: - file_name = ('/etc/netns/{netns}/network/interfaces.d/' - '{netns_int}.cfg'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) - elif distro == consts.CENTOS: - file_name = ('/etc/netns/{netns}/sysconfig/network-scripts/' - 'ifcfg-{netns_int}'.format( - netns=consts.AMPHORA_NAMESPACE, - netns_int=consts.NETNS_PRIMARY_INTERFACE)) + + file_name = ('/etc/octavia/interfaces/{netns_int}.json'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)) m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open with mock.patch('os.open') as mock_open, mock.patch.object( - os, 'fdopen', m) as mock_fdopen: + os, 'fdopen', m) as mock_fdopen, mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: mock_open.return_value = 123 if distro == consts.UBUNTU: rv = self.ubuntu_app.post('/' + api_server.VERSION + @@ -2133,58 +2135,66 @@ class TestServerTestCase(base.TestCase): mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH mock_open.assert_any_call(plug_inf_file, flags, mode) mock_fdopen.assert_any_call(123, 'r+') - handle = m() - if distro == consts.UBUNTU: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'auto {netns_int} {netns_int}:0\n\n' - 'iface {netns_int} inet6 auto\n' - 'iface {netns_int}:0 inet6 static\n' - 'address 2001:0db8:0000:0000:0000:0000:0000:0002\n' - 'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n' - 'netmask 32\n\n' - '# Add a source routing table to allow members to access ' - 'the VIP\n\n' - 'post-up /sbin/ip -6 route add default via 2001:db8::1 ' - 'dev eth1 onlink table 1\n' - 'post-down /sbin/ip -6 route del default via 2001:db8::1 ' - 'dev eth1 onlink table 1\n\n\n' - 'post-up /sbin/ip -6 route add 2001:db8::/32 ' - 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' - 'scope link table 1\n' - 'post-down /sbin/ip -6 route del 2001:db8::/32 ' - 'dev eth1 src 2001:0db8:0000:0000:0000:0000:0000:0002 ' - 'scope link table 1\n\n\n' - 'post-up /sbin/ip -6 rule add from ' - '2001:0db8:0000:0000:0000:0000:0000:0002/128 table 1 ' - 'priority 100\n' - 'post-down /sbin/ip -6 rule del from ' - '2001:0db8:0000:0000:0000:0000:0000:0002/128 table 1 ' - 'priority 100\n\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv6 eth1\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv6 ' - 'eth1'.format(netns_int=consts.NETNS_PRIMARY_INTERFACE)) - elif distro == consts.CENTOS: - handle.write.assert_any_call( - '\n# Generated by Octavia agent\n' - 'NM_CONTROLLED="no"\nDEVICE="{netns_int}"\n' - 'ONBOOT="yes"\nTYPE="Ethernet"\nUSERCTL="yes" \n' - 'IPV6INIT="yes"\nIPV6_DEFROUTE="yes"\n' - 'IPV6_AUTOCONF="yes" \n' - 'IPV6ADDR_SECONDARIES="2001:0db8:0000:0000:0000:0000:' - '0000:0002/32"\n'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)) - if distro == consts.UBUNTU: - mock_check_output.assert_called_with( - ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', '{netns_int}:0'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) - elif distro == consts.CENTOS: - mock_check_output.assert_called_with( - ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, - 'ifup', '{netns_int}'.format( - netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) + expected_dict = { + consts.NAME: consts.NETNS_PRIMARY_INTERFACE, + consts.MTU: None, + consts.ADDRESSES: [ + { + consts.IPV6AUTO: True + }, + { + consts.ADDRESS: '2001:db8::2', + consts.PREFIXLEN: 32 + } + ], + consts.ROUTES: [ + { + consts.DST: '::/0', + consts.GATEWAY: '2001:db8::1', + consts.FLAGS: [consts.ONLINK] + }, { + consts.DST: '::/0', + consts.GATEWAY: '2001:db8::1', + consts.FLAGS: [consts.ONLINK], + consts.TABLE: 1 + }, { + consts.DST: '2001:db8::/32', + consts.PREFSRC: '2001:db8::2', + consts.SCOPE: 'link', + consts.TABLE: 1 + } + ], + consts.RULES: [ + { + consts.SRC: '2001:db8::2', + consts.SRC_LEN: 128, + consts.TABLE: 1 + } + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv6 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv6 " + "{}".format(consts.NETNS_PRIMARY_INTERFACE)) + }] + } + } + + mock_dump.assert_called_once() + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, args[0], expected_dict) + + mock_check_output.assert_called_with( + ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, + 'amphora-interface', 'up', '{netns_int}'.format( + netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) mock_check_output.side_effect = [ 'unplug1', subprocess.CalledProcessError( @@ -2606,7 +2616,9 @@ class TestServerTestCase(base.TestCase): self.assertEqual(500, rv.status_code) def test_version_discovery(self): - with mock.patch('distro.id', return_value='ubuntu'): + with mock.patch('distro.id', return_value='ubuntu'), mock.patch( + 'octavia.amphorae.backends.agent.api_server.plug.' + 'Plug.plug_lo'): self.test_client = server.Server().app.test_client() expected_dict = {'api_version': api_server.VERSION} rv = self.test_client.get('/') diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py index 1ed82db7ea..658551087b 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_osutils.py @@ -12,18 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. import ipaddress -import os -import shutil from unittest import mock -from oslo_config import fixture as oslo_fixture - from octavia.amphorae.backends.agent.api_server import osutils -from octavia.common import config -from octavia.common import constants as consts from octavia.common import exceptions as octavia_exceptions -from octavia.common import utils -from octavia.tests.common import utils as test_utils from octavia.tests.unit import base @@ -73,204 +65,6 @@ class TestOSUtils(base.TestCase): octavia_exceptions.InvalidAmphoraOperatingSystem, osutils.BaseOS.get_os_util) - def test_get_network_interface_file(self): - conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF)) - - fake_agent_server_network_dir = "/path/to/interface" - fake_agent_server_network_file = "/path/to/interfaces_file" - - base_fake_nic_path = os.path.join(fake_agent_server_network_dir, - consts.NETNS_PRIMARY_INTERFACE) - base_real_nic_path = os.path.join( - consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format( - netns=consts.AMPHORA_NAMESPACE), - consts.NETNS_PRIMARY_INTERFACE) - - rh_interface_name = 'ifcfg-{nic}'.format( - nic=consts.NETNS_PRIMARY_INTERFACE) - rh_fake_nic_path = os.path.join(fake_agent_server_network_dir, - rh_interface_name) - rh_real_nic_path = os.path.join( - consts.RH_AMP_NET_DIR_TEMPLATE.format( - netns=consts.AMPHORA_NAMESPACE), - rh_interface_name) - - ubuntu_interface_name = '{nic}.cfg'.format( - nic=consts.NETNS_PRIMARY_INTERFACE) - ubuntu_fake_nic_path = os.path.join(fake_agent_server_network_dir, - ubuntu_interface_name) - ubuntu_real_nic_path = os.path.join( - consts.UBUNTU_AMP_NET_DIR_TEMPLATE.format( - netns=consts.AMPHORA_NAMESPACE), - ubuntu_interface_name) - - # Check that agent_server_network_file is returned, when provided - conf.config(group="amphora_agent", - agent_server_network_file=fake_agent_server_network_file) - - base_interface_file = ( - self.base_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(fake_agent_server_network_file, base_interface_file) - - rh_interface_file = ( - self.rh_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(fake_agent_server_network_file, rh_interface_file) - - ubuntu_interface_file = ( - self.ubuntu_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(fake_agent_server_network_file, ubuntu_interface_file) - - # Check that agent_server_network_dir is used, when provided - conf.config(group="amphora_agent", agent_server_network_file=None) - conf.config(group="amphora_agent", - agent_server_network_dir=fake_agent_server_network_dir) - - base_interface_file = ( - self.base_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(base_fake_nic_path, base_interface_file) - - rh_interface_file = ( - self.rh_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(rh_fake_nic_path, rh_interface_file) - - ubuntu_interface_file = ( - self.ubuntu_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(ubuntu_fake_nic_path, ubuntu_interface_file) - - # Check When neither agent_server_network_dir or - # agent_server_network_file where provided. - conf.config(group="amphora_agent", agent_server_network_file=None) - conf.config(group="amphora_agent", agent_server_network_dir=None) - - base_interface_file = ( - self.base_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(base_real_nic_path, base_interface_file) - - rh_interface_file = ( - self.rh_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(rh_real_nic_path, rh_interface_file) - - ubuntu_interface_file = ( - self.ubuntu_os_util. - get_network_interface_file(consts.NETNS_PRIMARY_INTERFACE)) - self.assertEqual(ubuntu_real_nic_path, ubuntu_interface_file) - - def _test_RH_get_static_routes_interface_file(self, version): - conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF)) - - fake_agent_server_network_dir = "/path/to/interface" - fake_agent_server_network_file = "/path/to/interfaces_file" - - route = 'route6' if version == 6 else 'route' - rh_route_name = '{route}-{nic}'.format( - route=route, nic=consts.NETNS_PRIMARY_INTERFACE) - rh_fake_route_path = os.path.join(fake_agent_server_network_dir, - rh_route_name) - rh_real_route_path = os.path.join( - consts.RH_AMP_NET_DIR_TEMPLATE.format( - netns=consts.AMPHORA_NAMESPACE), - rh_route_name) - - # Check that agent_server_network_file is returned, when provided - conf.config(group="amphora_agent", - agent_server_network_file=fake_agent_server_network_file) - - rh_route_file = ( - self.rh_os_util. - get_static_routes_interface_file(consts.NETNS_PRIMARY_INTERFACE, - version)) - self.assertEqual(fake_agent_server_network_file, rh_route_file) - - # Check that agent_server_network_dir is used, when provided - conf.config(group="amphora_agent", agent_server_network_file=None) - conf.config(group="amphora_agent", - agent_server_network_dir=fake_agent_server_network_dir) - - rh_route_file = ( - self.rh_os_util. - get_static_routes_interface_file(consts.NETNS_PRIMARY_INTERFACE, - version)) - self.assertEqual(rh_fake_route_path, rh_route_file) - - # Check When neither agent_server_network_dir or - # agent_server_network_file where provided. - conf.config(group="amphora_agent", agent_server_network_file=None) - conf.config(group="amphora_agent", agent_server_network_dir=None) - - rh_route_file = ( - self.rh_os_util. - get_static_routes_interface_file(consts.NETNS_PRIMARY_INTERFACE, - version)) - self.assertEqual(rh_real_route_path, rh_route_file) - - def test_RH_get_static_routes_interface_file(self): - self._test_RH_get_static_routes_interface_file(4) - - def test_RH_get_static_routes_interface_file_ipv6(self): - self._test_RH_get_static_routes_interface_file(6) - - def _test_RH_get_route_rules_interface_file(self, version): - conf = self.useFixture(oslo_fixture.Config(config.cfg.CONF)) - - fake_agent_server_network_dir = "/path/to/interface" - fake_agent_server_network_file = "/path/to/interfaces_file" - - rule = 'rule6' if version == 6 else 'rule' - rh_route_rules_name = '{rule}-{nic}'.format( - rule=rule, nic=consts.NETNS_PRIMARY_INTERFACE) - rh_fake_route_rules_path = os.path.join(fake_agent_server_network_dir, - rh_route_rules_name) - rh_real_route_rules_path = os.path.join( - consts.RH_AMP_NET_DIR_TEMPLATE.format( - netns=consts.AMPHORA_NAMESPACE), - rh_route_rules_name) - - # Check that agent_server_network_file is returned, when provided - conf.config(group="amphora_agent", - agent_server_network_file=fake_agent_server_network_file) - - rh_route_rules_file = ( - self.rh_os_util. - get_route_rules_interface_file(consts.NETNS_PRIMARY_INTERFACE, - version)) - self.assertEqual(fake_agent_server_network_file, rh_route_rules_file) - - # Check that agent_server_network_dir is used, when provided - conf.config(group="amphora_agent", agent_server_network_file=None) - conf.config(group="amphora_agent", - agent_server_network_dir=fake_agent_server_network_dir) - - rh_route_rules_file = ( - self.rh_os_util. - get_route_rules_interface_file(consts.NETNS_PRIMARY_INTERFACE, - version)) - self.assertEqual(rh_fake_route_rules_path, rh_route_rules_file) - - # Check When neither agent_server_network_dir or - # agent_server_network_file where provided. - conf.config(group="amphora_agent", agent_server_network_file=None) - conf.config(group="amphora_agent", agent_server_network_dir=None) - - rh_route_rules_file = ( - self.rh_os_util. - get_route_rules_interface_file(consts.NETNS_PRIMARY_INTERFACE, - version)) - self.assertEqual(rh_real_route_rules_path, rh_route_rules_file) - - def test_RH_get_route_rules_interface_file(self): - self._test_RH_get_route_rules_interface_file(4) - - def test_RH_get_route_rules_interface_file_ipv6(self): - self._test_RH_get_route_rules_interface_file(6) - def test_cmd_get_version_of_installed_package(self): package_name = 'foo' ubuntu_cmd = "dpkg-query -W -f=${{Version}} {name}".format( @@ -302,12 +96,9 @@ class TestOSUtils(base.TestCase): package_name)) self.assertEqual(centos_cmd, returned_centos_cmd) - def test_has_ifup_all(self): - self.assertTrue(self.base_os_util.has_ifup_all()) - self.assertTrue(self.ubuntu_os_util.has_ifup_all()) - self.assertFalse(self.rh_os_util.has_ifup_all()) - - def test_write_vip_interface_file(self): + @mock.patch('octavia.amphorae.backends.utils.interface_file.' + 'VIPInterfaceFile') + def test_write_vip_interface_file(self, mock_vip_interface_file): netns_interface = u'eth1234' FIXED_IP = u'192.0.2.2' SUBNET_CIDR = u'192.0.2.0/24' @@ -323,94 +114,64 @@ class TestOSUtils(base.TestCase): ip = ipaddress.ip_address(FIXED_IP) network = ipaddress.ip_network(SUBNET_CIDR) - broadcast = network.broadcast_address.exploded - netmask = network.netmask.exploded - netmask_prefix = utils.netmask_to_prefix(netmask) ipv6 = ipaddress.ip_address(FIXED_IP_IPV6) networkv6 = ipaddress.ip_network(SUBNET_CIDR_IPV6) - broadcastv6 = networkv6.broadcast_address.exploded - netmaskv6 = networkv6.prefixlen host_routes = [ - {'gw': NEXTHOP, 'network': ipaddress.ip_network(DEST1)}, - {'gw': NEXTHOP, 'network': ipaddress.ip_network(DEST2)} + {'nexthop': NEXTHOP, 'destination': DEST1}, + {'nexthop': NEXTHOP, 'destination': DEST2} ] - path = self.ubuntu_os_util.get_network_interface_file(netns_interface) - mock_open = self.useFixture(test_utils.OpenFixture(path)).mock_open - mock_template = mock.MagicMock() - - # Test an IPv4 VIP - with mock.patch('os.open'), mock.patch.object( - os, 'fdopen', mock_open): - self.ubuntu_os_util.write_vip_interface_file( - interface_file_path=path, - primary_interface=netns_interface, - vip=FIXED_IP, - ip=ip, - broadcast=broadcast, - netmask=netmask, - gateway=GATEWAY, - mtu=MTU, - vrrp_ip=None, - vrrp_version=None, - render_host_routes=host_routes, - template_vip=mock_template) - - mock_template.render.assert_called_once_with( - consts=consts, + self.ubuntu_os_util.write_vip_interface_file( interface=netns_interface, vip=FIXED_IP, - vip_ipv6=False, - prefix=netmask_prefix, - broadcast=broadcast, - netmask=netmask, + ip_version=ip.version, + prefixlen=network.prefixlen, + gateway=GATEWAY, + mtu=MTU, + vrrp_ip=None, + host_routes=host_routes) + + mock_vip_interface_file.assert_called_once_with( + name=netns_interface, + vip=FIXED_IP, + ip_version=ip.version, + prefixlen=network.prefixlen, gateway=GATEWAY, - network=SUBNET_CIDR, mtu=MTU, vrrp_ip=None, - vrrp_ipv6=False, host_routes=host_routes, - topology="SINGLE", - ) + topology="SINGLE") + mock_vip_interface_file.return_value.write.assert_called_once() # Now test with an IPv6 VIP - mock_template.reset_mock() - with mock.patch('os.open'), mock.patch.object( - os, 'fdopen', mock_open): - self.ubuntu_os_util.write_vip_interface_file( - interface_file_path=path, - primary_interface=netns_interface, - vip=FIXED_IP_IPV6, - ip=ipv6, - broadcast=broadcastv6, - netmask=netmaskv6, - gateway=GATEWAY, - mtu=MTU, - vrrp_ip=None, - vrrp_version=None, - render_host_routes=host_routes, - template_vip=mock_template) + mock_vip_interface_file.reset_mock() - mock_template.render.assert_called_once_with( - consts=consts, + self.ubuntu_os_util.write_vip_interface_file( interface=netns_interface, vip=FIXED_IP_IPV6, - vip_ipv6=True, - prefix=netmaskv6, - broadcast=broadcastv6, - netmask=netmaskv6, + ip_version=ipv6.version, + prefixlen=networkv6.prefixlen, gateway=GATEWAY, - network=SUBNET_CIDR_IPV6, mtu=MTU, vrrp_ip=None, - vrrp_ipv6=False, - host_routes=host_routes, - topology="SINGLE", - ) + host_routes=host_routes) - def test_write_port_interface_file(self): + mock_vip_interface_file.assert_called_once_with( + name=netns_interface, + vip=FIXED_IP_IPV6, + ip_version=ipv6.version, + prefixlen=networkv6.prefixlen, + gateway=GATEWAY, + mtu=MTU, + vrrp_ip=None, + host_routes=host_routes, + topology="SINGLE") + + @mock.patch('octavia.amphorae.backends.utils.interface_file.' + 'PortInterfaceFile') + def test_write_port_interface_file(self, mock_port_interface_file): FIXED_IP = u'192.0.2.2' NEXTHOP = u'192.0.2.1' DEST = u'198.51.100.0/24' @@ -431,111 +192,14 @@ class TestOSUtils(base.TestCase): netns_interface = 'eth1234' MTU = 1450 fixed_ips = [ip_addr, ipv6_addr] - path = 'mypath' - mock_template = mock.MagicMock() - mock_open = self.useFixture(test_utils.OpenFixture(path)).mock_open - mock_gen_text = mock.MagicMock() - mock_local_scripts = mock.MagicMock() - mock_wr_fi = mock.MagicMock() - with mock.patch('os.open'), mock.patch.object( - os, 'fdopen', mock_open), mock.patch.object( - osutils.BaseOS, '_generate_network_file_text', mock_gen_text): - self.base_os_util.write_port_interface_file( - netns_interface=netns_interface, - fixed_ips=fixed_ips, - mtu=MTU, - interface_file_path=path, - template_port=mock_template) + self.base_os_util.write_port_interface_file( + interface=netns_interface, + fixed_ips=fixed_ips, + mtu=MTU) - mock_gen_text.assert_called_once_with( - netns_interface, fixed_ips, MTU, mock_template) - - mock_gen_text.reset_mock() - - with mock.patch('os.open'), mock.patch.object( - os, 'fdopen', mock_open), mock.patch.object( - osutils.BaseOS, '_generate_network_file_text', - mock_gen_text), mock.patch.object( - osutils.RH, '_write_ifup_ifdown_local_scripts_if_possible', - mock_local_scripts), mock.patch.object( - osutils.RH, 'write_static_routes_interface_file', mock_wr_fi): - self.rh_os_util.write_port_interface_file( - netns_interface=netns_interface, - fixed_ips=fixed_ips, - mtu=MTU, - interface_file_path=path, - template_port=mock_template) - - rh_route_name = 'route-{nic}'.format(nic=netns_interface) - rh_real_route_path = os.path.join( - consts.RH_AMP_NET_DIR_TEMPLATE.format( - netns=consts.AMPHORA_NAMESPACE), - rh_route_name) - rh_route_name_ipv6 = 'route6-{nic}'.format(nic=netns_interface) - rh_real_route_path_ipv6 = os.path.join( - consts.RH_AMP_NET_DIR_TEMPLATE.format( - netns=consts.AMPHORA_NAMESPACE), - rh_route_name_ipv6) - - exp_routes = [ - {'network': ipaddress.ip_network(DEST), 'gw': NEXTHOP} - ] - exp_routes_ipv6 = [ - {'network': ipaddress.ip_network(DEST_IPV6), 'gw': NEXTHOP_IPV6} - ] - expected_calls = [ - mock.call(rh_real_route_path, netns_interface, - exp_routes, mock.ANY, None, None, None), - mock.call(rh_real_route_path_ipv6, netns_interface, - exp_routes_ipv6, mock.ANY, None, None, None)] - - mock_gen_text.assert_called_once_with( - netns_interface, fixed_ips, MTU, mock_template) - self.assertEqual(2, mock_wr_fi.call_count) - mock_wr_fi.assert_has_calls(expected_calls) - mock_local_scripts.assert_called_once() - - @mock.patch('shutil.copy2') - @mock.patch('os.makedirs') - @mock.patch('shutil.copytree') - def test_create_netns_dir(self, mock_copytree, mock_makedirs, mock_copy2): - network_dir = 'foo' - netns_network_dir = 'fake_netns_network' - ignore = shutil.ignore_patterns('fake_eth*', 'fake_loopback*') - self.rh_os_util.create_netns_dir(network_dir, - netns_network_dir, - ignore) - mock_copytree.assert_any_call( - network_dir, - os.path.join('/etc/netns/', - consts.AMPHORA_NAMESPACE, - netns_network_dir), - ignore=ignore, - symlinks=True) - - mock_makedirs.assert_any_call(os.path.join('/etc/netns/', - consts.AMPHORA_NAMESPACE)) - mock_copy2.assert_any_call( - '/etc/sysconfig/network', - '/etc/netns/{netns}/sysconfig'.format( - netns=consts.AMPHORA_NAMESPACE)) - - mock_copytree.reset_mock() - mock_makedirs.reset_mock() - mock_copy2.reset_mock() - - self.ubuntu_os_util.create_netns_dir(network_dir, - netns_network_dir, - ignore) - mock_copytree.assert_any_call( - network_dir, - os.path.join('/etc/netns/', - consts.AMPHORA_NAMESPACE, - netns_network_dir), - ignore=ignore, - symlinks=True) - - mock_makedirs.assert_any_call(os.path.join('/etc/netns/', - consts.AMPHORA_NAMESPACE)) - mock_copy2.assert_not_called() + mock_port_interface_file.assert_called_once_with( + name=netns_interface, + fixed_ips=fixed_ips, + mtu=MTU) + mock_port_interface_file.return_value.write.assert_called_once() diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py index 5527a88c8d..7e74f260fb 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py @@ -206,142 +206,3 @@ class TestPlug(base.TestCase): # Interface is not found in netns self.assertFalse(self.test_plug._netns_interface_exists('321')) - - -class TestPlugNetwork(base.TestCase): - def setUp(self): - super().setUp() - self.mock_platform = mock.patch("distro.id").start() - - def __generate_network_file_text_static_ip(self): - netns_interface = 'eth1234' - FIXED_IP = '192.0.2.2' - BROADCAST = '192.0.2.255' - SUBNET_CIDR = '192.0.2.0/24' - NETMASK = '255.255.255.0' - DEST1 = '198.51.100.0/24' - DEST2 = '203.0.113.0/24' - NEXTHOP = '192.0.2.1' - MTU = 1450 - fixed_ips = [{'ip_address': FIXED_IP, - 'subnet_cidr': SUBNET_CIDR, - 'host_routes': [ - {'destination': DEST1, 'nexthop': NEXTHOP}, - {'destination': DEST2, 'nexthop': NEXTHOP} - ]}] - format_text = ( - '\n\n# Generated by Octavia agent\n' - 'auto {netns_interface}\n' - 'iface {netns_interface} inet static\n' - 'address {fixed_ip}\n' - 'broadcast {broadcast}\n' - 'netmask {netmask}\n' - 'mtu {mtu}\n' - 'up route add -net {dest1} gw {nexthop} dev {netns_interface}\n' - 'down route del -net {dest1} gw {nexthop} dev {netns_interface}\n' - 'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n' - 'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv4 eth1234\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv4 eth1234\n') - - template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2') - text = self.test_plug._osutils._generate_network_file_text( - netns_interface, fixed_ips, MTU, template_port) - expected_text = format_text.format(netns_interface=netns_interface, - fixed_ip=FIXED_IP, - broadcast=BROADCAST, - netmask=NETMASK, - mtu=MTU, - dest1=DEST1, - dest2=DEST2, - nexthop=NEXTHOP) - self.assertEqual(expected_text, text) - - def __generate_network_file_text_two_static_ips(self): - netns_interface = 'eth1234' - FIXED_IP = '192.0.2.2' - BROADCAST = '192.0.2.255' - SUBNET_CIDR = '192.0.2.0/24' - NETMASK = '255.255.255.0' - DEST1 = '198.51.100.0/24' - DEST2 = '203.0.113.0/24' - NEXTHOP = '192.0.2.1' - MTU = 1450 - FIXED_IP_IPV6 = '2001:0db8:0000:0000:0000:0000:0000:0001' - BROADCAST_IPV6 = '2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff' - SUBNET_CIDR_IPV6 = '2001:db8::/32' - NETMASK_IPV6 = '32' - fixed_ips = [{'ip_address': FIXED_IP, - 'subnet_cidr': SUBNET_CIDR, - 'host_routes': [ - {'destination': DEST1, 'nexthop': NEXTHOP}, - {'destination': DEST2, 'nexthop': NEXTHOP} - ]}, - {'ip_address': FIXED_IP_IPV6, - 'subnet_cidr': SUBNET_CIDR_IPV6, - 'host_routes': []} - ] - format_text = ( - '\n\n# Generated by Octavia agent\n' - 'auto {netns_interface}\n' - 'iface {netns_interface} inet static\n' - 'address {fixed_ip}\n' - 'broadcast {broadcast}\n' - 'netmask {netmask}\n' - 'mtu {mtu}\n' - 'up route add -net {dest1} gw {nexthop} dev {netns_interface}\n' - 'down route del -net {dest1} gw {nexthop} dev {netns_interface}\n' - 'up route add -net {dest2} gw {nexthop} dev {netns_interface}\n' - 'down route del -net {dest2} gw {nexthop} dev {netns_interface}\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv4 ' - '{netns_interface}\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv4 ' - '{netns_interface}\n' - '\n\n# Generated by Octavia agent\n' - 'auto {netns_interface}\n' - 'iface {netns_interface} inet6 static\n' - 'address {fixed_ip_ipv6}\n' - 'broadcast {broadcast_ipv6}\n' - 'netmask {netmask_ipv6}\n' - 'mtu {mtu}\n' - 'post-up /usr/local/bin/lvs-masquerade.sh add ipv6 ' - '{netns_interface}\n' - 'post-down /usr/local/bin/lvs-masquerade.sh delete ipv6 ' - '{netns_interface}\n') - - template_port = osutils.j2_env.get_template('plug_port_ethX.conf.j2') - text = self.test_plug._osutils._generate_network_file_text( - netns_interface, fixed_ips, MTU, template_port) - expected_text = format_text.format(netns_interface=netns_interface, - fixed_ip=FIXED_IP, - broadcast=BROADCAST, - netmask=NETMASK, - mtu=MTU, - dest1=DEST1, - dest2=DEST2, - nexthop=NEXTHOP, - fixed_ip_ipv6=FIXED_IP_IPV6, - broadcast_ipv6=BROADCAST_IPV6, - netmask_ipv6=NETMASK_IPV6) - self.assertEqual(expected_text, text) - - def _setup(self, os): - self.mock_platform.return_value = os - self.osutil = osutils.BaseOS.get_os_util() - self.test_plug = plug.Plug(self.osutil) - - def test__generate_network_file_text_static_ip_ubuntu(self): - self._setup("ubuntu") - self.__generate_network_file_text_static_ip() - - def test__generate_network_file_text_static_ip_centos(self): - self._setup("centos") - self.__generate_network_file_text_static_ip() - - def test__generate_network_file_text_two_static_ips_ubuntu(self): - self._setup("ubuntu") - self.__generate_network_file_text_two_static_ips() - - def test__generate_network_file_text_two_static_ips_centos(self): - self._setup("centos") - self.__generate_network_file_text_two_static_ips() diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py index 1783de3068..add80ca92c 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_util.py @@ -147,7 +147,7 @@ class TestUtil(base.TestCase): mock_jinja_env.get_template.assert_called_once_with( consts.AMP_NETNS_SVC_PREFIX + '.systemd.j2') mock_template.render.assert_called_once_with( - amphora_nsname=consts.AMPHORA_NAMESPACE, HasIFUPAll=True) + amphora_nsname=consts.AMPHORA_NAMESPACE) handle = m() handle.write.assert_called_with('script') diff --git a/octavia/tests/unit/amphorae/backends/agent/test_agent_jinja_cfg.py b/octavia/tests/unit/amphorae/backends/agent/test_agent_jinja_cfg.py index 6c46eece12..31537e523d 100644 --- a/octavia/tests/unit/amphorae/backends/agent/test_agent_jinja_cfg.py +++ b/octavia/tests/unit/amphorae/backends/agent/test_agent_jinja_cfg.py @@ -55,8 +55,6 @@ class AgentJinjaTestCase(base.TestCase): def test_build_agent_config(self): ajc = agent_jinja_cfg.AgentJinjaTemplater() # Test execution order could influence this with the test below - self.conf.config(group='amphora_agent', - agent_server_network_file=None) self.conf.config(group="amphora_agent", administrative_log_facility=1) self.conf.config(group="amphora_agent", user_log_facility=0) @@ -98,8 +96,6 @@ class AgentJinjaTestCase(base.TestCase): def test_build_agent_config_with_interfaces_file(self): ajc = agent_jinja_cfg.AgentJinjaTemplater() - self.conf.config(group="amphora_agent", - agent_server_network_file='/etc/network/interfaces') self.conf.config(group="haproxy_amphora", use_upstart='False') self.conf.config(group="amphora_agent", administrative_log_facility=1) @@ -130,8 +126,6 @@ class AgentJinjaTestCase(base.TestCase): '/etc/octavia/certs/server.pem\n' 'agent_server_network_dir = ' '/etc/network/interfaces.d/\n' - 'agent_server_network_file = ' - '/etc/network/interfaces\n' 'agent_request_read_timeout = 180\n' 'amphora_id = ' + AMP_ID + '\n' 'amphora_udp_driver = keepalived_lvs\n' @@ -145,8 +139,6 @@ class AgentJinjaTestCase(base.TestCase): def test_build_agent_config_with_new_udp_driver(self): ajc = agent_jinja_cfg.AgentJinjaTemplater() - self.conf.config(group='amphora_agent', - agent_server_network_file=None) self.conf.config(group="amphora_agent", amphora_udp_driver='new_udp_driver') self.conf.config(group="amphora_agent", diff --git a/octavia/tests/unit/amphorae/backends/utils/test_interface.py b/octavia/tests/unit/amphorae/backends/utils/test_interface.py new file mode 100644 index 0000000000..ae5d8157c0 --- /dev/null +++ b/octavia/tests/unit/amphorae/backends/utils/test_interface.py @@ -0,0 +1,902 @@ +# Copyright 2020 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 os +import socket +from unittest import mock + +import pyroute2 + +from octavia.amphorae.backends.utils import interface +from octavia.amphorae.backends.utils import interface_file +from octavia.common import constants as consts +from octavia.common import exceptions +from octavia.tests.common import utils as test_utils +import octavia.tests.unit.base as base + + +class TestInterface(base.TestCase): + @mock.patch('os.listdir') + @mock.patch('octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.get_directory') + def test_interface_file_list(self, mock_get_directory, mock_listdir): + mock_get_directory.return_value = consts.AMP_NET_DIR_TEMPLATE + + ifaces = ('eth0', 'eth7', 'eth8') + mock_listdir.return_value = [ + "{}.json".format(iface) + for iface in ifaces + ] + mock_listdir.return_value.extend(["invalidfile"]) + + controller = interface.InterfaceController() + r = controller.interface_file_list() + config_file_list = list(r) + + for iface in ifaces: + f = os.path.join(consts.AMP_NET_DIR_TEMPLATE, + "{}.json".format(iface)) + self.assertIn(f, config_file_list) + + # unsupported file + f = os.path.join(consts.AMP_NET_DIR_TEMPLATE, + "invalidfile") + self.assertNotIn(f, config_file_list) + + # non existing file + f = os.path.join(consts.AMP_NET_DIR_TEMPLATE, + "eth2.json") + self.assertNotIn(f, config_file_list) + + @mock.patch('os.listdir') + @mock.patch('octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.get_directory') + def test_list(self, mock_get_directory, mock_listdir): + mock_get_directory.return_value = consts.AMP_NET_DIR_TEMPLATE + mock_listdir.return_value = ["fakeiface.json"] + + content = ('{\n' + '"addresses": [\n' + '{"address": "10.0.0.2",\n' + '"prefixlen": 24}\n' + '],\n' + '"mtu": 1450,\n' + '"name": "eth1",\n' + '"routes": [\n' + '{"dst": "0.0.0.0/0",\n' + '"gateway": "10.0.0.1"},\n' + '{"dst": "10.11.0.0/16",\n' + '"gateway": "10.0.0.24"}\n' + '],\n' + '"rules": [\n' + '{"src": "10.0.0.2",\n' + '"src_len": 32,\n' + '"table": 100}\n' + '],\n' + '"scripts": {\n' + '"up": [\n' + '{"command": "up-script"}],\n' + '"down": [\n' + '{"command": "down-script"}]\n' + '}}\n') + + filename = os.path.join(consts.AMP_NET_DIR_TEMPLATE, + "fakeiface.json") + + self.useFixture( + test_utils.OpenFixture(filename, + contents=content)) + + controller = interface.InterfaceController() + ifaces = controller.list() + + self.assertIn("eth1", ifaces) + iface = ifaces["eth1"] + + expected_dict = { + consts.NAME: "eth1", + consts.MTU: 1450, + consts.ADDRESSES: [{ + consts.ADDRESS: "10.0.0.2", + consts.PREFIXLEN: 24 + }], + consts.ROUTES: [{ + consts.DST: "0.0.0.0/0", + consts.GATEWAY: "10.0.0.1" + }, { + consts.DST: "10.11.0.0/16", + consts.GATEWAY: "10.0.0.24" + }], + consts.RULES: [{ + consts.SRC: "10.0.0.2", + consts.SRC_LEN: 32, + consts.TABLE: 100 + }], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: "up-script" + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: "down-script" + }] + } + } + + self.assertEqual(expected_dict[consts.NAME], iface.name) + self.assertEqual(expected_dict[consts.MTU], iface.mtu) + test_utils.assert_address_lists_equal( + self, expected_dict[consts.ADDRESSES], iface.addresses) + test_utils.assert_rule_lists_equal( + self, expected_dict[consts.RULES], iface.rules) + test_utils.assert_script_lists_equal( + self, expected_dict[consts.SCRIPTS], iface.scripts) + + def test__ipr_command(self): + mock_ipr_addr = mock.MagicMock() + + controller = interface.InterfaceController() + controller._ipr_command(mock_ipr_addr, + controller.ADD, + arg1=1, arg2=2) + + mock_ipr_addr.assert_called_once_with('add', arg1=1, arg2=2) + + def test__ipr_command_add_eexist(self): + mock_ipr_addr = mock.MagicMock() + mock_ipr_addr.side_effect = [ + pyroute2.NetlinkError(code=errno.EEXIST) + ] + + controller = interface.InterfaceController() + controller._ipr_command(mock_ipr_addr, + controller.ADD, + arg1=1, arg2=2) + + mock_ipr_addr.assert_called_once_with('add', arg1=1, arg2=2) + + def test__ipr_command_add_retry(self): + mock_ipr_addr = mock.MagicMock() + mock_ipr_addr.side_effect = [ + pyroute2.NetlinkError(code=errno.EINVAL), + pyroute2.NetlinkError(code=errno.EINVAL), + pyroute2.NetlinkError(code=errno.EINVAL), + None + ] + + controller = interface.InterfaceController() + controller._ipr_command(mock_ipr_addr, + controller.ADD, + retry_on_invalid_argument=True, + retry_interval=0, + arg1=1, arg2=2) + + mock_ipr_addr.assert_has_calls([ + mock.call('add', arg1=1, arg2=2), + mock.call('add', arg1=1, arg2=2), + mock.call('add', arg1=1, arg2=2), + mock.call('add', arg1=1, arg2=2)]) + + def test__ipr_command_add_einval_failed(self): + mock_ipr_addr = mock.MagicMock() + mock_ipr_addr.__name__ = "addr" + mock_ipr_addr.side_effect = [ + pyroute2.NetlinkError(code=errno.EINVAL) + ] * 21 + + controller = interface.InterfaceController() + self.assertRaises(exceptions.AmphoraNetworkConfigException, + controller._ipr_command, + mock_ipr_addr, + controller.ADD, + retry_on_invalid_argument=True, + max_retries=20, + retry_interval=0, + arg1=1, arg2=2) + mock_ipr_addr.assert_has_calls([ + mock.call('add', arg1=1, arg2=2) + ] * 20) + + def test__ipr_command_add_failed(self): + mock_ipr_addr = mock.MagicMock() + mock_ipr_addr.__name__ = "addr" + mock_ipr_addr.side_effect = [ + pyroute2.NetlinkError(code=errno.ENOENT) + ] + + controller = interface.InterfaceController() + self.assertRaises(exceptions.AmphoraNetworkConfigException, + controller._ipr_command, + mock_ipr_addr, + controller.ADD, + retry_on_invalid_argument=True, + max_retries=20, + retry_interval=0, + arg1=1, arg2=2) + mock_ipr_addr.assert_called_once_with( + 'add', arg1=1, arg2=2) + + def test__ipr_command_delete_failed_no_raise(self): + mock_ipr_addr = mock.MagicMock() + mock_ipr_addr.__name__ = "addr" + mock_ipr_addr.side_effect = [ + pyroute2.NetlinkError(code=errno.EINVAL) + ] + + controller = interface.InterfaceController() + controller._ipr_command(mock_ipr_addr, + controller.DELETE, + retry_on_invalid_argument=True, + max_retries=0, + raise_on_error=False, + arg1=1, arg2=2) + mock_ipr_addr.assert_called_once_with( + 'delete', arg1=1, arg2=2) + + def test__ipr_command_add_failed_retry_no_raise(self): + mock_ipr_addr = mock.MagicMock() + mock_ipr_addr.__name__ = "addr" + mock_ipr_addr.side_effect = [ + pyroute2.NetlinkError(code=errno.ENOENT) + ] + + controller = interface.InterfaceController() + controller._ipr_command(mock_ipr_addr, + controller.ADD, + max_retries=20, + retry_interval=0, + raise_on_error=False, + arg1=1, arg2=2) + mock_ipr_addr.assert_called_once_with( + 'add', arg1=1, arg2=2) + + @mock.patch('subprocess.check_output') + def test__dhclient_up(self, mock_check_output): + iface = "iface2" + + controller = interface.InterfaceController() + controller._dhclient_up(iface) + + mock_check_output.assert_called_once_with( + ["/sbin/dhclient", + "-lf", + "/var/lib/dhclient/dhclient-{}.leases".format( + iface), + "-pf", + "/run/dhclient-{}.pid".format(iface), + iface], stderr=-2) + + @mock.patch('subprocess.check_output') + def test__dhclient_down(self, mock_check_output): + iface = "iface2" + + controller = interface.InterfaceController() + controller._dhclient_down(iface) + + mock_check_output.assert_called_once_with( + ["/sbin/dhclient", + "-r", + "-lf", + "/var/lib/dhclient/dhclient-{}.leases".format( + iface), + "-pf", + "/run/dhclient-{}.pid".format(iface), + iface], stderr=-2) + + @mock.patch('subprocess.check_output') + def test__ipv6auto_up(self, mock_check_output): + iface = "iface2" + + controller = interface.InterfaceController() + controller._ipv6auto_up(iface) + + mock_check_output.assert_has_calls([ + mock.call(["/sbin/sysctl", "-w", + "net.ipv6.conf.iface2.accept_ra=2"], stderr=-2), + mock.call(["/sbin/sysctl", "-w", + "net.ipv6.conf.iface2.autoconf=1"], stderr=-2)]) + + @mock.patch('subprocess.check_output') + def test__ipv6auto_down(self, mock_check_output): + iface = "iface2" + + controller = interface.InterfaceController() + controller._ipv6auto_down(iface) + + mock_check_output.assert_has_calls([ + mock.call(["/sbin/sysctl", "-w", + "net.ipv6.conf.iface2.accept_ra=0"], stderr=-2), + mock.call(["/sbin/sysctl", "-w", + "net.ipv6.conf.iface2.autoconf=0"], stderr=-2)]) + + @mock.patch('pyroute2.IPRoute.rule') + @mock.patch('pyroute2.IPRoute.route') + @mock.patch('pyroute2.IPRoute.addr') + @mock.patch('pyroute2.IPRoute.link') + @mock.patch('pyroute2.IPRoute.link_lookup') + @mock.patch('subprocess.check_output') + def test_up(self, mock_check_output, mock_link_lookup, mock_link, + mock_addr, mock_route, mock_rule): + iface = interface_file.InterfaceFile( + name="eth1", + mtu=1450, + addresses=[{ + consts.ADDRESS: '1.2.3.4', + consts.PREFIXLEN: 24 + }, { + consts.ADDRESS: '10.2.3.4', + consts.PREFIXLEN: 16 + }, { + consts.ADDRESS: '2001:db8::3', + consts.PREFIXLEN: 64 + }], + routes=[{ + consts.DST: '10.0.0.0/8', + consts.GATEWAY: '1.0.0.1', + consts.TABLE: 10, + consts.ONLINK: True + }, { + consts.DST: '20.0.0.0/8', + consts.GATEWAY: '1.0.0.2', + consts.PREFSRC: '1.2.3.4', + consts.SCOPE: 'link' + }, { + consts.DST: '2001:db8:2::1/128', + consts.GATEWAY: '2001:db8::1' + }], + rules=[{ + consts.SRC: '1.1.1.1', + consts.SRC_LEN: 32, + consts.TABLE: 20, + }, { + consts.SRC: '2001:db8::1', + consts.SRC_LEN: 128, + consts.TABLE: 40, + }], + scripts={ + consts.IFACE_UP: [{ + consts.COMMAND: "post-up eth1" + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: "post-down eth1" + }], + }) + + idx = mock.MagicMock() + mock_link_lookup.return_value = [idx] + + controller = interface.InterfaceController() + controller.up(iface) + + mock_link.assert_called_once_with( + controller.SET, + index=idx, + state=consts.IFACE_UP, + mtu=1450) + + mock_addr.assert_has_calls([ + mock.call(controller.ADD, + index=idx, + address='1.2.3.4', + prefixlen=24, + family=socket.AF_INET), + mock.call(controller.ADD, + index=idx, + address='10.2.3.4', + prefixlen=16, + family=socket.AF_INET), + mock.call(controller.ADD, + index=idx, + address='2001:db8::3', + prefixlen=64, + family=socket.AF_INET6) + ]) + + mock_route.assert_has_calls([ + mock.call(controller.ADD, + oif=idx, + dst='10.0.0.0/8', + gateway='1.0.0.1', + table=10, + onlink=True, + family=socket.AF_INET), + mock.call(controller.ADD, + oif=idx, + dst='20.0.0.0/8', + gateway='1.0.0.2', + prefsrc='1.2.3.4', + scope='link', + family=socket.AF_INET), + mock.call(controller.ADD, + oif=idx, + dst='2001:db8:2::1/128', + gateway='2001:db8::1', + family=socket.AF_INET6)]) + + mock_rule.assert_has_calls([ + mock.call(controller.ADD, + src="1.1.1.1", + src_len=32, + table=20, + family=socket.AF_INET), + mock.call(controller.ADD, + src="2001:db8::1", + src_len=128, + table=40, + family=socket.AF_INET6)]) + + mock_check_output.assert_has_calls([ + mock.call(["post-up", "eth1"]) + ]) + + @mock.patch('pyroute2.IPRoute.rule') + @mock.patch('pyroute2.IPRoute.route') + @mock.patch('pyroute2.IPRoute.addr') + @mock.patch('pyroute2.IPRoute.link') + @mock.patch('pyroute2.IPRoute.link_lookup') + @mock.patch('subprocess.check_output') + def test_up_auto(self, mock_check_output, mock_link_lookup, mock_link, + mock_addr, mock_route, mock_rule): + iface = interface_file.InterfaceFile( + name="eth1", + mtu=1450, + addresses=[{ + consts.DHCP: True, + consts.IPV6AUTO: True + }], + routes=[], + rules=[], + scripts={ + consts.IFACE_UP: [{ + consts.COMMAND: "post-up eth1" + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: "post-down eth1" + }], + }) + + idx = mock.MagicMock() + mock_link_lookup.return_value = [idx] + + controller = interface.InterfaceController() + controller.up(iface) + + mock_link.assert_called_once_with( + controller.SET, + index=idx, + state=consts.IFACE_UP, + mtu=1450) + + mock_addr.assert_not_called() + mock_route.assert_not_called() + mock_rule.assert_not_called() + + mock_check_output.assert_has_calls([ + mock.call(["/sbin/dhclient", + "-lf", + "/var/lib/dhclient/dhclient-{}.leases".format( + iface.name), + "-pf", + "/run/dhclient-{}.pid".format(iface.name), + iface.name], stderr=-2), + mock.call(["/sbin/sysctl", "-w", + "net.ipv6.conf.{}.accept_ra=2".format(iface.name)], + stderr=-2), + mock.call(["/sbin/sysctl", "-w", + "net.ipv6.conf.{}.autoconf=1".format(iface.name)], + stderr=-2), + mock.call(["post-up", iface.name]) + ]) + + @mock.patch('pyroute2.IPRoute.rule') + @mock.patch('pyroute2.IPRoute.route') + @mock.patch('pyroute2.IPRoute.addr') + @mock.patch('pyroute2.IPRoute.link') + @mock.patch('pyroute2.IPRoute.get_links') + @mock.patch('pyroute2.IPRoute.link_lookup') + @mock.patch('subprocess.check_output') + def test_down(self, mock_check_output, mock_link_lookup, mock_get_links, + mock_link, mock_addr, mock_route, mock_rule): + iface = interface_file.InterfaceFile( + name="eth1", + mtu=1450, + addresses=[{ + consts.ADDRESS: '1.2.3.4', + consts.PREFIXLEN: 24 + }, { + consts.ADDRESS: '10.2.3.4', + consts.PREFIXLEN: 16 + }, { + consts.ADDRESS: '2001:db8::3', + consts.PREFIXLEN: 64 + }], + routes=[{ + consts.DST: '10.0.0.0/8', + consts.GATEWAY: '1.0.0.1', + consts.TABLE: 10, + consts.ONLINK: True + }, { + consts.DST: '20.0.0.0/8', + consts.GATEWAY: '1.0.0.2', + consts.PREFSRC: '1.2.3.4', + consts.SCOPE: 'link' + }, { + consts.DST: '2001:db8:2::1/128', + consts.GATEWAY: '2001:db8::1' + }], + rules=[{ + consts.SRC: '1.1.1.1', + consts.SRC_LEN: 32, + consts.TABLE: 20, + }, { + consts.SRC: '2001:db8::1', + consts.SRC_LEN: 128, + consts.TABLE: 40, + }], + scripts={ + consts.IFACE_UP: [{ + consts.COMMAND: "post-up eth1" + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: "post-down eth1" + }], + }) + + idx = mock.MagicMock() + mock_link_lookup.return_value = [idx] + + mock_get_links.return_value = [{ + consts.STATE: consts.IFACE_UP + }] + + controller = interface.InterfaceController() + controller.down(iface) + + mock_link.assert_called_once_with( + controller.SET, + index=idx, + state=consts.IFACE_DOWN) + + mock_addr.assert_has_calls([ + mock.call(controller.DELETE, + index=idx, + address='1.2.3.4', + prefixlen=24, + family=socket.AF_INET), + mock.call(controller.DELETE, + index=idx, + address='10.2.3.4', + prefixlen=16, + family=socket.AF_INET), + mock.call(controller.DELETE, + index=idx, + address='2001:db8::3', + prefixlen=64, + family=socket.AF_INET6) + ]) + + mock_route.assert_has_calls([ + mock.call(controller.DELETE, + oif=idx, + dst='10.0.0.0/8', + gateway='1.0.0.1', + table=10, + onlink=True, + family=socket.AF_INET), + mock.call(controller.DELETE, + oif=idx, + dst='20.0.0.0/8', + gateway='1.0.0.2', + prefsrc='1.2.3.4', + scope='link', + family=socket.AF_INET), + mock.call(controller.DELETE, + oif=idx, + dst='2001:db8:2::1/128', + gateway='2001:db8::1', + family=socket.AF_INET6)]) + + mock_rule.assert_has_calls([ + mock.call(controller.DELETE, + src="1.1.1.1", + src_len=32, + table=20, + family=socket.AF_INET), + mock.call(controller.DELETE, + src="2001:db8::1", + src_len=128, + table=40, + family=socket.AF_INET6)]) + + mock_check_output.assert_has_calls([ + mock.call(["post-down", "eth1"]) + ]) + + @mock.patch('pyroute2.IPRoute.rule') + @mock.patch('pyroute2.IPRoute.route') + @mock.patch('pyroute2.IPRoute.addr') + @mock.patch('pyroute2.IPRoute.link') + @mock.patch('pyroute2.IPRoute.get_links') + @mock.patch('pyroute2.IPRoute.link_lookup') + @mock.patch('subprocess.check_output') + def test_down_with_errors(self, mock_check_output, mock_link_lookup, + mock_get_links, mock_link, mock_addr, + mock_route, mock_rule): + iface = interface_file.InterfaceFile( + name="eth1", + mtu=1450, + addresses=[{ + consts.ADDRESS: '1.2.3.4', + consts.PREFIXLEN: 24 + }, { + consts.ADDRESS: '10.2.3.4', + consts.PREFIXLEN: 16 + }, { + consts.ADDRESS: '2001:db8::3', + consts.PREFIXLEN: 64 + }], + routes=[{ + consts.DST: '10.0.0.0/8', + consts.GATEWAY: '1.0.0.1', + consts.TABLE: 10, + consts.ONLINK: True + }, { + consts.DST: '20.0.0.0/8', + consts.GATEWAY: '1.0.0.2', + consts.PREFSRC: '1.2.3.4', + consts.SCOPE: 'link' + }, { + consts.DST: '2001:db8:2::1/128', + consts.GATEWAY: '2001:db8::1' + }], + rules=[{ + consts.SRC: '1.1.1.1', + consts.SRC_LEN: 32, + consts.TABLE: 20, + }, { + consts.SRC: '2001:db8::1', + consts.SRC_LEN: 128, + consts.TABLE: 40, + }], + scripts={ + consts.IFACE_UP: [{ + consts.COMMAND: "post-up eth1" + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: "post-down eth1" + }], + }) + + idx = mock.MagicMock() + mock_link_lookup.return_value = [idx] + + mock_get_links.return_value = [{ + consts.STATE: consts.IFACE_UP + }] + mock_addr.side_effect = [ + pyroute2.NetlinkError(123), + pyroute2.NetlinkError(123), + pyroute2.NetlinkError(123) + ] + mock_route.side_effect = [ + pyroute2.NetlinkError(123), + pyroute2.NetlinkError(123), + pyroute2.NetlinkError(123) + ] + mock_rule.side_effect = [ + pyroute2.NetlinkError(123), + pyroute2.NetlinkError(123), + ] + mock_check_output.side_effect = [ + Exception() + ] + + controller = interface.InterfaceController() + controller.down(iface) + + mock_link.assert_called_once_with( + controller.SET, + index=idx, + state=consts.IFACE_DOWN) + + mock_addr.assert_has_calls([ + mock.call(controller.DELETE, + index=idx, + address='1.2.3.4', + prefixlen=24, + family=socket.AF_INET), + mock.call(controller.DELETE, + index=idx, + address='10.2.3.4', + prefixlen=16, + family=socket.AF_INET), + mock.call(controller.DELETE, + index=idx, + address='2001:db8::3', + prefixlen=64, + family=socket.AF_INET6) + ]) + + mock_route.assert_has_calls([ + mock.call(controller.DELETE, + oif=idx, + dst='10.0.0.0/8', + gateway='1.0.0.1', + table=10, + onlink=True, + family=socket.AF_INET), + mock.call(controller.DELETE, + oif=idx, + dst='20.0.0.0/8', + gateway='1.0.0.2', + prefsrc='1.2.3.4', + scope='link', + family=socket.AF_INET), + mock.call(controller.DELETE, + oif=idx, + dst='2001:db8:2::1/128', + gateway='2001:db8::1', + family=socket.AF_INET6)]) + + mock_rule.assert_has_calls([ + mock.call(controller.DELETE, + src="1.1.1.1", + src_len=32, + table=20, + family=socket.AF_INET), + mock.call(controller.DELETE, + src="2001:db8::1", + src_len=128, + table=40, + family=socket.AF_INET6)]) + + mock_check_output.assert_has_calls([ + mock.call(["post-down", "eth1"]) + ]) + + @mock.patch('pyroute2.IPRoute.rule') + @mock.patch('pyroute2.IPRoute.route') + @mock.patch('pyroute2.IPRoute.addr') + @mock.patch('pyroute2.IPRoute.link') + @mock.patch('pyroute2.IPRoute.get_links') + @mock.patch('pyroute2.IPRoute.link_lookup') + @mock.patch('subprocess.check_output') + def test_down_already_down(self, mock_check_output, mock_link_lookup, + mock_get_links, mock_link, mock_addr, + mock_route, mock_rule): + iface = interface_file.InterfaceFile( + name="eth1", + mtu=1450, + addresses=[{ + consts.ADDRESS: '1.2.3.4', + consts.PREFIXLEN: 24 + }, { + consts.ADDRESS: '10.2.3.4', + consts.PREFIXLEN: 16 + }, { + consts.ADDRESS: '2001:db8::3', + consts.PREFIXLEN: 64 + }], + routes=[{ + consts.DST: '10.0.0.0/8', + consts.GATEWAY: '1.0.0.1', + consts.TABLE: 10, + consts.ONLINK: True + }, { + consts.DST: '20.0.0.0/8', + consts.GATEWAY: '1.0.0.2', + consts.PREFSRC: '1.2.3.4', + consts.SCOPE: 'link' + }, { + consts.DST: '2001:db8:2::1/128', + consts.GATEWAY: '2001:db8::1' + }], + rules=[{ + consts.SRC: '1.1.1.1', + consts.SRC_LEN: 32, + consts.TABLE: 20, + }, { + consts.SRC: '2001:db8::1', + consts.SRC_LEN: 128, + consts.TABLE: 40, + }], + scripts={ + consts.IFACE_UP: [{ + consts.COMMAND: "post-up eth1" + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: "post-down eth1" + }], + }) + + idx = mock.MagicMock() + mock_link_lookup.return_value = [idx] + + mock_get_links.return_value = [{ + consts.STATE: consts.IFACE_DOWN + }] + + controller = interface.InterfaceController() + controller.down(iface) + + mock_link.assert_not_called() + mock_addr.assert_not_called() + mock_route.assert_not_called() + mock_rule.assert_not_called() + mock_check_output.assert_not_called() + + @mock.patch('pyroute2.IPRoute.rule') + @mock.patch('pyroute2.IPRoute.route') + @mock.patch('pyroute2.IPRoute.addr') + @mock.patch('pyroute2.IPRoute.link') + @mock.patch('pyroute2.IPRoute.get_links') + @mock.patch('pyroute2.IPRoute.link_lookup') + @mock.patch('subprocess.check_output') + def test_down_auto(self, mock_check_output, mock_link_lookup, + mock_get_links, mock_link, mock_addr, mock_route, + mock_rule): + iface = interface_file.InterfaceFile( + name="eth1", + mtu=1450, + addresses=[{ + consts.DHCP: True, + consts.IPV6AUTO: True + }], + routes=[], + rules=[], + scripts={ + consts.IFACE_UP: [{ + consts.COMMAND: "post-up eth1" + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: "post-down eth1" + }], + }) + + idx = mock.MagicMock() + mock_link_lookup.return_value = [idx] + + mock_get_links.return_value = [{ + consts.STATE: consts.IFACE_UP + }] + + controller = interface.InterfaceController() + controller.down(iface) + + mock_link.assert_called_once_with( + controller.SET, + index=idx, + state=consts.IFACE_DOWN) + + mock_addr.assert_not_called() + mock_route.assert_not_called() + mock_rule.assert_not_called() + + mock_check_output.assert_has_calls([ + mock.call(["/sbin/dhclient", + "-r", + "-lf", + "/var/lib/dhclient/dhclient-{}.leases".format( + iface.name), + "-pf", + "/run/dhclient-{}.pid".format(iface.name), + iface.name], stderr=-2), + mock.call(["/sbin/sysctl", "-w", + "net.ipv6.conf.{}.accept_ra=0".format(iface.name)], + stderr=-2), + mock.call(["/sbin/sysctl", "-w", + "net.ipv6.conf.{}.autoconf=0".format(iface.name)], + stderr=-2), + mock.call(["post-down", iface.name]) + ]) diff --git a/octavia/tests/unit/amphorae/backends/utils/test_interface_file.py b/octavia/tests/unit/amphorae/backends/utils/test_interface_file.py new file mode 100644 index 0000000000..85b294915e --- /dev/null +++ b/octavia/tests/unit/amphorae/backends/utils/test_interface_file.py @@ -0,0 +1,583 @@ +# Copyright 2020 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 ipaddress +from unittest import mock + +from octavia.amphorae.backends.utils import interface_file +from octavia.common import constants as consts +from octavia.tests.common import utils as test_utils +import octavia.tests.unit.base as base + + +class TestInterfaceFile(base.TestCase): + def test_vip_interface_file(self): + netns_interface = 'eth1234' + MTU = 1450 + VIP_ADDRESS = '192.0.2.2' + SUBNET_CIDR = '192.0.2.0/24' + GATEWAY = '192.0.2.1' + DEST1 = '198.51.100.0/24' + NEXTHOP = '192.0.2.1' + VRRP_IP_ADDRESS = '192.10.2.4' + TOPOLOGY = 'SINGLE' + + cidr = ipaddress.ip_network(SUBNET_CIDR) + prefixlen = cidr.prefixlen + + vip_interface_file = interface_file.VIPInterfaceFile( + name=netns_interface, + mtu=MTU, + vip=VIP_ADDRESS, + ip_version=cidr.version, + prefixlen=prefixlen, + gateway=GATEWAY, + vrrp_ip=VRRP_IP_ADDRESS, + host_routes=[ + {'destination': DEST1, 'nexthop': NEXTHOP} + ], + topology=TOPOLOGY) + + expected_dict = { + consts.NAME: netns_interface, + consts.MTU: MTU, + consts.ADDRESSES: [ + { + consts.ADDRESS: VRRP_IP_ADDRESS, + consts.PREFIXLEN: prefixlen + }, + { + consts.ADDRESS: VIP_ADDRESS, + consts.PREFIXLEN: prefixlen + } + ], + consts.ROUTES: [ + { + consts.DST: "0.0.0.0/0", + consts.GATEWAY: GATEWAY, + consts.FLAGS: [consts.ONLINK], + }, + { + consts.DST: "0.0.0.0/0", + consts.GATEWAY: GATEWAY, + consts.FLAGS: [consts.ONLINK], + consts.TABLE: 1 + }, + { + consts.DST: cidr.exploded, + consts.PREFSRC: VIP_ADDRESS, + consts.SCOPE: 'link', + consts.TABLE: 1 + }, + { + consts.DST: DEST1, + consts.GATEWAY: NEXTHOP, + consts.FLAGS: [consts.ONLINK] + }, + { + consts.DST: DEST1, + consts.GATEWAY: NEXTHOP, + consts.TABLE: 1, + consts.FLAGS: [consts.ONLINK] + } + ], + consts.RULES: [ + { + consts.SRC: VIP_ADDRESS, + consts.SRC_LEN: 32, + consts.TABLE: 1 + } + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "{}".format(netns_interface)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "{}".format(netns_interface)) + }] + } + } + + with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: + vip_interface_file.write() + + mock_dump.assert_called_once() + + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, expected_dict, args[0]) + + def test_vip_interface_file_dhcp(self): + netns_interface = 'eth1234' + MTU = 1450 + VIP_ADDRESS = '192.0.2.2' + SUBNET_CIDR = '192.0.2.0/24' + TOPOLOGY = 'SINGLE' + + cidr = ipaddress.ip_network(SUBNET_CIDR) + prefixlen = cidr.prefixlen + + vip_interface_file = interface_file.VIPInterfaceFile( + name=netns_interface, + mtu=MTU, + vip=VIP_ADDRESS, + ip_version=cidr.version, + prefixlen=prefixlen, + gateway=None, + vrrp_ip=None, + host_routes=[], + topology=TOPOLOGY) + + expected_dict = { + consts.NAME: netns_interface, + consts.MTU: MTU, + consts.ADDRESSES: [ + { + consts.DHCP: True + }, { + consts.ADDRESS: VIP_ADDRESS, + consts.PREFIXLEN: prefixlen + } + ], + consts.ROUTES: [ + { + consts.DST: cidr.exploded, + consts.PREFSRC: VIP_ADDRESS, + consts.SCOPE: 'link', + consts.TABLE: 1 + } + ], + consts.RULES: [ + { + consts.SRC: VIP_ADDRESS, + consts.SRC_LEN: 32, + consts.TABLE: 1 + } + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "{}".format(netns_interface)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "{}".format(netns_interface)) + }] + } + } + + with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: + vip_interface_file.write() + + mock_dump.assert_called_once() + + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, expected_dict, args[0]) + + def test_vip_interface_file_active_standby(self): + netns_interface = 'eth1234' + MTU = 1450 + VIP_ADDRESS = '192.0.2.2' + SUBNET_CIDR = '192.0.2.0/24' + GATEWAY = '192.0.2.1' + VRRP_IP_ADDRESS = '192.10.2.4' + TOPOLOGY = 'ACTIVE_STANDBY' + + cidr = ipaddress.ip_network(SUBNET_CIDR) + prefixlen = cidr.prefixlen + + vip_interface_file = interface_file.VIPInterfaceFile( + name=netns_interface, + mtu=MTU, + vip=VIP_ADDRESS, + ip_version=cidr.version, + prefixlen=prefixlen, + gateway=GATEWAY, + vrrp_ip=VRRP_IP_ADDRESS, + host_routes=[], + topology=TOPOLOGY) + + expected_dict = { + consts.NAME: netns_interface, + consts.MTU: MTU, + consts.ADDRESSES: [ + { + consts.ADDRESS: VRRP_IP_ADDRESS, + consts.PREFIXLEN: prefixlen + } + ], + consts.ROUTES: [ + { + consts.DST: "0.0.0.0/0", + consts.GATEWAY: GATEWAY, + consts.FLAGS: [consts.ONLINK], + }, + { + consts.DST: "0.0.0.0/0", + consts.GATEWAY: GATEWAY, + consts.FLAGS: [consts.ONLINK], + consts.TABLE: 1 + } + ], + consts.RULES: [], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "{}".format(netns_interface)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "{}".format(netns_interface)) + }] + } + } + + with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: + vip_interface_file.write() + + mock_dump.assert_called_once() + + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, expected_dict, args[0]) + + def test_vip_interface_file_ipv6(self): + netns_interface = 'eth1234' + MTU = 1450 + VIP_ADDRESS = '2001:db8::7' + SUBNET_CIDR = '2001:db8::/64' + GATEWAY = '2001:db8::1' + DEST1 = '2001:db8:2::/64' + NEXTHOP = '2001:db8:2::1' + VRRP_IP_ADDRESS = '2001:db8::42' + TOPOLOGY = 'SINGLE' + + cidr = ipaddress.ip_network(SUBNET_CIDR) + prefixlen = cidr.prefixlen + + vip_interface_file = interface_file.VIPInterfaceFile( + name=netns_interface, + mtu=MTU, + vip=VIP_ADDRESS, + ip_version=cidr.version, + prefixlen=prefixlen, + gateway=GATEWAY, + vrrp_ip=VRRP_IP_ADDRESS, + host_routes=[ + {'destination': DEST1, 'nexthop': NEXTHOP} + ], + topology=TOPOLOGY) + + expected_dict = { + consts.NAME: netns_interface, + consts.MTU: MTU, + consts.ADDRESSES: [ + { + consts.ADDRESS: VRRP_IP_ADDRESS, + consts.PREFIXLEN: prefixlen + }, + { + consts.ADDRESS: VIP_ADDRESS, + consts.PREFIXLEN: prefixlen + } + ], + consts.ROUTES: [ + { + consts.DST: "::/0", + consts.GATEWAY: GATEWAY, + consts.FLAGS: [consts.ONLINK] + }, + { + consts.DST: "::/0", + consts.GATEWAY: GATEWAY, + consts.TABLE: 1, + consts.FLAGS: [consts.ONLINK] + }, + { + consts.DST: cidr.exploded, + consts.PREFSRC: VIP_ADDRESS, + consts.SCOPE: 'link', + consts.TABLE: 1 + }, + { + consts.DST: DEST1, + consts.GATEWAY: NEXTHOP, + consts.FLAGS: [consts.ONLINK] + }, + { + consts.DST: DEST1, + consts.GATEWAY: NEXTHOP, + consts.TABLE: 1, + consts.FLAGS: [consts.ONLINK] + } + ], + consts.RULES: [ + { + consts.SRC: VIP_ADDRESS, + consts.SRC_LEN: 128, + consts.TABLE: 1 + } + ], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv6 " + "{}".format(netns_interface)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv6 " + "{}".format(netns_interface)) + }] + } + } + + with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: + vip_interface_file.write() + + mock_dump.assert_called_once() + + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, expected_dict, args[0]) + + def test_port_interface_file(self): + netns_interface = 'eth1234' + FIXED_IP = '192.0.2.2' + SUBNET_CIDR = '192.0.2.0/24' + DEST1 = '198.51.100.0/24' + DEST2 = '203.0.113.0/24' + DEST3 = 'fd01::/64' + NEXTHOP = '192.0.2.1' + NEXTHOP2 = '2001:db7::8' + MTU = 1450 + FIXED_IP_IPV6 = '2001:0db8:0000:0000:0000:0000:0000:0001' + SUBNET_CIDR_IPV6 = '2001:db8::/64' + fixed_ips = [{'ip_address': FIXED_IP, + 'subnet_cidr': SUBNET_CIDR, + 'host_routes': [ + {'destination': DEST1, 'nexthop': NEXTHOP}, + {'destination': DEST2, 'nexthop': NEXTHOP} + ]}, + {'ip_address': FIXED_IP_IPV6, + 'subnet_cidr': SUBNET_CIDR_IPV6, + 'host_routes': [ + {'destination': DEST3, 'nexthop': NEXTHOP2} + ]}, + ] + + port_interface_file = interface_file.PortInterfaceFile( + name=netns_interface, + fixed_ips=fixed_ips, + mtu=MTU) + + expected_dict = { + consts.NAME: netns_interface, + consts.MTU: MTU, + consts.ADDRESSES: [ + { + consts.ADDRESS: FIXED_IP, + consts.PREFIXLEN: ( + ipaddress.ip_network(SUBNET_CIDR).prefixlen + ) + }, + { + consts.ADDRESS: FIXED_IP_IPV6, + consts.PREFIXLEN: ( + ipaddress.ip_network(SUBNET_CIDR_IPV6).prefixlen + ) + } + ], + consts.ROUTES: [ + { + consts.DST: DEST1, + consts.GATEWAY: NEXTHOP + }, + { + consts.DST: DEST2, + consts.GATEWAY: NEXTHOP + }, + { + consts.DST: DEST3, + consts.GATEWAY: NEXTHOP2 + } + ], + consts.RULES: [], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "{}".format(netns_interface)) + }, { + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv6 " + "{}".format(netns_interface)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "{}".format(netns_interface)) + }, { + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv6 " + "{}".format(netns_interface)) + }] + } + } + + with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: + port_interface_file.write() + + mock_dump.assert_called_once() + + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, expected_dict, args[0]) + + def test_port_interface_file_dhcp(self): + netns_interface = 'eth1234' + MTU = 1450 + + port_interface_file = interface_file.PortInterfaceFile( + name=netns_interface, + fixed_ips=None, + mtu=MTU) + + expected_dict = { + consts.NAME: netns_interface, + consts.MTU: MTU, + consts.ADDRESSES: [{ + consts.DHCP: True, + consts.IPV6AUTO: True, + }], + consts.ROUTES: [], + consts.RULES: [], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv4 " + "{}".format(netns_interface)) + }, { + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add ipv6 " + "{}".format(netns_interface)) + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv4 " + "{}".format(netns_interface)) + }, { + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete ipv6 " + "{}".format(netns_interface)) + }] + } + } + + with mock.patch('os.open'), mock.patch('os.fdopen'), mock.patch( + 'octavia.amphorae.backends.utils.interface_file.' + 'InterfaceFile.dump') as mock_dump: + port_interface_file.write() + + mock_dump.assert_called_once() + + args = mock_dump.mock_calls[0][1] + test_utils.assert_interface_files_equal( + self, expected_dict, args[0]) + + def test_from_file(self): + filename = 'interface.json' + content = ('{"addresses": [\n' + '{"address": "10.0.0.181",\n' + '"prefixlen": 26}\n' + '],\n' + '"mtu": 1450,\n' + '"name": "eth1",\n' + '"routes": [\n' + '{"dst": "0.0.0.0/0",\n' + '"gateway": "10.0.0.129",\n' + '"onlink": true}\n' + '],\n' + '"rules": [\n' + '{"src": "10.0.0.157",\n' + '"src_len": 32,\n' + '"table": 1}\n' + '],\n' + '"scripts": {\n' + '"down": [\n' + '{"command": "script-down"}\n' + '], "up": [ \n' + '{"command": "script-up"}\n' + ']}}\n') + + self.useFixture( + test_utils.OpenFixture(filename, + contents=content)) + + iface = interface_file.InterfaceFile.from_file(filename) + + expected_dict = { + consts.NAME: "eth1", + consts.MTU: 1450, + consts.ADDRESSES: [{ + consts.ADDRESS: "10.0.0.181", + consts.PREFIXLEN: 26 + }], + consts.ROUTES: [{ + consts.DST: "0.0.0.0/0", + consts.GATEWAY: "10.0.0.129", + consts.FLAGS: [consts.ONLINK] + }], + consts.RULES: [{ + consts.SRC: "10.0.0.157", + consts.SRC_LEN: 32, + consts.TABLE: 1 + }], + consts.SCRIPTS: { + consts.IFACE_UP: [{ + consts.COMMAND: "script-up" + }], + consts.IFACE_DOWN: [{ + consts.COMMAND: "script-down" + }] + } + } + + self.assertEqual(expected_dict[consts.NAME], iface.name) + self.assertEqual(expected_dict[consts.MTU], iface.mtu) + test_utils.assert_address_lists_equal( + self, expected_dict[consts.ADDRESSES], iface.addresses) + test_utils.assert_rule_lists_equal( + self, expected_dict[consts.RULES], iface.rules) + test_utils.assert_script_lists_equal( + self, expected_dict[consts.SCRIPTS], iface.scripts) diff --git a/octavia/tests/unit/cmd/test_interface.py b/octavia/tests/unit/cmd/test_interface.py new file mode 100644 index 0000000000..fde2498a06 --- /dev/null +++ b/octavia/tests/unit/cmd/test_interface.py @@ -0,0 +1,137 @@ +# Copyright 2021 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. +from unittest import mock + +from octavia.amphorae.backends.utils import interface_file +from octavia.cmd import interface +from octavia.tests.unit import base + + +class TestInterfaceCMD(base.TestCase): + + def setUp(self): + super().setUp() + + self.interface1 = interface_file.InterfaceFile("eth1") + self.interface2 = interface_file.InterfaceFile("eth2") + + def test_interfaces_find(self): + controller = mock.Mock() + controller.list = mock.Mock() + controller.list.return_value = { + "eth1": self.interface1, + "eth2": self.interface2 + } + + ret = interface.interfaces_find(controller, "eth2") + self.assertCountEqual([self.interface2], ret) + controller.list.assert_called_once() + + def test_interfaces_find_all(self): + controller = mock.Mock() + controller.list = mock.Mock() + controller.list.return_value = { + "eth1": self.interface1, + "eth2": self.interface2 + } + + ret = interface.interfaces_find(controller, "all") + self.assertCountEqual([self.interface1, self.interface2], ret) + controller.list.assert_called_once() + + def test_interfaces_find_all_empty(self): + controller = mock.Mock() + controller.list = mock.Mock() + controller.list.return_value = {} + + ret = interface.interfaces_find(controller, "all") + self.assertEqual(0, len(ret)) + controller.list.assert_called_once() + + def test_interfaces_find_not_found(self): + controller = mock.Mock() + controller.list = mock.Mock() + controller.list.return_value = { + "eth1": self.interface1, + "eth2": self.interface2 + } + + self.assertRaisesRegex( + interface.InterfaceException, + "Could not find interface 'eth3'.", + interface.interfaces_find, + controller, "eth3") + controller.list.assert_called_once() + + def test_interfaces_update(self): + action_fn = mock.Mock() + action_str = mock.Mock() + interfaces = [self.interface1, self.interface2] + + interface.interfaces_update(interfaces, action_fn, action_str) + self.assertEqual(2, len(action_fn.mock_calls)) + action_fn.assert_called_with(self.interface2) + + def test_interfaces_update_with_errors(self): + action_fn = mock.Mock() + action_str = mock.Mock() + interfaces = [self.interface1, self.interface2] + action_fn.side_effect = [None, Exception("error msg")] + + self.assertRaisesRegex( + interface.InterfaceException, + "Could not configure interface:.*eth2.*error msg", + interface.interfaces_update, + interfaces, action_fn, action_str) + self.assertEqual(2, len(action_fn.mock_calls)) + + @mock.patch("octavia.amphorae.backends.utils.interface." + "InterfaceController") + @mock.patch("octavia.cmd.interface.interfaces_find") + @mock.patch("octavia.cmd.interface.interfaces_update") + def test_interface_cmd(self, mock_interfaces_update, + mock_interfaces_find, mock_controller): + controller = mock.Mock() + controller.up = mock.Mock() + controller.down = mock.Mock() + mock_controller.return_value = controller + mock_interfaces_find.return_value = [self.interface1] + + interface.interface_cmd("eth1", "up") + + mock_interfaces_find.assert_called_once_with( + controller, "eth1") + mock_interfaces_update.assert_called_once_with( + [self.interface1], mock_controller.return_value.up, "up") + + mock_interfaces_find.reset_mock() + mock_interfaces_update.reset_mock() + + mock_interfaces_find.return_value = [self.interface2] + + interface.interface_cmd("eth2", "down") + + mock_interfaces_find.assert_called_once_with( + controller, "eth2") + mock_interfaces_update.assert_called_once_with( + [self.interface2], mock_controller.return_value.down, "down") + + @mock.patch("octavia.amphorae.backends.utils.interface." + "InterfaceController") + def test_interface_cmd_invalid_action(self, mock_controller): + self.assertRaisesRegex( + interface.InterfaceException, + "Unknown action.*invalidaction", + interface.interface_cmd, + "eth1", "invalidaction") diff --git a/releasenotes/notes/amphora-network-interface-management-d77bc9905ed997f6.yaml b/releasenotes/notes/amphora-network-interface-management-d77bc9905ed997f6.yaml new file mode 100644 index 0000000000..26b710d9a1 --- /dev/null +++ b/releasenotes/notes/amphora-network-interface-management-d77bc9905ed997f6.yaml @@ -0,0 +1,13 @@ +--- +deprecations: + - | + The ``[amphora_agent].agent_server_network_file`` configuration option is + now deprecated, the new Amphora network configuration tool introduced in + Xena does not support a single configuration file. +fixes: + - | + Amphora network configuration for the VIP interface and the pool member + interfaces are now applied with the amphora-interface tool. + amphora-interface uses pyroute2 low-level functions to configure the + interfaces instead of distribution-specific tools such as "network-scripts" + or "/etc/network/interfaces" files. diff --git a/setup.cfg b/setup.cfg index 6cd7a4ebd9..c8f443502e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,7 @@ console_scripts = haproxy-vrrp-check = octavia.cmd.haproxy_vrrp_check:main octavia-status = octavia.cmd.status:main amphora-health-checker = octavia.cmd.health_checker:main + amphora-interface = octavia.cmd.interface:main octavia.api.drivers = noop_driver = octavia.api.drivers.noop_driver.driver:NoopProviderDriver noop_driver-alt = octavia.api.drivers.noop_driver.driver:NoopProviderDriver