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

Change-Id: If0b211b3ef4fce3b7a58823fc8d1e48448e4d346
This commit is contained in:
Gregory Thiemonge 2021-10-04 17:31:04 +02:00
parent 81134bdfec
commit 1a25289e2e
9 changed files with 715 additions and 105 deletions

View File

@ -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):

View File

@ -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",

View File

@ -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)

View File

@ -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):

View File

@ -925,5 +925,7 @@ IFACE_DOWN = 'down'
COMMAND = 'command'
IGNORE = 'ignore'
# Amphora network directory
AMP_NET_DIR_TEMPLATE = '/etc/octavia/interfaces/'

View File

@ -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):

View File

@ -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)]

View File

@ -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)

View File

@ -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: (