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:
parent
81134bdfec
commit
1a25289e2e
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -925,5 +925,7 @@ IFACE_DOWN = 'down'
|
|||
|
||||
COMMAND = 'command'
|
||||
|
||||
IGNORE = 'ignore'
|
||||
|
||||
# Amphora network directory
|
||||
AMP_NET_DIR_TEMPLATE = '/etc/octavia/interfaces/'
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: (
|
||||
|
|
Loading…
Reference in New Issue