diff --git a/octavia/amphorae/backends/agent/api_server/osutils.py b/octavia/amphorae/backends/agent/api_server/osutils.py index 008ed59dfa..f808a71338 100644 --- a/octavia/amphorae/backends/agent/api_server/osutils.py +++ b/octavia/amphorae/backends/agent/api_server/osutils.py @@ -87,7 +87,7 @@ class BaseOS(object): port_interface.write() @classmethod - def _bring_if_up(cls, interface, what): + def bring_interface_up(cls, interface, name): cmd = ("ip netns exec {ns} amphora-interface up {params}".format( ns=consts.AMPHORA_NAMESPACE, params=interface)) LOG.debug("Executing: %s", cmd) @@ -100,25 +100,9 @@ class BaseOS(object): e, e.output) raise exceptions.HTTPException( response=webob.Response(json=dict( - message='Error plugging {0}'.format(what), + message='Error plugging {0}'.format(name), details=e.output), status=500)) - @classmethod - def _bring_if_down(cls, interface): - 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 set %s down due to error: %s %s', - interface, e, e.output) - - @classmethod - def bring_interfaces_up(cls, ip, primary_interface): - cls._bring_if_down(primary_interface) - cls._bring_if_up(primary_interface, 'VIP') - class Ubuntu(BaseOS): diff --git a/octavia/amphorae/backends/agent/api_server/plug.py b/octavia/amphorae/backends/agent/api_server/plug.py index 3864359e04..8b30eb4ee1 100644 --- a/octavia/amphorae/backends/agent/api_server/plug.py +++ b/octavia/amphorae/backends/agent/api_server/plug.py @@ -117,7 +117,7 @@ class Plug(object): IFLA_IFNAME=primary_interface) # bring interfaces up - self._osutils.bring_interfaces_up(ip, primary_interface) + self._osutils.bring_interface_up(primary_interface, 'VIP') return webob.Response(json=dict( message="OK", @@ -179,8 +179,7 @@ class Plug(object): net_ns_fd=consts.AMPHORA_NAMESPACE, IFLA_IFNAME=netns_interface) - self._osutils._bring_if_down(netns_interface) - self._osutils._bring_if_up(netns_interface, 'network') + self._osutils.bring_interface_up(netns_interface, 'network') return webob.Response(json=dict( message="OK", diff --git a/octavia/amphorae/backends/utils/interface.py b/octavia/amphorae/backends/utils/interface.py index 04090dbdef..a6396c4f3c 100644 --- a/octavia/amphorae/backends/utils/interface.py +++ b/octavia/amphorae/backends/utils/interface.py @@ -36,6 +36,7 @@ class InterfaceController(object): ADD = 'add' DELETE = 'delete' SET = 'set' + FLUSH = 'flush' def interface_file_list(self): net_dir = interface_file.InterfaceFile.get_directory() @@ -57,7 +58,7 @@ class InterfaceController(object): if ipaddress.ip_network(address, strict=False).version == 6 else socket.AF_INET) - def _ipr_command(self, method, command, + def _ipr_command(self, method, *args, retry_on_invalid_argument=False, retry_interval=.2, raise_on_error=True, @@ -66,24 +67,26 @@ class InterfaceController(object): for dummy in range(max_retries + 1): try: - method(command, **kwargs) + method(*args, **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) + LOG.debug("Retrying after %f 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) + if args: + command = args[0] + 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) + method.__name__, args, kwargs, max_retries) if raise_on_error: raise exceptions.AmphoraNetworkConfigException(msg) LOG.error(msg) @@ -134,55 +137,222 @@ class InterfaceController(object): LOG.debug("Running '%s'", cmd) subprocess.check_output(cmd, stderr=subprocess.STDOUT) + def _normalize_ip_address(self, address): + if not address: + return None + ip_address = ipaddress.ip_address(address) + return ip_address.compressed + + def _normalize_ip_network(self, address): + if not address: + return None + ip_network = ipaddress.ip_network(address, strict=False) + return ip_network.compressed + 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) + link = ipr.get_links(idx)[0] + current_state = link.get(consts.STATE) + has_dynamic_addr = False + + if current_state == consts.IFACE_DOWN: + self._ipr_command(ipr.link, self.SET, index=idx, + state=consts.IFACE_UP, mtu=interface.mtu) + + for address in interface.addresses: + if address.get(consts.DHCP): + has_dynamic_addr = True + self._dhclient_up(interface.name) + if address.get(consts.IPV6AUTO): + has_dynamic_addr = True + self._ipv6auto_up(interface.name) + + # Addresses + # Get existing addresses + current_addresses = [ + (self._normalize_ip_address( + dict(addr['attrs'])['IFA_ADDRESS']), + addr['prefixlen']) + for addr in ipr.get_addr(index=idx)] + + # Add new addresses 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) + key = (self._normalize_ip_address(address.get(consts.ADDRESS)), + address.get(consts.PREFIXLEN)) + if key in current_addresses: + current_addresses.remove(key) + elif not address.get(consts.IGNORE): + 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) + # TODO(gthiemonge): find a smarter way to skip DHCP/AUTO addresses + if not has_dynamic_addr: + # Remove unused addresses + for addr, prefixlen in current_addresses: + address = { + consts.ADDRESS: addr, + consts.PREFIXLEN: prefixlen, + consts.FAMILY: self._family(addr) + } + LOG.debug("%s: Deleting address %s", interface.name, + address) + self._ipr_command(ipr.addr, self.DELETE, index=idx, + **address) + + # Routes + # Get existing routes + current_routes = [] + for route in ipr.get_routes(oif=idx): + attrs = dict(route['attrs']) + # Disabling B104: hardcoded_bind_all_interfaces + dst = attrs.get( + 'RTA_DST', + '0.0.0.0' if route['family'] == 2 else '::') # nosec + + key = ("{}/{}".format(self._normalize_ip_address(dst), + route.get('dst_len', 0)), + self._normalize_ip_address(attrs.get('RTA_GATEWAY')), + self._normalize_ip_address(attrs.get('RTA_PREFSRC')), + attrs.get('RTA_TABLE')) + current_routes.append(key) + + # Add new routes 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) + key = (self._normalize_ip_network(route.get('dst')), + self._normalize_ip_address(route.get('gateway')), + self._normalize_ip_address(route.get('prefsrc')), + route.get('table', 254)) + if key in current_routes: + current_routes.remove(key) + elif not route.get(consts.IGNORE): + 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) + if not has_dynamic_addr: + # Remove mandatory/auto/default routes from the list of routes + # to remove + for addr in interface.addresses: + if consts.ADDRESS not in address: + continue + + ip_addr = self._normalize_ip_address( + addr.get(consts.ADDRESS)) + net = ipaddress.ip_network( + "{}/{}".format(ip_addr, addr.get(consts.PREFIXLEN)), + strict=False) + + max_prefixlen = 32 if net.version == 4 else 128 + + for prefsrc in (None, ip_addr): + # Multicast route + key = ('ff00::/8', None, prefsrc, 255) + if key in current_routes: + current_routes.remove(key) + + # Link local route + key = ('fe80::/64', None, prefsrc, 254) + if key in current_routes: + current_routes.remove(key) + + # Network route + key = (net.compressed, None, prefsrc, 254) + if key in current_routes: + current_routes.remove(key) + + for base in (net[0].compressed, net[-1].compressed, + ip_addr): + key = ("{}/{}".format(base, max_prefixlen), + None, prefsrc, 255) + if key in current_routes: + current_routes.remove(key) + + # Delete unused routes + for r in current_routes: + route = {'dst': r[0], + 'gateway': r[1], + 'prefsrc': r[2], + 'table': r[3]} + route[consts.FAMILY] = self._family(route[consts.DST]) + + LOG.debug("%s: Deleting route %s", interface.name, route) + self._ipr_command(ipr.route, self.DELETE, + retry_on_invalid_argument=True, + raise_on_error=False, + oif=idx, **route) + + # Rules + # Get existing rules + current_rules = [] + for rule in ipr.get_rules(): + attrs = dict(rule['attrs']) + if not attrs.get('FRA_SRC'): + continue + + # FRA_PROTOCOL == 18 means that the rule was set by keepalived, + # skip this rule + if attrs.get('FRA_PROTOCOL') == 18: + continue + + key = (attrs.get('FRA_TABLE'), + attrs.get('FRA_SRC'), + rule['src_len']) + print(attrs) + current_rules.append(key) + + # Add new rules 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) + key = (rule.get('table', 254), + rule.get('src'), + rule.get('src_len')) + if key in current_rules: + current_rules.remove(key) + elif not rule.get(consts.IGNORE): + 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()) + if not has_dynamic_addr: + # Remove old rules + for r in current_rules: + rule = {'table': r[0], + 'src': r[1], + 'src_len': r[2]} + if rule[consts.SRC]: + rule[consts.FAMILY] = self._family(rule[consts.SRC]) + LOG.debug("%s: Deleting rule %s", interface.name, rule) + self._ipr_command(ipr.rule, self.DELETE, + retry_on_invalid_argument=True, + **rule) + + if current_state == consts.IFACE_DOWN: + 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) @@ -223,6 +393,9 @@ class InterfaceController(object): raise_on_error=False, index=idx, **address) + self._ipr_command(ipr.flush_addr, raise_on_error=False, + index=idx) + self._ipr_command(ipr.link, self.SET, raise_on_error=False, index=idx, state=consts.IFACE_DOWN) diff --git a/octavia/amphorae/backends/utils/interface_file.py b/octavia/amphorae/backends/utils/interface_file.py index bb650ecc5c..d83597f1cb 100644 --- a/octavia/amphorae/backends/utils/interface_file.py +++ b/octavia/amphorae/backends/utils/interface_file.py @@ -106,7 +106,7 @@ class VIPInterfaceFile(InterfaceFile): def __init__(self, name, mtu, vip, ip_version, prefixlen, gateway, vrrp_ip, host_routes, - topology): + topology, fixed_ips=None): super().__init__(name, mtu=mtu) @@ -137,41 +137,75 @@ class VIPInterfaceFile(InterfaceFile): 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, - }) + # In ACTIVE_STANDBY topology, keepalived sets some addresses, routes + # and rules. + # Keep track of those resources in the interface file but mark them + # with a special flag so the amphora-interface would not add/delete + # keepalived-maintained things. + ignore = topology == consts.TOPOLOGY_ACTIVE_STANDBY + + if ignore: + # Keepalived sets this prefixlen for the addresses it maintains + vip_prefixlen = 32 if ip_version == 4 else 128 + else: + vip_prefixlen = prefixlen + + self.addresses.append({ + consts.ADDRESS: vip, + consts.PREFIXLEN: vip_prefixlen, + consts.IGNORE: ignore + }) + 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, + consts.IGNORE: ignore + }) + self.rules.append({ + consts.SRC: vip, + consts.SRC_LEN: 128 if ip_version == 6 else 32, + consts.TABLE: 1, + consts.IGNORE: ignore + }) 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)) - }) + ip_versions = {ip_version} + + if fixed_ips: + 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) + + for ip_v in ip_versions: + self.scripts[consts.IFACE_UP].append({ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh add {} {}".format( + 'ipv6' if ip_v == 6 else 'ipv4', name)) + }) + self.scripts[consts.IFACE_DOWN].append({ + consts.COMMAND: ( + "/usr/local/bin/lvs-masquerade.sh delete {} {}".format( + 'ipv6' if ip_v == 6 else 'ipv4', name)) + }) class PortInterfaceFile(InterfaceFile): diff --git a/octavia/common/constants.py b/octavia/common/constants.py index 6240b5718e..a698b4716e 100644 --- a/octavia/common/constants.py +++ b/octavia/common/constants.py @@ -925,5 +925,7 @@ IFACE_DOWN = 'down' COMMAND = 'command' +IGNORE = 'ignore' + # Amphora network directory AMP_NET_DIR_TEMPLATE = '/etc/octavia/interfaces/' diff --git a/octavia/tests/common/utils.py b/octavia/tests/common/utils.py index 61f2efd21c..140031cd59 100644 --- a/octavia/tests/common/utils.py +++ b/octavia/tests/common/utils.py @@ -77,6 +77,8 @@ def assert_route_lists_equal(obj, l1, l2): ipaddress.ip_address(r2[consts.PREFSRC])) for attr in (consts.ONLINK, consts.TABLE, consts.SCOPE): obj.assertEqual(r1.get(attr), r2.get(attr)) + obj.assertEqual(r1.get(consts.IGNORE, False), + r2.get(consts.IGNORE, False)) def assert_rule_lists_equal(obj, l1, l2): 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 a40f3e7dba..59fab933d1 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 @@ -1835,7 +1835,6 @@ class TestServerTestCase(base.TestCase): consts.NETNS_PRIMARY_INTERFACE], stderr=-2) mock_check_output.side_effect = [ - 'unplug1', subprocess.CalledProcessError( 7, 'test', RANDOM_ERROR), subprocess.CalledProcessError( 7, 'test', RANDOM_ERROR)] @@ -2196,7 +2195,6 @@ class TestServerTestCase(base.TestCase): 'amphora-interface', 'up', '{netns_int}'.format( netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2) mock_check_output.side_effect = [ - 'unplug1', subprocess.CalledProcessError( 7, 'test', RANDOM_ERROR), subprocess.CalledProcessError( 7, 'test', RANDOM_ERROR)] diff --git a/octavia/tests/unit/amphorae/backends/utils/test_interface.py b/octavia/tests/unit/amphorae/backends/utils/test_interface.py index ae5d8157c0..d6a96072f5 100644 --- a/octavia/tests/unit/amphorae/backends/utils/test_interface.py +++ b/octavia/tests/unit/amphorae/backends/utils/test_interface.py @@ -324,10 +324,11 @@ class TestInterface(base.TestCase): @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_up(self, mock_check_output, mock_link_lookup, mock_link, - mock_addr, mock_route, mock_rule): + def test_up(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, @@ -376,6 +377,10 @@ class TestInterface(base.TestCase): idx = mock.MagicMock() mock_link_lookup.return_value = [idx] + mock_get_links.return_value = [{ + consts.STATE: consts.IFACE_DOWN + }] + controller = interface.InterfaceController() controller.up(iface) @@ -444,10 +449,220 @@ class TestInterface(base.TestCase): @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.get_rules') + @mock.patch('pyroute2.IPRoute.get_routes') + @mock.patch('pyroute2.IPRoute.get_addr') @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): + def test_up_update(self, mock_check_output, mock_link_lookup, + mock_get_addr, mock_get_routes, mock_get_rules, + 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: '10.4.3.2', + consts.PREFIXLEN: 16, + consts.IGNORE: True + }, { + 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:3::1/64', + consts.GATEWAY: '2001:db8::1', + consts.IGNORE: True + }, { + 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_get_addr.return_value = [{ + 'prefixlen': 24, + 'attrs': { + 'IFA_ADDRESS': '2.0.0.1' + } + }, { + 'prefixlen': 16, + 'attrs': { + 'IFA_ADDRESS': '10.2.3.4' + } + }] + + mock_get_routes.return_value = [{ + 'dst_len': 16, + 'family': 2, + 'attrs': { + 'RTA_DST': '10.2.0.0', + 'RTA_GATEWAY': None, + 'RTA_PREFSRC': '10.2.3.4', + 'RTA_TABLE': 254 + } + }, { + 'dst_len': 32, + 'family': 2, + 'attrs': { + 'RTA_DST': '10.2.3.4', + 'RTA_GATEWAY': None, + 'RTA_PREFSRC': '10.2.3.4', + 'RTA_TABLE': 255 + } + }, { + 'dst_len': 16, + 'family': 2, + 'attrs': { + 'RTA_DST': '24.24.0.0', + 'RTA_GATEWAY': '2.0.0.254', + 'RTA_PREFSRC': '2.0.0.1', + 'RTA_TABLE': 254 + } + }, { + 'dst_len': 8, + 'family': 2, + 'attrs': { + 'RTA_DST': '20.0.0.0', + 'RTA_GATEWAY': '1.0.0.2', + 'RTA_PREFSRC': '1.2.3.4', + 'RTA_TABLE': 254 + } + }] + + mock_get_rules.return_value = [{ + 'src_len': 32, + 'attrs': { + 'FRA_SRC': '1.1.1.1', + 'FRA_TABLE': 20, + 'FRA_PROTOCOL': 0 + } + }, { + 'src_len': 32, + 'attrs': { + 'FRA_SRC': '2.2.2.2', + 'FRA_TABLE': 254, + 'FRA_PROTOCOL': 18 # Keepalived + } + }, { + 'src_len': 32, + 'attrs': { + 'FRA_SRC': '3.3.3.3', + 'FRA_TABLE': 254, + 'FRA_PROTOCOL': 0 + } + }] + + controller = interface.InterfaceController() + controller.up(iface) + + mock_link.assert_not_called() + + 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='2001:db8::3', + prefixlen=64, + family=socket.AF_INET6), + mock.call(controller.DELETE, + index=idx, + address='2.0.0.1', + prefixlen=24, + family=socket.AF_INET) + ]) + + 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='2001:db8:2::1/128', + gateway='2001:db8::1', + family=socket.AF_INET6), + mock.call(controller.DELETE, + oif=idx, + dst='24.24.0.0/16', + gateway='2.0.0.254', + prefsrc='2.0.0.1', + table=254, + family=socket.AF_INET)]) + + mock_rule.assert_has_calls([ + mock.call(controller.ADD, + src="2001:db8::1", + src_len=128, + table=40, + family=socket.AF_INET6), + mock.call(controller.DELETE, + src='3.3.3.3', + src_len=32, + table=254, + family=socket.AF_INET)]) + + 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.get_rules') + @mock.patch('pyroute2.IPRoute.get_routes') + @mock.patch('pyroute2.IPRoute.get_addr') + @mock.patch('pyroute2.IPRoute.link_lookup') + @mock.patch('subprocess.check_output') + def test_up_auto(self, mock_check_output, mock_link_lookup, mock_get_addr, + mock_get_routes, mock_get_rules, mock_get_links, + mock_link, mock_addr, mock_route, mock_rule): iface = interface_file.InterfaceFile( name="eth1", mtu=1450, @@ -469,6 +684,10 @@ class TestInterface(base.TestCase): idx = mock.MagicMock() mock_link_lookup.return_value = [idx] + mock_get_links.return_value = [{ + consts.STATE: consts.IFACE_DOWN + }] + controller = interface.InterfaceController() controller.up(iface) @@ -626,13 +845,14 @@ class TestInterface(base.TestCase): @mock.patch('pyroute2.IPRoute.rule') @mock.patch('pyroute2.IPRoute.route') @mock.patch('pyroute2.IPRoute.addr') + @mock.patch('pyroute2.IPRoute.flush_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): + mock_get_links, mock_link, mock_flush_addr, + mock_addr, mock_route, mock_rule): iface = interface_file.InterfaceFile( name="eth1", mtu=1450, @@ -687,6 +907,9 @@ class TestInterface(base.TestCase): mock_addr.side_effect = [ pyroute2.NetlinkError(123), pyroute2.NetlinkError(123), + pyroute2.NetlinkError(123), + ] + mock_flush_addr.side_effect = [ pyroute2.NetlinkError(123) ] mock_route.side_effect = [ @@ -728,6 +951,10 @@ class TestInterface(base.TestCase): family=socket.AF_INET6) ]) + mock_flush_addr.assert_has_calls([ + mock.call(index=idx) + ]) + mock_route.assert_has_calls([ mock.call(controller.DELETE, oif=idx, @@ -839,13 +1066,14 @@ class TestInterface(base.TestCase): @mock.patch('pyroute2.IPRoute.rule') @mock.patch('pyroute2.IPRoute.route') @mock.patch('pyroute2.IPRoute.addr') + @mock.patch('pyroute2.IPRoute.flush_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): + mock_get_links, mock_link, mock_flush_addr, + mock_addr, mock_route, mock_rule): iface = interface_file.InterfaceFile( name="eth1", mtu=1450, @@ -883,6 +1111,8 @@ class TestInterface(base.TestCase): mock_route.assert_not_called() mock_rule.assert_not_called() + mock_flush_addr.assert_called_once() + mock_check_output.assert_has_calls([ mock.call(["/sbin/dhclient", "-r", @@ -900,3 +1130,49 @@ class TestInterface(base.TestCase): stderr=-2), mock.call(["post-down", iface.name]) ]) + + def test__normalize_ip_address(self): + controller = interface.InterfaceController() + + # Simple IPv4 address + addr = controller._normalize_ip_address('192.168.0.1') + self.assertEqual('192.168.0.1', addr) + + # Simple IPv6 address + addr = controller._normalize_ip_address('2001::1') + self.assertEqual('2001::1', addr) + + # Uncompressed IPv6 address + addr = controller._normalize_ip_address( + '2001:0000:0000:0000:0000:0000:0000:0001') + self.assertEqual('2001::1', addr) + + addr = controller._normalize_ip_address(None) + self.assertIsNone(addr) + + def test__normalize_ip_network(self): + controller = interface.InterfaceController() + + # Simple IP address + addr = controller._normalize_ip_network('192.168.0.1') + self.assertEqual('192.168.0.1/32', addr) + + # "Normal" network + addr = controller._normalize_ip_network('10.0.0.0/16') + self.assertEqual('10.0.0.0/16', addr) + + # Network with hostbits set + addr = controller._normalize_ip_network('10.0.0.10/16') + self.assertEqual('10.0.0.0/16', addr) + + # IPv6 network with hostbits set + addr = controller._normalize_ip_network('2001::1/64') + self.assertEqual('2001::/64', addr) + + # Uncompressed IPv6 network + addr = controller._normalize_ip_network( + '2001:0000:0000:0000:0000:0000:0000:0001/64') + self.assertEqual('2001::/64', addr) + + addr = controller._normalize_ip_network(None) + self.assertIsNone(addr) diff --git a/octavia/tests/unit/amphorae/backends/utils/test_interface_file.py b/octavia/tests/unit/amphorae/backends/utils/test_interface_file.py index 85b294915e..99754dd24a 100644 --- a/octavia/tests/unit/amphorae/backends/utils/test_interface_file.py +++ b/octavia/tests/unit/amphorae/backends/utils/test_interface_file.py @@ -124,6 +124,129 @@ class TestInterfaceFile(base.TestCase): test_utils.assert_interface_files_equal( self, expected_dict, args[0]) + def test_vip_interface_file_with_fixed_ips(self): + netns_interface = 'eth1234' + MTU = 1450 + VIP_ADDRESS = '192.0.2.2' + FIXED_IP = '10.0.0.1' + SUBNET_CIDR = '192.0.2.0/24' + SUBNET2_CIDR = '10.0.0.0/16' + GATEWAY = '192.0.2.1' + DEST1 = '198.51.100.0/24' + DEST2 = '203.0.113.0/24' + NEXTHOP = '192.0.2.1' + NEXTHOP2 = '10.0.1.254' + VRRP_IP_ADDRESS = '192.10.2.4' + TOPOLOGY = 'SINGLE' + + cidr = ipaddress.ip_network(SUBNET_CIDR) + prefixlen = cidr.prefixlen + + cidr2 = ipaddress.ip_network(SUBNET2_CIDR) + prefixlen2 = cidr2.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} + ], + fixed_ips=[{'ip_address': FIXED_IP, + 'subnet_cidr': SUBNET2_CIDR, + 'host_routes': [ + {'destination': DEST2, 'nexthop': NEXTHOP2} + ]}], + 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.ADDRESS: FIXED_IP, + consts.PREFIXLEN: prefixlen2 + }, + ], + 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.DST: DEST2, + consts.GATEWAY: NEXTHOP2, + 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 @@ -226,6 +349,11 @@ class TestInterfaceFile(base.TestCase): { consts.ADDRESS: VRRP_IP_ADDRESS, consts.PREFIXLEN: prefixlen + }, + { + consts.ADDRESS: VIP_ADDRESS, + consts.PREFIXLEN: 32, + consts.IGNORE: True } ], consts.ROUTES: [ @@ -239,9 +367,23 @@ class TestInterfaceFile(base.TestCase): consts.GATEWAY: GATEWAY, consts.FLAGS: [consts.ONLINK], consts.TABLE: 1 + }, + { + consts.DST: SUBNET_CIDR, + consts.PREFSRC: VIP_ADDRESS, + consts.SCOPE: 'link', + consts.TABLE: 1, + consts.IGNORE: True + } + ], + consts.RULES: [ + { + consts.SRC: VIP_ADDRESS, + consts.SRC_LEN: 32, + consts.TABLE: 1, + consts.IGNORE: True } ], - consts.RULES: [], consts.SCRIPTS: { consts.IFACE_UP: [{ consts.COMMAND: (