Subnet host routes support for plug_network

Subnets will sometimes be defined to have static routes that all
fixed ips on that subnet should use, neutron calls them host routes
in the API.  This makes Octavia aware of them.

Co-Authored-By: Michael Johnson <johnsomor@gmail.com>
Change-Id: I37b79da5e4cf532a31780537702d6effa656de5b
This commit is contained in:
Brandon Logan 2016-08-01 16:14:43 -05:00 committed by Michael Johnson
parent 8a2872f9a3
commit d826fc2865
11 changed files with 629 additions and 59 deletions

View File

@ -16,6 +16,7 @@
import logging
import os
import shutil
import socket
import stat
import subprocess
@ -47,9 +48,11 @@ template_port = j2_env.get_template(ETH_X_PORT_CONF)
template_vip = j2_env.get_template(ETH_X_VIP_CONF)
def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip=None):
def plug_vip(vip, subnet_cidr, gateway,
mac_address, vrrp_ip=None, host_routes=None):
# Validate vip and subnet_cidr, calculate broadcast address and netmask
try:
render_host_routes = []
ip = ipaddress.ip_address(
vip if six.text_type == type(vip) else six.u(vip))
network = ipaddress.ip_network(
@ -65,6 +68,14 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip=None):
vrrp_ip if six.text_type == type(vrrp_ip) else six.u(vrrp_ip)
)
vrrp_version = vrrp_ip_obj.version
if host_routes:
for hr in host_routes:
network = ipaddress.ip_network(
hr['destination'] if isinstance(
hr['destination'], six.text_type) else
six.u(hr['destination']))
render_host_routes.append({'network': network,
'gw': hr['nexthop']})
except ValueError:
return flask.make_response(flask.jsonify(dict(
message="Invalid VIP")), 400)
@ -127,6 +138,7 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip=None):
gateway=gateway,
vrrp_ip=vrrp_ip,
vrrp_ipv6=vrrp_version is 6,
host_routes=render_host_routes,
)
text_file.write(text)
@ -156,6 +168,59 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip=None):
vip=vip, interface=primary_interface))), 202)
def _generate_network_file_text(netns_interface, fixed_ips):
text = ''
if fixed_ips is None:
text = template_port.render(interface=netns_interface)
else:
for index, fixed_ip in enumerate(fixed_ips, -1):
if index == -1:
netns_ip_interface = netns_interface
else:
netns_ip_interface = "{int}:{ip}".format(
int=netns_interface, ip=index)
try:
ip_addr = fixed_ip['ip_address']
cidr = fixed_ip['subnet_cidr']
ip = ipaddress.ip_address(
ip_addr if six.text_type == type(
ip_addr) else six.u(ip_addr))
network = ipaddress.ip_network(
cidr if six.text_type == type(
cidr) else six.u(cidr))
broadcast = network.broadcast_address.exploded
netmask = (network.prefixlen if ip.version is 6
else network.netmask.exploded)
host_routes = []
for hr in fixed_ip.get('host_routes', []):
network = ipaddress.ip_network(
hr['destination'] if isinstance(
hr['destination'], six.text_type) else
six.u(hr['destination']))
host_routes.append({'network': network,
'gw': hr['nexthop']})
except ValueError:
return flask.make_response(flask.jsonify(dict(
message="Invalid network IP")), 400)
new_text = template_port.render(interface=netns_ip_interface,
ipv6=ip.version is 6,
ip_address=ip.exploded,
broadcast=broadcast,
netmask=netmask,
host_routes=host_routes)
text = '\n'.join([text, new_text])
return text
def _check_ip_addresses(fixed_ips):
if fixed_ips:
for ip in fixed_ips:
try:
socket.inet_pton(socket.AF_INET, ip.get('ip_address'))
except socket.error:
socket.inet_pton(socket.AF_INET6, ip.get('ip_address'))
def plug_network(mac_address, fixed_ips):
# Check if the interface is already in the network namespace
@ -167,6 +232,13 @@ def plug_network(mac_address, fixed_ips):
# This is the interface as it was initially plugged into the
# default network namespace, this will likely always be eth1
try:
_check_ip_addresses(fixed_ips=fixed_ips)
except socket.error:
return flask.make_response(flask.jsonify(dict(
message="Invalid network port")), 400)
default_netns_interface = _interface_by_mac(mac_address)
# We need to determine the interface name when inside the namespace
@ -199,40 +271,8 @@ def plug_network(mac_address, fixed_ips):
with os.fdopen(os.open(interface_file_path, flags, mode),
'w') as text_file:
if fixed_ips is None:
text = template_port.render(interface=netns_interface)
text_file.write(text)
else:
ip_count = -1
for fixed_ip in fixed_ips:
if ip_count == -1:
netns_ip_interface = netns_interface
ip_count += 1
else:
netns_ip_interface = "{int}:{ip}".format(
int=netns_interface, ip=ip_count)
ip_count += 1
try:
ip_addr = fixed_ip['ip_address']
cidr = fixed_ip['subnet_cidr']
ip = ipaddress.ip_address(
ip_addr if six.text_type == type(
ip_addr) else six.u(ip_addr))
network = ipaddress.ip_network(
cidr if six.text_type == type(
cidr) else six.u(cidr))
broadcast = network.broadcast_address.exploded
netmask = (network.prefixlen if ip.version is 6
else network.netmask.exploded)
except ValueError:
return flask.make_response(flask.jsonify(dict(
message="Invalid network IP")), 400)
text = template_port.render(interface=netns_ip_interface,
ipv6=ip.version is 6,
ip_address=ip.exploded,
broadcast=broadcast,
netmask=netmask)
text_file.write(text)
text = _generate_network_file_text(netns_interface, fixed_ips)
text_file.write(text)
# Update the list of interfaces to add to the namespace
_update_plugged_interfaces_file(netns_interface, mac_address)

View File

@ -123,7 +123,8 @@ def plug_vip(vip):
net_info['subnet_cidr'],
net_info['gateway'],
net_info['mac_address'],
net_info.get('vrrp_ip'))
net_info.get('vrrp_ip'),
net_info.get('host_routes'))
@app.route('/' + api_server.VERSION + '/plug/network', methods=['POST'])

View File

@ -20,6 +20,10 @@ iface {{ interface }} inet{{ '6' if ipv6 }} static
address {{ ip_address }}
broadcast {{ broadcast }}
netmask {{ netmask }}
{%- for hr in host_routes %}
up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- endfor %}
{%- else %}
iface {{ interface }} inet dhcp
auto {{ interface }}:0

View File

@ -23,6 +23,10 @@ address {{ vrrp_ip }}
broadcast {{ broadcast }}
netmask {{ netmask }}
gateway {{ gateway }}
{%- for hr in host_routes %}
up route add -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
down route del -net {{ hr.network }} gw {{ hr.gw }} dev {{ interface }}
{%- endfor %}
{%- else %}
iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }}
{%- endif %}

View File

@ -127,10 +127,14 @@ class HaproxyAmphoraLoadBalancerDriver(
# compatibility with old amphora agent versions.
port = amphorae_network_config.get(amphora.id).vrrp_port
host_routes = [{'nexthop': hr.nexthop,
'destination': hr.destination}
for hr in subnet.host_routes]
net_info = {'subnet_cidr': subnet.cidr,
'gateway': subnet.gateway_ip,
'mac_address': port.mac_address,
'vrrp_ip': amphora.vrrp_ip}
'vrrp_ip': amphora.vrrp_ip,
'host_routes': host_routes}
try:
self.client.plug_vip(amphora,
load_balancer.vip.ip_address,
@ -143,8 +147,12 @@ class HaproxyAmphoraLoadBalancerDriver(
def post_network_plug(self, amphora, port):
fixed_ips = []
for fixed_ip in port.fixed_ips:
host_routes = [{'nexthop': hr.nexthop,
'destination': hr.destination}
for hr in fixed_ip.subnet.host_routes]
ip = {'ip_address': fixed_ip.ip_address,
'subnet_cidr': fixed_ip.subnet.cidr}
'subnet_cidr': fixed_ip.subnet.cidr,
'host_routes': host_routes}
fixed_ips.append(ip)
port_info = {'mac_address': port.mac_address,
'fixed_ips': fixed_ips}

View File

@ -59,7 +59,8 @@ class Network(data_models.BaseDataModel):
class Subnet(data_models.BaseDataModel):
def __init__(self, id=None, name=None, network_id=None, project_id=None,
gateway_ip=None, cidr=None, ip_version=None):
gateway_ip=None, cidr=None, ip_version=None,
host_routes=None):
self.id = id
self.name = name
self.network_id = network_id
@ -67,6 +68,7 @@ class Subnet(data_models.BaseDataModel):
self.gateway_ip = gateway_ip
self.cidr = cidr
self.ip_version = ip_version
self.host_routes = host_routes
class Port(data_models.BaseDataModel):
@ -113,3 +115,10 @@ class AmphoraNetworkConfig(data_models.BaseDataModel):
self.vrrp_port = vrrp_port
self.ha_subnet = ha_subnet
self.ha_port = ha_port
class HostRoute(data_models.BaseDataModel):
def __init__(self, nexthop=None, destination=None):
self.nexthop = nexthop
self.destination = destination

View File

@ -18,12 +18,17 @@ from octavia.network import data_models as network_models
def convert_subnet_dict_to_model(subnet_dict):
subnet = subnet_dict.get('subnet', subnet_dict)
subnet_hrs = subnet.get('host_routes', [])
host_routes = [network_models.HostRoute(nexthop=hr.get('nexthop'),
destination=hr.get('destination'))
for hr in subnet_hrs]
return network_models.Subnet(id=subnet.get('id'), name=subnet.get('name'),
network_id=subnet.get('network_id'),
project_id=subnet.get('tenant_id'),
gateway_ip=subnet.get('gateway_ip'),
cidr=subnet.get('cidr'),
ip_version=subnet.get('ip_version')
ip_version=subnet.get('ip_version'),
host_routes=host_routes
)

View File

@ -38,12 +38,12 @@ RANDOM_ERROR = 'random error'
OK = dict(message='OK')
class ServerTestCase(base.TestCase):
class TestServerTestCase(base.TestCase):
app = None
def setUp(self):
self.app = server.app.test_client()
super(ServerTestCase, self).setUp()
super(TestServerTestCase, self).setUp()
@mock.patch('os.path.exists')
@mock.patch('os.makedirs')
@ -487,16 +487,15 @@ class ServerTestCase(base.TestCase):
handle.write.assert_any_call(six.b('TestT'))
handle.write.assert_any_call(six.b('est'))
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'plug._netns_interface_exists')
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('pyroute2.IPRoute')
@mock.patch('pyroute2.NetNS')
@mock.patch('subprocess.check_output')
def test_plug_network(self, mock_check_output, mock_netns,
mock_pyroute2, mock_ifaddress, mock_interfaces,
mock_int_exists):
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'plug._netns_interface_exists')
def test_plug_network(self, mock_int_exists, mock_check_output, mock_netns,
mock_pyroute2, mock_ifaddress, mock_interfaces):
port_info = {'mac_address': '123'}
test_int_num = random.randint(0, 9999)
@ -605,7 +604,7 @@ class ServerTestCase(base.TestCase):
handle = m()
handle.write.assert_any_call(
'\n# Generated by Octavia agent\n'
'\n\n# Generated by Octavia agent\n'
'auto eth' + test_int_num +
'\niface eth' + test_int_num + ' inet static\n' +
'address 10.0.0.5\nbroadcast 10.0.0.255\n' +
@ -645,7 +644,7 @@ class ServerTestCase(base.TestCase):
handle = m()
handle.write.assert_any_call(
'\n# Generated by Octavia agent\n'
'\n\n# Generated by Octavia agent\n'
'auto eth' + test_int_num +
'\niface eth' + test_int_num + ' inet6 static\n' +
'address 2001:0db8:0000:0000:0000:0000:0000:0002\n'
@ -696,6 +695,78 @@ class ServerTestCase(base.TestCase):
'message': 'Error plugging network'},
json.loads(rv.data.decode('utf-8')))
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('pyroute2.IPRoute')
@mock.patch('pyroute2.NetNS')
@mock.patch('subprocess.check_output')
def test_plug_network_host_routes(self, mock_check_output, mock_netns,
mock_pyroute2, mock_ifaddress,
mock_interfaces):
SUBNET_CIDR = '192.0.2.0/24'
BROADCAST = '192.0.2.255'
NETMASK = '255.255.255.0'
IP = '192.0.1.5'
MAC = '123'
DEST1 = '198.51.100.0/24'
DEST2 = '203.0.113.0/24'
NEXTHOP = '192.0.2.1'
netns_handle = mock_netns.return_value.__enter__.return_value
netns_handle.get_links.return_value = [{
'attrs': [['IFLA_IFNAME', consts.NETNS_PRIMARY_INTERFACE]]}]
port_info = {'mac_address': MAC, 'fixed_ips': [
{'ip_address': IP, 'subnet_cidr': SUBNET_CIDR,
'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}]}]}
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
file_name = '/etc/netns/{0}/network/interfaces.d/{1}.cfg'.format(
consts.AMPHORA_NAMESPACE, consts.NETNS_PRIMARY_INTERFACE)
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
with mock.patch('os.open') as mock_open, mock.patch.object(
os, 'fdopen', m) as mock_fdopen:
mock_open.return_value = 123
rv = self.app.post('/' + api_server.VERSION + "/plug/network",
content_type='application/json',
data=json.dumps(port_info))
self.assertEqual(202, rv.status_code)
mock_open.assert_any_call(file_name, flags, mode)
mock_fdopen.assert_any_call(123, 'w')
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
flags = os.O_RDWR | os.O_CREAT
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
mock_open.assert_any_call(plug_inf_file, flags, mode)
mock_fdopen.assert_any_call(123, 'r+')
handle = m()
handle.write.assert_any_call(
'\n\n# Generated by Octavia agent\n'
'auto ' + consts.NETNS_PRIMARY_INTERFACE +
'\niface ' + consts.NETNS_PRIMARY_INTERFACE +
' inet static\n' +
'address ' + IP + '\nbroadcast ' + BROADCAST + '\n' +
'netmask ' + NETMASK + '\n' +
'up route add -net ' + DEST1 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
'down route del -net ' + DEST1 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
'up route add -net ' + DEST2 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
'down route del -net ' + DEST2 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
)
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', consts.NETNS_PRIMARY_INTERFACE], stderr=-2)
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'plug._netns_interface_exists')
@mock.patch('netifaces.interfaces')
@ -755,6 +826,75 @@ class ServerTestCase(base.TestCase):
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# Happy Path IPv4, with VRRP_IP and host route
full_subnet_info = {
'subnet_cidr': '203.0.113.0/24',
'gateway': '203.0.113.1',
'mac_address': '123',
'vrrp_ip': '203.0.113.4',
'host_routes': [{'destination': '203.0.114.0/24',
'nexthop': '203.0.113.5'},
{'destination': '203.0.115.0/24',
'nexthop': '203.0.113.5'}]
}
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
file_name = ('/etc/netns/{netns}/network/interfaces.d/'
'{netns_int}.cfg'.format(
netns=consts.AMPHORA_NAMESPACE,
netns_int=consts.NETNS_PRIMARY_INTERFACE))
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
with mock.patch('os.open') as mock_open, mock.patch.object(
os, 'fdopen', m) as mock_fdopen:
mock_open.return_value = 123
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/203.0.113.2",
content_type='application/json',
data=json.dumps(full_subnet_info))
self.assertEqual(202, rv.status_code)
mock_open.assert_any_call(file_name, flags, mode)
mock_fdopen.assert_any_call(123, 'w')
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
flags = os.O_RDWR | os.O_CREAT
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
mock_open.assert_any_call(plug_inf_file, flags, mode)
mock_fdopen.assert_any_call(123, 'r+')
handle = m()
handle.write.assert_any_call(
'\n# Generated by Octavia agent\n'
'auto {netns_int} {netns_int}:0\n'
'iface {netns_int} inet static\n'
'address 203.0.113.4\n'
'broadcast 203.0.113.255\n'
'netmask 255.255.255.0\n'
'gateway 203.0.113.1\n'
'up route add -net 203.0.114.0/24 gw 203.0.113.5 '
'dev {netns_int}\n'
'down route del -net 203.0.114.0/24 gw 203.0.113.5 '
'dev {netns_int}\n'
'up route add -net 203.0.115.0/24 gw 203.0.113.5 '
'dev {netns_int}\n'
'down route del -net 203.0.115.0/24 gw 203.0.113.5 '
'dev {netns_int}\n'
'\n'
'iface {netns_int}:0 inet static\n'
'address 203.0.113.2\n'
'broadcast 203.0.113.255\n'
'netmask 255.255.255.0'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE))
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', '{netns_int}:0'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2)
# One Interface down, Happy Path IPv4
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
@ -868,6 +1008,73 @@ class ServerTestCase(base.TestCase):
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# Happy Path IPv6, with VRRP_IP and host route
full_subnet_info = {
'subnet_cidr': '2001:db8::/32',
'gateway': '2001:db8::1',
'mac_address': '123',
'vrrp_ip': '2001:db8::4',
'host_routes': [{'destination': '2001:db9::/32',
'nexthop': '2001:db8::5'},
{'destination': '2001:db9::/32',
'nexthop': '2001:db8::5'}]
}
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
file_name = ('/etc/netns/{netns}/network/interfaces.d/'
'{netns_int}.cfg'.format(
netns=consts.AMPHORA_NAMESPACE,
netns_int=consts.NETNS_PRIMARY_INTERFACE))
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
with mock.patch('os.open') as mock_open, mock.patch.object(
os, 'fdopen', m) as mock_fdopen:
mock_open.return_value = 123
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(full_subnet_info))
self.assertEqual(202, rv.status_code)
mock_open.assert_any_call(file_name, flags, mode)
mock_fdopen.assert_any_call(123, 'w')
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
flags = os.O_RDWR | os.O_CREAT
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
mock_open.assert_any_call(plug_inf_file, flags, mode)
mock_fdopen.assert_any_call(123, 'r+')
handle = m()
handle.write.assert_any_call(
'\n# Generated by Octavia agent\n'
'auto {netns_int} {netns_int}:0\n'
'iface {netns_int} inet6 static\n'
'address 2001:db8::4\n'
'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n'
'netmask 32\n'
'gateway 2001:db8::1\n'
'up route add -net 2001:db9::/32 gw 2001:db8::5 '
'dev {netns_int}\n'
'down route del -net 2001:db9::/32 gw 2001:db8::5 '
'dev {netns_int}\n'
'up route add -net 2001:db9::/32 gw 2001:db8::5 '
'dev {netns_int}\n'
'down route del -net 2001:db9::/32 gw 2001:db8::5 '
'dev {netns_int}\n'
'\n'
'iface {netns_int}:0 inet6 static\n'
'address 2001:0db8:0000:0000:0000:0000:0000:0002\n'
'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n'
'netmask 32'.format(netns_int=consts.NETNS_PRIMARY_INTERFACE))
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', '{netns_int}:0'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2)
# One Interface down, Happy Path IPv6
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],

View File

@ -499,8 +499,7 @@ class ServerTestCase(base.TestCase):
@mock.patch('octavia.amphorae.backends.agent.api_server.'
'plug._netns_interface_exists')
def test_plug_network(self, mock_int_exists, mock_check_output, mock_netns,
mock_pyroute2, mock_ifaddress,
mock_interfaces):
mock_pyroute2, mock_ifaddress, mock_interfaces):
port_info = {'mac_address': '123'}
test_int_num = random.randint(0, 9999)
@ -588,6 +587,78 @@ class ServerTestCase(base.TestCase):
'message': 'Error plugging network'},
json.loads(rv.data.decode('utf-8')))
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('pyroute2.IPRoute')
@mock.patch('pyroute2.NetNS')
@mock.patch('subprocess.check_output')
def test_plug_network_host_routes(self, mock_check_output, mock_netns,
mock_pyroute2, mock_ifaddress,
mock_interfaces):
SUBNET_CIDR = '192.0.2.0/24'
BROADCAST = '192.0.2.255'
NETMASK = '255.255.255.0'
IP = '192.0.1.5'
MAC = '123'
DEST1 = '198.51.100.0/24'
DEST2 = '203.0.113.0/24'
NEXTHOP = '192.0.2.1'
netns_handle = mock_netns.return_value.__enter__.return_value
netns_handle.get_links.return_value = [{
'attrs': [['IFLA_IFNAME', consts.NETNS_PRIMARY_INTERFACE]]}]
port_info = {'mac_address': MAC, 'fixed_ips': [
{'ip_address': IP, 'subnet_cidr': SUBNET_CIDR,
'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}]}]}
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
file_name = '/etc/netns/{0}/network/interfaces.d/{1}.cfg'.format(
consts.AMPHORA_NAMESPACE, consts.NETNS_PRIMARY_INTERFACE)
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
with mock.patch('os.open') as mock_open, mock.patch.object(
os, 'fdopen', m) as mock_fdopen:
mock_open.return_value = 123
rv = self.app.post('/' + api_server.VERSION + "/plug/network",
content_type='application/json',
data=json.dumps(port_info))
self.assertEqual(202, rv.status_code)
mock_open.assert_any_call(file_name, flags, mode)
mock_fdopen.assert_any_call(123, 'w')
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
flags = os.O_RDWR | os.O_CREAT
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
mock_open.assert_any_call(plug_inf_file, flags, mode)
mock_fdopen.assert_any_call(123, 'r+')
handle = m()
handle.write.assert_any_call(
'\n\n# Generated by Octavia agent\n'
'auto ' + consts.NETNS_PRIMARY_INTERFACE +
'\niface ' + consts.NETNS_PRIMARY_INTERFACE +
' inet static\n' +
'address ' + IP + '\nbroadcast ' + BROADCAST + '\n' +
'netmask ' + NETMASK + '\n' +
'up route add -net ' + DEST1 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
'down route del -net ' + DEST1 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
'up route add -net ' + DEST2 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
'down route del -net ' + DEST2 + ' gw ' + NEXTHOP +
' dev ' + consts.NETNS_PRIMARY_INTERFACE + '\n'
)
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', consts.NETNS_PRIMARY_INTERFACE], stderr=-2)
@mock.patch('netifaces.interfaces')
@mock.patch('netifaces.ifaddresses')
@mock.patch('pyroute2.IPRoute')
@ -635,6 +706,75 @@ class ServerTestCase(base.TestCase):
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# Happy Path IPv4, with VRRP_IP and host route
full_subnet_info = {
'subnet_cidr': '203.0.113.0/24',
'gateway': '203.0.113.1',
'mac_address': '123',
'vrrp_ip': '203.0.113.4',
'host_routes': [{'destination': '203.0.114.0/24',
'nexthop': '203.0.113.5'},
{'destination': '203.0.115.0/24',
'nexthop': '203.0.113.5'}]
}
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
file_name = ('/etc/netns/{netns}/network/interfaces.d/'
'{netns_int}.cfg'.format(
netns=consts.AMPHORA_NAMESPACE,
netns_int=consts.NETNS_PRIMARY_INTERFACE))
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
with mock.patch('os.open') as mock_open, mock.patch.object(
os, 'fdopen', m) as mock_fdopen:
mock_open.return_value = 123
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/203.0.113.2",
content_type='application/json',
data=json.dumps(full_subnet_info))
self.assertEqual(202, rv.status_code)
mock_open.assert_any_call(file_name, flags, mode)
mock_fdopen.assert_any_call(123, 'w')
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
flags = os.O_RDWR | os.O_CREAT
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
mock_open.assert_any_call(plug_inf_file, flags, mode)
mock_fdopen.assert_any_call(123, 'r+')
handle = m()
handle.write.assert_any_call(
'\n# Generated by Octavia agent\n'
'auto {netns_int} {netns_int}:0\n'
'iface {netns_int} inet static\n'
'address 203.0.113.4\n'
'broadcast 203.0.113.255\n'
'netmask 255.255.255.0\n'
'gateway 203.0.113.1\n'
'up route add -net 203.0.114.0/24 gw 203.0.113.5 '
'dev {netns_int}\n'
'down route del -net 203.0.114.0/24 gw 203.0.113.5 '
'dev {netns_int}\n'
'up route add -net 203.0.115.0/24 gw 203.0.113.5 '
'dev {netns_int}\n'
'down route del -net 203.0.115.0/24 gw 203.0.113.5 '
'dev {netns_int}\n'
'\n'
'iface {netns_int}:0 inet static\n'
'address 203.0.113.2\n'
'broadcast 203.0.113.255\n'
'netmask 255.255.255.0'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE))
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', '{netns_int}:0'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2)
# One Interface down, Happy Path IPv4
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
@ -748,6 +888,73 @@ class ServerTestCase(base.TestCase):
self.assertEqual(dict(details="No suitable network interface found"),
json.loads(rv.data.decode('utf-8')))
# Happy Path IPv6, with VRRP_IP and host route
full_subnet_info = {
'subnet_cidr': '2001:db8::/32',
'gateway': '2001:db8::1',
'mac_address': '123',
'vrrp_ip': '2001:db8::4',
'host_routes': [{'destination': '2001:db9::/32',
'nexthop': '2001:db8::5'},
{'destination': '2001:db9::/32',
'nexthop': '2001:db8::5'}]
}
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],
{netifaces.AF_LINK: [{'addr': '123'}]}]
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
file_name = ('/etc/netns/{netns}/network/interfaces.d/'
'{netns_int}.cfg'.format(
netns=consts.AMPHORA_NAMESPACE,
netns_int=consts.NETNS_PRIMARY_INTERFACE))
m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open
with mock.patch('os.open') as mock_open, mock.patch.object(
os, 'fdopen', m) as mock_fdopen:
mock_open.return_value = 123
rv = self.app.post('/' + api_server.VERSION +
"/plug/vip/2001:db8::2",
content_type='application/json',
data=json.dumps(full_subnet_info))
self.assertEqual(202, rv.status_code)
mock_open.assert_any_call(file_name, flags, mode)
mock_fdopen.assert_any_call(123, 'w')
plug_inf_file = '/var/lib/octavia/plugged_interfaces'
flags = os.O_RDWR | os.O_CREAT
mode = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH
mock_open.assert_any_call(plug_inf_file, flags, mode)
mock_fdopen.assert_any_call(123, 'r+')
handle = m()
handle.write.assert_any_call(
'\n# Generated by Octavia agent\n'
'auto {netns_int} {netns_int}:0\n'
'iface {netns_int} inet6 static\n'
'address 2001:db8::4\n'
'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n'
'netmask 32\n'
'gateway 2001:db8::1\n'
'up route add -net 2001:db9::/32 gw 2001:db8::5 '
'dev {netns_int}\n'
'down route del -net 2001:db9::/32 gw 2001:db8::5 '
'dev {netns_int}\n'
'up route add -net 2001:db9::/32 gw 2001:db8::5 '
'dev {netns_int}\n'
'down route del -net 2001:db9::/32 gw 2001:db8::5 '
'dev {netns_int}\n'
'\n'
'iface {netns_int}:0 inet6 static\n'
'address 2001:0db8:0000:0000:0000:0000:0000:0002\n'
'broadcast 2001:0db8:ffff:ffff:ffff:ffff:ffff:ffff\n'
'netmask 32'.format(netns_int=consts.NETNS_PRIMARY_INTERFACE))
mock_check_output.assert_called_with(
['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE,
'ifup', '{netns_int}:0'.format(
netns_int=consts.NETNS_PRIMARY_INTERFACE)], stderr=-2)
# One Interface down, Happy Path IPv6
mock_interfaces.side_effect = [['blah']]
mock_ifaddress.side_effect = [[netifaces.AF_LINK],

View File

@ -132,3 +132,39 @@ class TestPlug(base.TestCase):
# Interface is not found in netns
self.assertFalse(plug._netns_interface_exists('321'))
class TestPlugNetwork(base.TestCase):
def test__generate_network_file_text_static_ip(self):
netns_interface = 'eth1234'
FIXED_IP = '192.0.2.2'
BROADCAST = '192.0.2.255'
SUBNET_CIDR = '192.0.2.0/24'
NETMASK = '255.255.255.0'
DEST1 = '198.51.100.0/24'
DEST2 = '203.0.113.0/24'
NEXTHOP = '192.0.2.1'
fixed_ips = [{'ip_address': FIXED_IP,
'subnet_cidr': SUBNET_CIDR,
'host_routes': [
{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}
]}]
text = plug._generate_network_file_text(netns_interface, fixed_ips)
expected_text = (
'\n\n# Generated by Octavia agent\n'
'auto ' + netns_interface + '\n'
'iface ' + netns_interface + ' inet static\n'
'address ' + FIXED_IP + '\n'
'broadcast ' + BROADCAST + '\n'
'netmask ' + NETMASK + '\n'
'up route add -net ' + DEST1 + ' gw ' + NEXTHOP +
' dev ' + netns_interface + '\n'
'down route del -net ' + DEST1 + ' gw ' + NEXTHOP +
' dev ' + netns_interface + '\n'
'up route add -net ' + DEST2 + ' gw ' + NEXTHOP +
' dev ' + netns_interface + '\n'
'down route del -net ' + DEST2 + ' gw ' + NEXTHOP +
' dev ' + netns_interface + '\n')
self.assertEqual(expected_text, text)

View File

@ -28,8 +28,8 @@ from octavia.network import data_models as network_models
from octavia.tests.unit import base as base
from octavia.tests.unit.common.sample_configs import sample_configs
FAKE_CIDR = '10.0.0.0/24'
FAKE_GATEWAY = '10.0.0.1'
FAKE_CIDR = '198.51.100.0/24'
FAKE_GATEWAY = '192.51.100.1'
FAKE_IP = 'fake'
FAKE_PEM_FILENAME = "file_name"
FAKE_UUID_1 = uuidutils.generate_uuid()
@ -41,6 +41,11 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
def setUp(self):
super(TestHaproxyAmphoraLoadBalancerDriverTest, self).setUp()
DEST1 = '198.51.100.0/24'
DEST2 = '203.0.113.0/24'
NEXTHOP = '192.0.2.1'
self.driver = driver.HaproxyAmphoraLoadBalancerDriver()
self.driver.cert_manager = mock.MagicMock()
@ -54,15 +59,22 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.sv = sample_configs.sample_vip_tuple()
self.lb = self.sl.load_balancer
self.fixed_ip = mock.MagicMock()
self.fixed_ip.ip_address = '10.0.0.5'
self.fixed_ip.subnet.cidr = '10.0.0.0/24'
self.fixed_ip.ip_address = '198.51.100.5'
self.fixed_ip.subnet.cidr = '198.51.100.0/24'
self.port = network_models.Port(mac_address=FAKE_MAC_ADDRESS,
fixed_ips=[self.fixed_ip])
self.host_routes = [network_models.HostRoute(destination=DEST1,
nexthop=NEXTHOP),
network_models.HostRoute(destination=DEST2,
nexthop=NEXTHOP)]
host_routes_data = [{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}]
self.subnet_info = {'subnet_cidr': FAKE_CIDR,
'gateway': FAKE_GATEWAY,
'mac_address': FAKE_MAC_ADDRESS,
'vrrp_ip': self.amp.vrrp_ip}
'vrrp_ip': self.amp.vrrp_ip,
'host_routes': host_routes_data}
@mock.patch('octavia.common.tls_utils.cert_parser.load_certificates_data')
@mock.patch('octavia.common.tls_utils.cert_parser.get_host_names')
@ -158,6 +170,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
amphorae_network_config = mock.MagicMock()
amphorae_network_config.get().vip_subnet.cidr = FAKE_CIDR
amphorae_network_config.get().vip_subnet.gateway_ip = FAKE_GATEWAY
amphorae_network_config.get().vip_subnet.host_routes = self.host_routes
amphorae_network_config.get().vrrp_port = self.port
self.driver.post_vip_plug(self.amp, self.lb, amphorae_network_config)
self.driver.client.plug_vip.assert_called_once_with(
@ -176,8 +189,44 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
self.driver.post_network_plug(self.amp, self.port)
self.driver.client.plug_network.assert_called_once_with(
self.amp, dict(mac_address=FAKE_MAC_ADDRESS,
fixed_ips=[dict(ip_address='10.0.0.5',
subnet_cidr='10.0.0.0/24')]))
fixed_ips=[dict(ip_address='198.51.100.5',
subnet_cidr='198.51.100.0/24',
host_routes=[])]))
def test_post_network_plug_with_host_routes(self):
SUBNET_ID = 'SUBNET_ID'
FIXED_IP1 = '192.0.2.2'
FIXED_IP2 = '192.0.2.3'
SUBNET_CIDR = '192.0.2.0/24'
DEST1 = '198.51.100.0/24'
DEST2 = '203.0.113.0/24'
NEXTHOP = '192.0.2.1'
host_routes = [network_models.HostRoute(destination=DEST1,
nexthop=NEXTHOP),
network_models.HostRoute(destination=DEST2,
nexthop=NEXTHOP)]
subnet = network_models.Subnet(id=SUBNET_ID, cidr=SUBNET_CIDR,
ip_version=4, host_routes=host_routes)
fixed_ips = [
network_models.FixedIP(subnet_id=subnet.id, ip_address=FIXED_IP1,
subnet=subnet),
network_models.FixedIP(subnet_id=subnet.id, ip_address=FIXED_IP2,
subnet=subnet)
]
port = network_models.Port(mac_address=FAKE_MAC_ADDRESS,
fixed_ips=fixed_ips)
self.driver.post_network_plug(self.amp, port)
expected_fixed_ips = [
{'ip_address': FIXED_IP1, 'subnet_cidr': SUBNET_CIDR,
'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}]},
{'ip_address': FIXED_IP2, 'subnet_cidr': SUBNET_CIDR,
'host_routes': [{'destination': DEST1, 'nexthop': NEXTHOP},
{'destination': DEST2, 'nexthop': NEXTHOP}]}
]
self.driver.client.plug_network.assert_called_once_with(
self.amp, dict(mac_address=FAKE_MAC_ADDRESS,
fixed_ips=expected_fixed_ips))
def test_get_vrrp_interface(self):
self.driver.get_vrrp_interface(self.amp)
@ -745,7 +794,7 @@ class TestAmphoraAPIClientTest(base.TestCase):
@requests_mock.mock()
def test_get_interface(self, m):
interface = [{"interface": "eth1"}]
ip_addr = '10.0.0.1'
ip_addr = '192.51.100.1'
m.get("{base}/interface/{ip_addr}".format(base=self.base_url,
ip_addr=ip_addr),
json=interface)