Reconfigure amphora network interfaces seamlessly
Update amphora-interface app to allow reconfiguration of the network interfaces without having to set them down first. IP addresses/routes maintained by keepalived in Active/Standby topology are now explicitly defined and marked as 'ignored' in the interface files, allowing amphora-interface to not consider them as unused addresses. Add fixed_ips support on the VIP port, enabling the possibility to set up many ip addresses on the VIP port Story: 2005608 Task: 30847 Change-Id: If0b211b3ef4fce3b7a58823fc8d1e48448e4d346 (cherry picked from commit7118e8d56f
) (cherry picked from commitd75613fab2
)
This commit is contained in:
parent
28a6f28702
commit
c513c9e77a
@ -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):
|
||||
|
||||
|
@ -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",
|
||||
|
@ -24,6 +24,8 @@ from oslo_log import log as logging
|
||||
import pyroute2
|
||||
# pylint: disable=no-name-in-module
|
||||
from pyroute2.netlink.rtnl import ifaddrmsg
|
||||
# pylint: disable=no-name-in-module
|
||||
from pyroute2.netlink.rtnl import rt_proto
|
||||
|
||||
from octavia.amphorae.backends.utils import interface_file
|
||||
from octavia.common import constants as consts
|
||||
@ -38,6 +40,7 @@ class InterfaceController(object):
|
||||
ADD = 'add'
|
||||
DELETE = 'delete'
|
||||
SET = 'set'
|
||||
FLUSH = 'flush'
|
||||
|
||||
TENTATIVE_WAIT_INTERVAL = .2
|
||||
TENTATIVE_WAIT_TIMEOUT = 30
|
||||
@ -62,7 +65,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,
|
||||
@ -71,24 +74,30 @@ 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:
|
||||
args_str = ', '.join(str(a) for a in args)
|
||||
kwargs_str = ', '.join(
|
||||
f'{k}={v}' for k, v in kwargs.items()
|
||||
)
|
||||
msg = (f"Cannot call {method.__name__} {command} "
|
||||
f"with ({args_str}, {kwargs_str}): {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)
|
||||
@ -146,7 +155,7 @@ class InterfaceController(object):
|
||||
has_tentative = [
|
||||
True
|
||||
for addr in addrs
|
||||
if (addr['family'] == socket.AF_INET6 and
|
||||
if (addr[consts.FAMILY] == socket.AF_INET6 and
|
||||
addr['flags'] & ifaddrmsg.IFA_F_TENTATIVE)]
|
||||
if not has_tentative:
|
||||
return
|
||||
@ -154,57 +163,200 @@ class InterfaceController(object):
|
||||
LOG.warning("Some IPV6 addresses remain still in 'tentative' state "
|
||||
"after %d seconds.", self.TENTATIVE_WAIT_TIMEOUT)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
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):
|
||||
self._dhclient_up(interface.name)
|
||||
if address.get(consts.IPV6AUTO):
|
||||
self._ipv6auto_up(interface.name)
|
||||
|
||||
self._addresses_up(interface, ipr, idx)
|
||||
self._routes_up(interface, ipr, idx)
|
||||
self._rules_up(interface, ipr, idx)
|
||||
|
||||
self._scripts_up(interface, current_state)
|
||||
|
||||
def _addresses_up(self, interface, ipr, idx):
|
||||
# Get existing addresses, this list is used to delete removed addresses
|
||||
current_addresses = []
|
||||
for addr in ipr.get_addr(index=idx):
|
||||
attrs = dict(addr['attrs'])
|
||||
# Skip non-static (ex: dynamic) addresses
|
||||
if not attrs['IFA_FLAGS'] & ifaddrmsg.IFA_F_PERMANENT:
|
||||
continue
|
||||
|
||||
key = (self._normalize_ip_address(attrs['IFA_ADDRESS']),
|
||||
addr[consts.PREFIXLEN])
|
||||
current_addresses.append(key)
|
||||
|
||||
# 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
|
||||
key = (self._normalize_ip_address(address.get(consts.ADDRESS)),
|
||||
address.get(consts.PREFIXLEN))
|
||||
if key in current_addresses:
|
||||
current_addresses.remove(key)
|
||||
elif address.get(consts.OCTAVIA_OWNED, True):
|
||||
# By default all adresses are managed/owned by Octavia
|
||||
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)
|
||||
|
||||
self._wait_tentative(ipr, idx)
|
||||
self._wait_tentative(ipr, idx)
|
||||
|
||||
for route in interface.routes:
|
||||
# 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)
|
||||
|
||||
def _routes_up(self, interface, ipr, idx):
|
||||
# Get existing routes, this list will be used to remove old/unused
|
||||
# routes
|
||||
current_routes = []
|
||||
for route in ipr.get_routes(oif=idx):
|
||||
# We only consider 'static' routes (routes that are added by
|
||||
# octavia-interface), we don't update kernel or ra routes.
|
||||
if route['proto'] != rt_proto['static']:
|
||||
continue
|
||||
|
||||
attrs = dict(route['attrs'])
|
||||
family = route[consts.FAMILY]
|
||||
# Disabling B104: hardcoded_bind_all_interfaces
|
||||
dst = attrs.get(
|
||||
'RTA_DST',
|
||||
'0.0.0.0' if family == socket.AF_INET 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:
|
||||
key = (self._normalize_ip_network(route.get(consts.DST)),
|
||||
self._normalize_ip_address(route.get(consts.GATEWAY)),
|
||||
self._normalize_ip_address(route.get(consts.PREFSRC)),
|
||||
route.get(consts.TABLE, 254))
|
||||
if key in current_routes:
|
||||
# Route is already there, we want to keep it, remove it from
|
||||
# the list of routes to delete
|
||||
current_routes.remove(key)
|
||||
else:
|
||||
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.
|
||||
# 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:
|
||||
# Delete unused routes (only 'static' routes are considered, we only
|
||||
# delete routes we have previously added)
|
||||
for r in current_routes:
|
||||
route = {consts.DST: r[0],
|
||||
consts.GATEWAY: r[1],
|
||||
consts.PREFSRC: r[2],
|
||||
consts.TABLE: r[3],
|
||||
consts.FAMILY: self._family(r[0])}
|
||||
|
||||
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)
|
||||
|
||||
def _rules_up(self, interface, ipr, idx):
|
||||
# Get existing rules
|
||||
current_rules = []
|
||||
for rule in ipr.get_rules():
|
||||
attrs = dict(rule['attrs'])
|
||||
if not attrs.get('FRA_SRC'):
|
||||
continue
|
||||
|
||||
# skip the rules defined by the kernel (FRA_PROTOCOL == 2) or by
|
||||
# keepalived (FRA_PROTOCOL == 18)
|
||||
# we only consider removing the rules that we have previously added
|
||||
if attrs.get('FRA_PROTOCOL') in (2, 18):
|
||||
continue
|
||||
|
||||
key = (attrs.get('FRA_TABLE'),
|
||||
self._normalize_ip_address(attrs.get('FRA_SRC')),
|
||||
rule[consts.SRC_LEN])
|
||||
current_rules.append(key)
|
||||
|
||||
# Add new rules
|
||||
for rule in interface.rules:
|
||||
key = (rule.get(consts.TABLE, 254),
|
||||
self._normalize_ip_address(rule.get(consts.SRC)),
|
||||
rule.get(consts.SRC_LEN))
|
||||
if key in current_rules:
|
||||
current_rules.remove(key)
|
||||
else:
|
||||
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())
|
||||
# Remove old rules
|
||||
for r in current_rules:
|
||||
rule = {consts.TABLE: r[0],
|
||||
consts.SRC: r[1],
|
||||
consts.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)
|
||||
|
||||
def _scripts_up(self, interface, current_state):
|
||||
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)
|
||||
@ -245,6 +397,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)
|
||||
|
||||
|
@ -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,67 @@ 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
|
||||
})
|
||||
# In ACTIVE_STANDBY topology, keepalived configures the VIP address.
|
||||
# Keep track of it in the interface file but mark it with a special
|
||||
# flag so the amphora-interface would not add/delete
|
||||
# keepalived-maintained things.
|
||||
self.addresses.append({
|
||||
consts.ADDRESS: vip,
|
||||
consts.PREFIXLEN: 128 if ip_version == 6 else 32,
|
||||
# OCTAVIA_OWNED = False when this address is managed by another
|
||||
# tool (keepalived)
|
||||
consts.OCTAVIA_OWNED: topology != consts.TOPOLOGY_ACTIVE_STANDBY
|
||||
})
|
||||
if topology != consts.TOPOLOGY_ACTIVE_STANDBY:
|
||||
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.TABLE: 1
|
||||
})
|
||||
self.rules.append({
|
||||
consts.SRC: vip,
|
||||
consts.SRC_LEN: 128 if ip_version == 6 else 32,
|
||||
consts.TABLE: 1,
|
||||
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))
|
||||
})
|
||||
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):
|
||||
|
@ -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.OCTAVIA_OWNED, True),
|
||||
r2.get(consts.OCTAVIA_OWNED, True))
|
||||
|
||||
|
||||
def assert_rule_lists_equal(obj, l1, l2):
|
||||
|
@ -1661,7 +1661,7 @@ class TestServerTestCase(base.TestCase):
|
||||
consts.PREFIXLEN: 24
|
||||
}, {
|
||||
consts.ADDRESS: "203.0.113.2",
|
||||
consts.PREFIXLEN: 24
|
||||
consts.PREFIXLEN: 32
|
||||
}
|
||||
],
|
||||
consts.ROUTES: [
|
||||
@ -1785,7 +1785,7 @@ class TestServerTestCase(base.TestCase):
|
||||
consts.DHCP: True
|
||||
}, {
|
||||
consts.ADDRESS: "203.0.113.2",
|
||||
consts.PREFIXLEN: 24
|
||||
consts.PREFIXLEN: 32
|
||||
}
|
||||
],
|
||||
consts.ROUTES: [
|
||||
@ -1837,7 +1837,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)]
|
||||
@ -2026,7 +2025,7 @@ class TestServerTestCase(base.TestCase):
|
||||
consts.PREFIXLEN: 32
|
||||
}, {
|
||||
consts.ADDRESS: '2001:0db8::2',
|
||||
consts.PREFIXLEN: 32
|
||||
consts.PREFIXLEN: 128
|
||||
}
|
||||
],
|
||||
consts.ROUTES: [
|
||||
@ -2147,7 +2146,7 @@ class TestServerTestCase(base.TestCase):
|
||||
},
|
||||
{
|
||||
consts.ADDRESS: '2001:db8::2',
|
||||
consts.PREFIXLEN: 32
|
||||
consts.PREFIXLEN: 128
|
||||
}
|
||||
],
|
||||
consts.ROUTES: [
|
||||
@ -2198,7 +2197,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)]
|
||||
|
@ -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,13 +449,213 @@ 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')
|
||||
@mock.patch('octavia.amphorae.backends.utils.interface.'
|
||||
'InterfaceController._wait_tentative')
|
||||
def test_up_update(self, mock_wait_tentative, 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.OCTAVIA_OWNED: False
|
||||
}, {
|
||||
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_get_addr.return_value = [{
|
||||
'prefixlen': 24,
|
||||
'attrs': {
|
||||
'IFA_ADDRESS': '2.0.0.1',
|
||||
'IFA_FLAGS': 0x80 # IFA_F_PERMANENT
|
||||
}
|
||||
}, {
|
||||
'prefixlen': 16,
|
||||
'attrs': {
|
||||
'IFA_ADDRESS': '10.2.3.4',
|
||||
'IFA_FLAGS': 0x80 # IFA_F_PERMANENT
|
||||
}
|
||||
}, {
|
||||
'prefixlen': 16,
|
||||
'attrs': {
|
||||
'IFA_ADDRESS': '10.2.3.5',
|
||||
'IFA_FLAGS': 0x0 # not IFA_F_PERMANENT
|
||||
}
|
||||
}]
|
||||
|
||||
mock_get_routes.return_value = [{
|
||||
'dst_len': 16,
|
||||
'family': 2,
|
||||
'proto': 4, # STATIC
|
||||
'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,
|
||||
'proto': 4, # STATIC
|
||||
'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')
|
||||
@mock.patch('octavia.amphorae.backends.utils.interface.'
|
||||
'InterfaceController._wait_tentative')
|
||||
def test_up_auto(self, mock_wait_tentative, mock_check_output,
|
||||
mock_link_lookup, mock_link, mock_addr, mock_route,
|
||||
mock_rule):
|
||||
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,
|
||||
@ -472,6 +677,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)
|
||||
|
||||
@ -629,13 +838,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,
|
||||
@ -690,6 +900,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 = [
|
||||
@ -731,6 +944,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,
|
||||
@ -842,13 +1059,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,
|
||||
@ -886,6 +1104,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",
|
||||
@ -950,3 +1170,49 @@ class TestInterface(base.TestCase):
|
||||
|
||||
controller._wait_tentative(mock_ipr, idx)
|
||||
self.assertEqual(4, len(mock_time_sleep.mock_calls))
|
||||
|
||||
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)
|
||||
|
@ -59,7 +59,7 @@ class TestInterfaceFile(base.TestCase):
|
||||
},
|
||||
{
|
||||
consts.ADDRESS: VIP_ADDRESS,
|
||||
consts.PREFIXLEN: prefixlen
|
||||
consts.PREFIXLEN: 32,
|
||||
}
|
||||
],
|
||||
consts.ROUTES: [
|
||||
@ -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: 32
|
||||
},
|
||||
{
|
||||
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
|
||||
@ -153,7 +276,7 @@ class TestInterfaceFile(base.TestCase):
|
||||
consts.DHCP: True
|
||||
}, {
|
||||
consts.ADDRESS: VIP_ADDRESS,
|
||||
consts.PREFIXLEN: prefixlen
|
||||
consts.PREFIXLEN: 32,
|
||||
}
|
||||
],
|
||||
consts.ROUTES: [
|
||||
@ -226,6 +349,11 @@ class TestInterfaceFile(base.TestCase):
|
||||
{
|
||||
consts.ADDRESS: VRRP_IP_ADDRESS,
|
||||
consts.PREFIXLEN: prefixlen
|
||||
},
|
||||
{
|
||||
consts.ADDRESS: VIP_ADDRESS,
|
||||
consts.PREFIXLEN: 32,
|
||||
consts.OCTAVIA_OWNED: False
|
||||
}
|
||||
],
|
||||
consts.ROUTES: [
|
||||
@ -304,7 +432,7 @@ class TestInterfaceFile(base.TestCase):
|
||||
},
|
||||
{
|
||||
consts.ADDRESS: VIP_ADDRESS,
|
||||
consts.PREFIXLEN: prefixlen
|
||||
consts.PREFIXLEN: 128
|
||||
}
|
||||
],
|
||||
consts.ROUTES: [
|
||||
|
Loading…
Reference in New Issue
Block a user