diff --git a/octavia/amphorae/backends/agent/api_server/plug.py b/octavia/amphorae/backends/agent/api_server/plug.py index 8a94f72066..e378e07964 100644 --- a/octavia/amphorae/backends/agent/api_server/plug.py +++ b/octavia/amphorae/backends/agent/api_server/plug.py @@ -16,7 +16,6 @@ import logging import os import shutil -import socket import stat import subprocess @@ -37,23 +36,36 @@ from octavia.i18n import _LE, _LI CONF = cfg.CONF CONF.import_group('amphora_agent', 'octavia.common.config') -ETH_PORT_CONF = 'plug_vip_ethX.conf.j2' - -ETH_X_VIP_CONF = 'plug_port_ethX.conf.j2' +ETH_X_VIP_CONF = 'plug_vip_ethX.conf.j2' +ETH_X_PORT_CONF = 'plug_port_ethX.conf.j2' LOG = logging.getLogger(__name__) j2_env = jinja2.Environment(loader=jinja2.FileSystemLoader( os.path.dirname(os.path.realpath(__file__)) + consts.AGENT_API_TEMPLATES)) -template_port = j2_env.get_template(ETH_X_VIP_CONF) -template_vip = j2_env.get_template(ETH_PORT_CONF) +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): - # validate vip +def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip=None): + # Validate vip and subnet_cidr, calculate broadcast address and netmask try: - socket.inet_aton(vip) - except socket.error: + ip = ipaddress.ip_address( + vip if six.text_type == type(vip) else six.u(vip)) + network = ipaddress.ip_network( + subnet_cidr if six.text_type == type(subnet_cidr) + else six.u(subnet_cidr)) + vip = ip.exploded + broadcast = network.broadcast_address.exploded + netmask = (network.prefixlen if ip.version is 6 + else network.netmask.exploded) + vrrp_version = None + if vrrp_ip: + vrrp_ip_obj = ipaddress.ip_address( + vrrp_ip if six.text_type == type(vrrp_ip) else six.u(vrrp_ip) + ) + vrrp_version = vrrp_ip_obj.version + except ValueError: return flask.make_response(flask.jsonify(dict( message="Invalid VIP")), 400) @@ -61,11 +73,6 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip): primary_interface = "{interface}".format(interface=interface) secondary_interface = "{interface}:0".format(interface=interface) - # assume for now only a fixed subnet size - sections = vip.split('.')[:3] - sections.append('255') - broadcast = '.'.join(sections) - # We need to setup the netns network directory so that the ifup # commands used here and in the startup scripts "sees" the right # interfaces and scripts. @@ -104,11 +111,13 @@ def plug_vip(vip, subnet_cidr, gateway, mac_address, vrrp_ip): text = template_vip.render( interface=interface, vip=vip, + vip_ipv6=ip.version is 6, broadcast=broadcast, - # assume for now only a fixed subnet size - netmask='255.255.255.0', + netmask=netmask, gateway=gateway, - vrrp_ip=vrrp_ip) + vrrp_ip=vrrp_ip, + vrrp_ipv6=vrrp_version is 6, + ) text_file.write(text) # Update the list of interfaces to add to the namespace diff --git a/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 b/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 index 43985e5b08..e66c56a250 100644 --- a/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 +++ b/octavia/amphorae/backends/agent/api_server/templates/plug_vip_ethX.conf.j2 @@ -18,16 +18,16 @@ auto {{ interface }} {{ interface }}:0 {%- if vrrp_ip %} -iface {{ interface }} inet static +iface {{ interface }} inet{{ '6' if vrrp_ipv6 }} static address {{ vrrp_ip }} broadcast {{ broadcast }} netmask {{ netmask }} gateway {{ gateway }} {%- else %} -iface {{ interface }} inet dhcp +iface {{ interface }} inet{{ '6' if vip_ipv6 }} {{ 'auto' if vip_ipv6 else 'dhcp' }} {%- endif %} -iface {{ interface }}:0 inet static +iface {{ interface }}:0 inet{{ '6' if vip_ipv6 }} static address {{ vip }} broadcast {{ broadcast }} netmask {{ netmask }} diff --git a/octavia/db/migration/alembic_migrations/versions/82b9402e71fd_update_vip_address_size.py b/octavia/db/migration/alembic_migrations/versions/82b9402e71fd_update_vip_address_size.py new file mode 100644 index 0000000000..d52ad499d7 --- /dev/null +++ b/octavia/db/migration/alembic_migrations/versions/82b9402e71fd_update_vip_address_size.py @@ -0,0 +1,33 @@ +# Copyright 2016 Rackspace +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Update vip address size + +Revision ID: 82b9402e71fd +Revises: 62816c232310 +Create Date: 2016-07-17 14:36:36.698870 + +""" + +# revision identifiers, used by Alembic. +revision = '82b9402e71fd' +down_revision = '4a6ec0ab7284' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.alter_column(u'vip', u'ip_address', + existing_type=sa.String(64)) diff --git a/octavia/db/models.py b/octavia/db/models.py index 8b4bca3387..328ccfdbf7 100644 --- a/octavia/db/models.py +++ b/octavia/db/models.py @@ -316,7 +316,7 @@ class Vip(base_models.BASE): sa.ForeignKey("load_balancer.id", name="fk_vip_load_balancer_id"), nullable=False, primary_key=True) - ip_address = sa.Column(sa.String(36), nullable=True) + ip_address = sa.Column(sa.String(64), nullable=True) port_id = sa.Column(sa.String(36), nullable=True) subnet_id = sa.Column(sa.String(36), nullable=True) load_balancer = orm.relationship("LoadBalancer", uselist=False, diff --git a/octavia/network/drivers/neutron/allowed_address_pairs.py b/octavia/network/drivers/neutron/allowed_address_pairs.py index c41edea9d3..d3f10e4fde 100644 --- a/octavia/network/drivers/neutron/allowed_address_pairs.py +++ b/octavia/network/drivers/neutron/allowed_address_pairs.py @@ -29,6 +29,8 @@ from octavia.network import data_models as n_data_models from octavia.network.drivers.neutron import base as neutron_base from octavia.network.drivers.neutron import utils +import ipaddress + LOG = logging.getLogger(__name__) AAP_EXT_ALIAS = 'allowed-address-pairs' @@ -111,6 +113,11 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): if sec_grps and sec_grps.get('security_groups'): return sec_grps.get('security_groups')[0] + def _get_ethertype_for_ip(self, ip): + address = ipaddress.ip_address( + ip if six.text_type == type(ip) else six.u(ip)) + return 'IPv6' if address.version is 6 else 'IPv4' + def _update_security_group_rules(self, load_balancer, sec_grp_id): rules = self.neutron_client.list_security_group_rules( security_group_id=sec_grp_id) @@ -140,9 +147,11 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): if rule.get('port_range_max') in del_ports: self.neutron_client.delete_security_group_rule(rule.get('id')) + ethertype = self._get_ethertype_for_ip(load_balancer.vip.ip_address) for port in add_ports: self._create_security_group_rule(sec_grp_id, 'TCP', port_min=port, - port_max=port) + port_max=port, + ethertype=ethertype) # Currently we are using the VIP network for VRRP # so we need to open up the protocols for it @@ -152,7 +161,8 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): self._create_security_group_rule( sec_grp_id, constants.VRRP_PROTOCOL_NUM, - direction='ingress') + direction='ingress', + ethertype=ethertype) except neutron_client_exceptions.Conflict: # It's ok if this rule already exists pass @@ -162,7 +172,7 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver): try: self._create_security_group_rule( sec_grp_id, constants.AUTH_HEADER_PROTOCOL_NUMBER, - direction='ingress') + direction='ingress', ethertype=ethertype) except neutron_client_exceptions.Conflict: # It's ok if this rule already exists pass diff --git a/octavia/network/drivers/neutron/base.py b/octavia/network/drivers/neutron/base.py index 1a99fb3376..29b8358452 100644 --- a/octavia/network/drivers/neutron/base.py +++ b/octavia/network/drivers/neutron/base.py @@ -120,14 +120,15 @@ class BaseNeutronDriver(base.AbstractNetworkDriver): def _create_security_group_rule(self, sec_grp_id, protocol, direction='ingress', port_min=None, - port_max=None): + port_max=None, ethertype='IPv6'): rule = { 'security_group_rule': { 'security_group_id': sec_grp_id, 'direction': direction, 'protocol': protocol, 'port_range_min': port_min, - 'port_range_max': port_max + 'port_range_max': port_max, + 'ethertype': ethertype, } } self.neutron_client.create_security_group_rule(rule) diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py index 719d91d35b..04afd3b216 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server.py @@ -690,15 +690,17 @@ class ServerTestCase(base.TestCase): @mock.patch('subprocess.check_output') @mock.patch('shutil.copytree') @mock.patch('os.makedirs') - def test_plug_VIP(self, mock_makedirs, mock_copytree, mock_check_output, - mock_netns, mock_netns_create, mock_pyroute2, - mock_ifaddress, mock_interfaces): + def test_plug_vip4(self, mock_makedirs, mock_copytree, mock_check_output, + mock_netns, mock_netns_create, mock_pyroute2, + mock_ifaddress, mock_interfaces): - subnet_info = {'subnet_cidr': '10.0.0.0/24', - 'gateway': '10.0.0.1', - 'mac_address': '123'} + subnet_info = { + 'subnet_cidr': '203.0.113.0/24', + 'gateway': '203.0.113.1', + 'mac_address': '123' + } - # malformated ip + # malformed ip rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error', data=json.dumps(subnet_info), content_type='application/json') @@ -727,7 +729,7 @@ class ServerTestCase(base.TestCase): self.assertEqual(dict(details="No suitable network interface found"), json.loads(rv.data.decode('utf-8'))) - # One Interface down, Happy Path + # One Interface down, Happy Path IPv4 mock_interfaces.side_effect = [['blah']] mock_ifaddress.side_effect = [[netifaces.AF_LINK], {netifaces.AF_LINK: [{'addr': '123'}]}] @@ -789,6 +791,114 @@ class ServerTestCase(base.TestCase): 'message': 'Error plugging VIP'}, json.loads(rv.data.decode('utf-8'))) + @mock.patch('netifaces.interfaces') + @mock.patch('netifaces.ifaddresses') + @mock.patch('pyroute2.IPRoute') + @mock.patch('pyroute2.netns.create') + @mock.patch('pyroute2.NetNS') + @mock.patch('subprocess.check_output') + @mock.patch('shutil.copytree') + @mock.patch('os.makedirs') + def test_plug_vip6(self, mock_makedirs, mock_copytree, mock_check_output, + mock_netns, mock_netns_create, mock_pyroute2, + mock_ifaddress, mock_interfaces): + + subnet_info = { + 'subnet_cidr': '2001:db8::/32', + 'gateway': '2001:db8::1', + 'mac_address': '123' + } + + # malformed ip + rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error', + data=json.dumps(subnet_info), + content_type='application/json') + self.assertEqual(400, rv.status_code) + + # No subnet info + rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error') + self.assertEqual(400, rv.status_code) + + # No interface at all + mock_interfaces.side_effect = [[]] + rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + self.assertEqual(404, rv.status_code) + self.assertEqual(dict(details="No suitable network interface found"), + json.loads(rv.data.decode('utf-8'))) + + # Two interfaces down + mock_interfaces.side_effect = [['blah', 'blah2']] + mock_ifaddress.side_effect = [['blabla'], ['blabla']] + rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + self.assertEqual(404, rv.status_code) + self.assertEqual(dict(details="No suitable network interface found"), + json.loads(rv.data.decode('utf-8'))) + + # One Interface down, Happy Path IPv6 + 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/{}/network/interfaces.d/blah.cfg'.format( + consts.AMPHORA_NAMESPACE) + 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(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 blah blah:0\n' + 'iface blah inet6 auto\n\n' + 'iface blah: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') + mock_check_output.assert_called_with( + ['ip', 'netns', 'exec', consts.AMPHORA_NAMESPACE, + 'ifup', 'blah:0'], stderr=-2) + + mock_interfaces.side_effect = [['blah']] + mock_ifaddress.side_effect = [[netifaces.AF_LINK], + {netifaces.AF_LINK: [{'addr': '123'}]}] + mock_check_output.side_effect = [ + 'unplug1', + subprocess.CalledProcessError( + 7, 'test', RANDOM_ERROR), subprocess.CalledProcessError( + 7, 'test', RANDOM_ERROR)] + + m = self.useFixture(test_utils.OpenFixture(file_name)).mock_open + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + rv = self.app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + self.assertEqual(500, rv.status_code) + self.assertEqual( + {'details': RANDOM_ERROR, + 'message': 'Error plugging VIP'}, + json.loads(rv.data.decode('utf-8'))) + @mock.patch('pyroute2.NetNS') def test_get_interface(self, mock_netns): diff --git a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server_sysvinit.py b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server_sysvinit.py index 71fb20cc44..63c1163955 100644 --- a/octavia/tests/functional/amphorae/backend/agent/api_server/test_server_sysvinit.py +++ b/octavia/tests/functional/amphorae/backend/agent/api_server/test_server_sysvinit.py @@ -593,15 +593,17 @@ class ServerTestCase(base.TestCase): @mock.patch('subprocess.check_output') @mock.patch('shutil.copytree') @mock.patch('os.makedirs') - def test_plug_VIP(self, mock_makedirs, mock_copytree, mock_check_output, - mock_netns, mock_netns_create, mock_pyroute2, - mock_ifaddress, mock_interfaces): + def test_plug_vip4(self, mock_makedirs, mock_copytree, mock_check_output, + mock_netns, mock_netns_create, mock_pyroute2, + mock_ifaddress, mock_interfaces): - subnet_info = {'subnet_cidr': '10.0.0.0/24', - 'gateway': '10.0.0.1', - 'mac_address': '123'} + subnet_info = { + 'subnet_cidr': '203.0.113.0/24', + 'gateway': '203.0.113.1', + 'mac_address': '123' + } - # malformated ip + # malformed ip rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error', data=json.dumps(subnet_info), content_type='application/json') @@ -630,7 +632,7 @@ class ServerTestCase(base.TestCase): self.assertEqual(dict(details="No suitable network interface found"), json.loads(rv.data.decode('utf-8'))) - # One Interface down, Happy Path + # One Interface down, Happy Path IPv4 mock_interfaces.side_effect = [['blah']] mock_ifaddress.side_effect = [[netifaces.AF_LINK], {netifaces.AF_LINK: [{'addr': '123'}]}] @@ -692,6 +694,115 @@ class ServerTestCase(base.TestCase): 'message': 'Error plugging VIP'}, json.loads(rv.data.decode('utf-8'))) + @mock.patch('netifaces.interfaces') + @mock.patch('netifaces.ifaddresses') + @mock.patch('pyroute2.IPRoute') + @mock.patch('pyroute2.netns.create') + @mock.patch('pyroute2.NetNS') + @mock.patch('subprocess.check_output') + @mock.patch('shutil.copytree') + @mock.patch('os.makedirs') + def test_plug_vip6(self, mock_makedirs, mock_copytree, mock_check_output, + mock_netns, mock_netns_create, mock_pyroute2, + mock_ifaddress, mock_interfaces): + + subnet_info = { + 'subnet_cidr': '2001:db8::/32', + 'gateway': '2001:db8::1', + 'mac_address': '123' + } + + # malformed ip + rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error', + data=json.dumps(subnet_info), + content_type='application/json') + self.assertEqual(400, rv.status_code) + + # No subnet info + rv = self.app.post('/' + api_server.VERSION + '/plug/vip/error') + self.assertEqual(400, rv.status_code) + + # No interface at all + mock_interfaces.side_effect = [[]] + rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + self.assertEqual(404, rv.status_code) + self.assertEqual(dict(details="No suitable network interface found"), + json.loads(rv.data.decode('utf-8'))) + + # Two interfaces down + mock_interfaces.side_effect = [['blah', 'blah2']] + mock_ifaddress.side_effect = [['blabla'], ['blabla']] + rv = self.app.post('/' + api_server.VERSION + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + self.assertEqual(404, rv.status_code) + self.assertEqual(dict(details="No suitable network interface found"), + json.loads(rv.data.decode('utf-8'))) + + # One Interface down, Happy Path IPv6 + 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/blah.cfg'.format( + consts.AMPHORA_NAMESPACE) + m = mock.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(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 blah blah:0\n' + 'iface blah inet6 auto\n\n' + 'iface blah: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') + mock_check_output.assert_called_with( + ['ip', 'netns', 'exec', 'amphora-haproxy', 'ifup', + 'blah:0'], stderr=-2) + + mock_interfaces.side_effect = [['blah']] + mock_ifaddress.side_effect = [[netifaces.AF_LINK], + {netifaces.AF_LINK: [{'addr': '123'}]}] + mock_check_output.side_effect = [ + 'unplug1', + subprocess.CalledProcessError( + 7, 'test', RANDOM_ERROR), subprocess.CalledProcessError( + 7, 'test', RANDOM_ERROR)] + + m = mock.mock_open() + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + rv = self.app.post('/' + api_server.VERSION + + "/plug/vip/2001:db8::2", + content_type='application/json', + data=json.dumps(subnet_info)) + self.assertEqual(500, rv.status_code) + self.assertEqual( + {'details': RANDOM_ERROR, + 'message': 'Error plugging VIP'}, + json.loads(rv.data.decode('utf-8'))) + @mock.patch('pyroute2.NetNS') def test_get_interface(self, mock_netns): diff --git a/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py b/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py index b447aa2deb..51304e6bfc 100644 --- a/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py +++ b/octavia/tests/unit/amphorae/backends/agent/api_server/test_plug.py @@ -12,24 +12,109 @@ # License for the specific language governing permissions and limitations # under the License. +import os + import mock import netifaces from octavia.amphorae.backends.agent.api_server import plug import octavia.tests.unit.base as base +FAKE_CIDR_IPV4 = '10.0.0.0/24' +FAKE_GATEWAY_IPV4 = '10.0.0.1' +FAKE_IP_IPV4 = '10.0.0.2' +FAKE_CIDR_IPV6 = '2001:db8::/32' +FAKE_GATEWAY_IPV6 = '2001:db8::1' +FAKE_IP_IPV6 = '2001:db8::2' +FAKE_IP_IPV6_EXPANDED = '2001:0db8:0000:0000:0000:0000:0000:0002' +FAKE_MAC_ADDRESS = 'ab:cd:ef:00:ff:22' +FAKE_INTERFACE = 'eth0' + -@mock.patch.object(plug, "netifaces") class TestPlug(base.TestCase): + def setUp(self): + super(TestPlug, self).setUp() - def test__interface_by_mac_case_insensitive(self, mock_netifaces): - mock_netifaces.AF_LINK = netifaces.AF_LINK - mock_interface = 'eth0' - mock_netifaces.interfaces.return_value = [mock_interface] - mock_netifaces.ifaddresses.return_value = { + self.mock_netifaces = mock.patch.object(plug, "netifaces").start() + self.addCleanup(self.mock_netifaces.stop) + + # Set up our fake interface + self.mock_netifaces.AF_LINK = netifaces.AF_LINK + self.mock_netifaces.interfaces.return_value = [FAKE_INTERFACE] + self.mock_netifaces.ifaddresses.return_value = { netifaces.AF_LINK: [ - {'addr': 'ab:cd:ef:00:ff:22'} + {'addr': FAKE_MAC_ADDRESS.lower()} ] } - interface = plug._interface_by_mac('AB:CD:EF:00:FF:22') - self.assertEqual('eth0', interface) + + def test__interface_by_mac_case_insensitive(self): + interface = plug._interface_by_mac(FAKE_MAC_ADDRESS.upper()) + self.assertEqual(FAKE_INTERFACE, interface) + + @mock.patch.object(plug, "flask") + @mock.patch('pyroute2.IPRoute') + @mock.patch('pyroute2.netns.create') + @mock.patch('pyroute2.NetNS') + @mock.patch('subprocess.check_output') + @mock.patch('shutil.copytree') + @mock.patch('os.makedirs') + def test_plug_vip_ipv4(self, mock_makedirs, mock_copytree, + mock_check_output, mock_netns, mock_netns_create, + mock_pyroute2, mock_flask): + m = mock.mock_open() + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + plug.plug_vip( + vip=FAKE_IP_IPV4, + subnet_cidr=FAKE_CIDR_IPV4, + gateway=FAKE_GATEWAY_IPV4, + mac_address=FAKE_MAC_ADDRESS + ) + mock_flask.jsonify.assert_any_call({ + 'message': 'OK', + 'details': 'VIP {vip} plugged on interface {interface}'.format( + vip=FAKE_IP_IPV4, interface=FAKE_INTERFACE) + }) + + @mock.patch.object(plug, "flask") + @mock.patch('pyroute2.IPRoute') + @mock.patch('pyroute2.netns.create') + @mock.patch('pyroute2.NetNS') + @mock.patch('subprocess.check_output') + @mock.patch('shutil.copytree') + @mock.patch('os.makedirs') + def test_plug_vip_ipv6(self, mock_makedirs, mock_copytree, + mock_check_output, mock_netns, mock_netns_create, + mock_pyroute2, mock_flask): + m = mock.mock_open() + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + plug.plug_vip( + vip=FAKE_IP_IPV6, + subnet_cidr=FAKE_CIDR_IPV6, + gateway=FAKE_GATEWAY_IPV6, + mac_address=FAKE_MAC_ADDRESS + ) + mock_flask.jsonify.assert_any_call({ + 'message': 'OK', + 'details': 'VIP {vip} plugged on interface {interface}'.format( + vip=FAKE_IP_IPV6_EXPANDED, interface=FAKE_INTERFACE) + }) + + @mock.patch.object(plug, "flask") + @mock.patch('pyroute2.IPRoute') + @mock.patch('pyroute2.netns.create') + @mock.patch('pyroute2.NetNS') + @mock.patch('subprocess.check_output') + @mock.patch('shutil.copytree') + @mock.patch('os.makedirs') + def test_plug_vip_bad_ip(self, mock_makedirs, mock_copytree, + mock_check_output, mock_netns, mock_netns_create, + mock_pyroute2, mock_flask): + m = mock.mock_open() + with mock.patch('os.open'), mock.patch.object(os, 'fdopen', m): + plug.plug_vip( + vip="error", + subnet_cidr=FAKE_CIDR_IPV4, + gateway=FAKE_GATEWAY_IPV4, + mac_address=FAKE_MAC_ADDRESS + ) + mock_flask.jsonify.assert_any_call({'message': 'Invalid VIP'}) diff --git a/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py b/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py index 2346be0e47..6dd8d0fb92 100644 --- a/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py +++ b/octavia/tests/unit/network/drivers/neutron/test_allowed_address_pairs.py @@ -457,7 +457,8 @@ class TestAllowedAddressPairsDriver(base.TestCase): def test_update_vip(self): listeners = [data_models.Listener(protocol_port=80, peer_port=1024), data_models.Listener(protocol_port=443, peer_port=1025)] - lb = data_models.LoadBalancer(id='1', listeners=listeners) + vip = data_models.Vip(ip_address='10.0.0.2') + lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip) list_sec_grps = self.driver.neutron_client.list_security_groups list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]} fake_rules = { @@ -478,7 +479,8 @@ class TestAllowedAddressPairsDriver(base.TestCase): 'direction': 'ingress', 'protocol': 'TCP', 'port_range_min': 1024, - 'port_range_max': 1024 + 'port_range_max': 1024, + 'ethertype': 'IPv4' } } expected_create_rule_2 = { @@ -487,7 +489,8 @@ class TestAllowedAddressPairsDriver(base.TestCase): 'direction': 'ingress', 'protocol': 'TCP', 'port_range_min': 1025, - 'port_range_max': 1025 + 'port_range_max': 1025, + 'ethertype': 'IPv4' } } expected_create_rule_3 = { @@ -496,7 +499,8 @@ class TestAllowedAddressPairsDriver(base.TestCase): 'direction': 'ingress', 'protocol': 'TCP', 'port_range_min': 443, - 'port_range_max': 443 + 'port_range_max': 443, + 'ethertype': 'IPv4' } } create_rule.assert_has_calls([mock.call(expected_create_rule_1), @@ -508,7 +512,8 @@ class TestAllowedAddressPairsDriver(base.TestCase): data_models.Listener( protocol_port=443, provisioning_status=constants.PENDING_DELETE)] - lb = data_models.LoadBalancer(id='1', listeners=listeners) + vip = data_models.Vip(ip_address='10.0.0.2') + lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip) list_sec_grps = self.driver.neutron_client.list_security_groups list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]} fake_rules = { @@ -527,7 +532,8 @@ class TestAllowedAddressPairsDriver(base.TestCase): def test_update_vip_when_no_listeners(self): listeners = [] - lb = data_models.LoadBalancer(id='1', listeners=listeners) + vip = data_models.Vip(ip_address='10.0.0.2') + lb = data_models.LoadBalancer(id='1', listeners=listeners, vip=vip) list_sec_grps = self.driver.neutron_client.list_security_groups list_sec_grps.return_value = {'security_groups': [{'id': 'secgrp-1'}]} fake_rules = { diff --git a/releasenotes/notes/IPv6-support-953ef81ed8555fce.yaml b/releasenotes/notes/IPv6-support-953ef81ed8555fce.yaml new file mode 100644 index 0000000000..24c866b044 --- /dev/null +++ b/releasenotes/notes/IPv6-support-953ef81ed8555fce.yaml @@ -0,0 +1,7 @@ +--- +features: + - Adds support for IPv6 +upgrade: + - To support IPv6 a databse migration and amphora image update are required. +fixes: + - Resolves an issue with subnets larger than /24