neutron/neutron/tests/unit/db/test_l3_extra_gws_db.py

672 lines
24 KiB
Python

# Copyright (c) 2023 Canonical Ltd.
# All Rights Reserved.
#
# 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.
from unittest import mock
import copy
import netaddr
from neutron_lib.api.definitions import external_net as enet_apidef
from neutron_lib.api.definitions import l3 as l3_apidef
from neutron_lib.api.definitions import l3_ext_gw_mode
from neutron_lib.api.definitions import l3_ext_gw_multihoming
from neutron_lib import constants
from neutron_lib.db import api as db_api
from neutron_lib import exceptions
from neutron_lib.utils import net as net_utils
from oslo_utils import uuidutils
from neutron.db import l3_extra_gws_db
from neutron.db.models import l3 as l3_models
from neutron.ipam import exceptions as ipam_exceptions
from neutron.objects import ipam as ipam_obj
from neutron.objects import network as net_obj
from neutron.objects import ports as port_obj
from neutron.objects import router as l3_obj
from neutron.objects import subnet as subnet_obj
from neutron.tests.unit.extensions import test_l3
from neutron.tests.unit import testlib_api
_uuid = uuidutils.generate_uuid
class TestDbIntPlugin(test_l3.TestL3NatIntPlugin,
l3_extra_gws_db.ExtraGatewaysMixinDbMixin):
supported_extension_aliases = [enet_apidef.ALIAS, l3_apidef.ALIAS,
l3_ext_gw_mode.ALIAS,
l3_ext_gw_multihoming.ALIAS]
class TestExtraGatewaysDb(testlib_api.SqlTestCase):
def setUp(self):
super().setUp()
plugin = __name__ + '.' + TestDbIntPlugin.__name__
self.setup_coreplugin(plugin)
self.target_object = TestDbIntPlugin()
# Patch the context
ctx_patcher = mock.patch('neutron_lib.context', autospec=True)
mock_context = ctx_patcher.start()
self.context = mock_context.get_admin_context()
self.context.elevated.return_value = self.context
self.context.session = db_api.get_writer_session()
# Create a simple setup with one external network and a subnet on it.
self.ext_net_a_id = _uuid()
self.ext_sub_a_id = _uuid()
self.tenant_id = _uuid()
self.network_a = net_obj.Network(
self.context,
id=self.ext_net_a_id,
project_id=self.tenant_id,
admin_state_up=True,
status=constants.NET_STATUS_ACTIVE)
self.network_a.create()
self.net_ext_a = net_obj.ExternalNetwork(
self.context, network_id=self.ext_net_a_id)
self.net_ext_a.create()
self.ext_sub_a = subnet_obj.Subnet(self.context,
id=self.ext_sub_a_id,
project_id=self.tenant_id,
ip_version=constants.IP_VERSION_4,
cidr=net_utils.AuthenticIPNetwork('192.0.2.0/25'),
gateway_ip=netaddr.IPAddress('192.0.2.1'),
network_id=self.ext_net_a_id)
self.ext_sub_a.create()
self.ext_net_b_id = _uuid()
self.ext_sub_b_id = _uuid()
self.network_b = net_obj.Network(
self.context,
id=self.ext_net_b_id,
project_id=self.tenant_id,
admin_state_up=True,
status=constants.NET_STATUS_ACTIVE)
self.network_b.create()
self.net_ext_b = net_obj.ExternalNetwork(
self.context, network_id=self.ext_net_b_id)
self.net_ext_b.create()
self.ext_sub_b = subnet_obj.Subnet(
self.context,
id=self.ext_sub_b_id,
project_id=self.tenant_id,
ip_version=constants.IP_VERSION_4,
cidr=net_utils.AuthenticIPNetwork('192.0.2.128/25'),
gateway_ip=netaddr.IPAddress('192.0.2.129'),
network_id=self.ext_net_b_id)
self.ext_sub_b.create()
self.ext_net_c_id = _uuid()
self.ext_sub_c_id = _uuid()
self.network_c = net_obj.Network(
self.context,
id=self.ext_net_c_id,
project_id=self.tenant_id,
admin_state_up=True,
status=constants.NET_STATUS_ACTIVE)
self.network_c.create()
self.net_ext_c = net_obj.ExternalNetwork(
self.context, network_id=self.ext_net_c_id)
self.net_ext_c.create()
self.ext_sub_c = subnet_obj.Subnet(
self.context,
id=self.ext_sub_c_id,
project_id=self.tenant_id,
ip_version=constants.IP_VERSION_4,
# Overlaps with subnet A above on purpose for overlap testing.
cidr=net_utils.AuthenticIPNetwork('192.0.2.0/25'),
gateway_ip=netaddr.IPAddress('192.0.2.1'),
network_id=self.ext_net_c_id)
self.ext_sub_c.create()
# Create an IPAM subnet for fixed ip allocations.
self.ipam_ext_subnet_a = ipam_obj.IpamSubnet(
self.context,
neutron_subnet_id=self.ext_sub_a_id)
self.ipam_ext_subnet_a.create()
self.ipam_ext_subnet_b = ipam_obj.IpamSubnet(
self.context,
neutron_subnet_id=self.ext_sub_b_id)
self.ipam_ext_subnet_b.create()
self.ipam_ext_subnet_c = ipam_obj.IpamSubnet(
self.context,
neutron_subnet_id=self.ext_sub_c_id)
self.ipam_ext_subnet_c.create()
# Create an allocation pool that will use the IPAM subnet.
self.ipam_ext_pool_a = ipam_obj.IpamAllocationPool(
self.context,
id=_uuid(),
ipam_subnet_id=self.ipam_ext_subnet_a.id,
first_ip='192.0.2.3',
last_ip='192.0.2.126',
)
self.ipam_ext_pool_a.create()
self.ipam_ext_pool_b = ipam_obj.IpamAllocationPool(
self.context,
id=_uuid(),
ipam_subnet_id=self.ipam_ext_subnet_b.id,
first_ip='192.0.2.131',
last_ip='192.0.2.254',
)
self.ipam_ext_pool_b.create()
self.ipam_ext_pool_c = ipam_obj.IpamAllocationPool(
self.context,
id=_uuid(),
ipam_subnet_id=self.ipam_ext_subnet_c.id,
first_ip='192.0.2.3',
last_ip='192.0.2.126',
)
self.ipam_ext_pool_c.create()
# Create a router that will be modified during the tests.
self.router = l3_models.Router(
id=_uuid(),
name=None,
tenant_id=self.tenant_id,
admin_state_up=True,
status=constants.NET_STATUS_ACTIVE,
enable_snat=True,
gw_port_id=None)
self.context.session.add(self.router)
self.context.session.expire_all()
self.context.session.commit()
def test_add_external_gateways_trivial(self):
ext_gws = []
body = {
"router": {
"external_gateways": ext_gws
}
}
# A trivial case with an empty list passed in.
result = self.target_object._add_external_gateways(
self.context, self.router.id, ext_gws, body)
self.assertEqual([], result)
def test_add_external_gateways_single(self):
ext_gws = [{"network_id": self.ext_net_a_id}]
body = {
"router": {
"external_gateways": ext_gws
}
}
result = self.target_object.add_external_gateways(
self.context, self.router.id, body)
res_gw_a = result['router']['external_gateways'][0]
self.assertEqual(res_gw_a['network_id'], self.ext_net_a_id)
self.assertIsNotNone(res_gw_a['external_fixed_ips'])
new_router = self.target_object.get_router(self.context,
self.router.id)
new_gw_info = new_router['external_gateway_info']
self.assertEqual(new_gw_info['network_id'], self.ext_net_a_id)
self.assertIsNotNone(new_gw_info['external_fixed_ips'])
gw_ports = port_obj.Port.get_ports_by_router_and_network(
self.context, self.router.id, constants.DEVICE_OWNER_ROUTER_GW,
self.ext_net_a_id)
self.assertEqual(len(gw_ports), 1)
gw_port = gw_ports[0]
self.assertEqual(new_router['gw_port_id'], gw_port['id'])
def test_add_external_gateways_multiple(self):
ext_gws = [
{"network_id": self.ext_net_a_id},
{"network_id": self.ext_net_b_id},
]
body = {
"router": {
"external_gateways": ext_gws
}
}
result = self.target_object.add_external_gateways(
self.context, self.router.id, body)
res_gw_a = result['router']['external_gateways'][0]
res_gw_b = result['router']['external_gateways'][1]
self.assertEqual(res_gw_a['network_id'], self.ext_net_a_id)
self.assertEqual(res_gw_b['network_id'], self.ext_net_b_id)
self.assertIsNotNone(res_gw_a['external_fixed_ips'])
self.assertIsNotNone(res_gw_b['external_fixed_ips'])
new_router = self.target_object.get_router(self.context,
self.router.id)
new_gw_info = new_router['external_gateway_info']
self.assertEqual(new_gw_info['network_id'], self.ext_net_a_id)
self.assertIsNotNone(new_gw_info['external_fixed_ips'])
gw_ports = l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})
self.assertEqual(len(gw_ports), 2)
# Now check that calling the ADD API multiple times succeeds.
ext_gws = [
{"network_id": self.ext_net_b_id},
]
body = {
"router": {
"external_gateways": ext_gws
}
}
result = self.target_object.add_external_gateways(
self.context, self.router.id, body)
res_gw_a = result['router']['external_gateways'][0]
res_gw_b = result['router']['external_gateways'][1]
res_gw_c = result['router']['external_gateways'][2]
self.assertEqual(res_gw_a['network_id'], self.ext_net_a_id)
self.assertEqual(res_gw_b['network_id'], self.ext_net_b_id)
self.assertEqual(res_gw_c['network_id'], self.ext_net_b_id)
self.assertIsNotNone(res_gw_a['external_fixed_ips'])
self.assertIsNotNone(res_gw_b['external_fixed_ips'])
self.assertIsNotNone(res_gw_c['external_fixed_ips'])
new_router = self.target_object.get_router(self.context,
self.router.id)
new_gw_info = new_router['external_gateway_info']
self.assertEqual(new_gw_info['network_id'], self.ext_net_a_id)
self.assertIsNotNone(new_gw_info['external_fixed_ips'])
gw_ports = l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})
self.assertEqual(len(gw_ports), 3)
# Check that adding a gateway with already allocated fixed IPs fails.
ext_gws = [
{"network_id": self.ext_net_b_id,
"external_fixed_ips": res_gw_c['external_fixed_ips']},
]
body = {
"router": {
"external_gateways": ext_gws
}
}
self.assertRaises(
ipam_exceptions.IpAddressAlreadyAllocated,
self.target_object.add_external_gateways, self.context,
self.router.id, body
)
def test_remove_external_gateways_trivial(self):
ext_gws = []
body = {
"router": {
"external_gateways": ext_gws
}
}
# A trivial case with an empty list passed in.
result = self.target_object.remove_external_gateways(
self.context, self.router.id, body)
self.assertIsNone(result['router']['external_gateway_info'])
def test_remove_external_gateways_single(self):
ext_gws = [{"network_id": self.ext_net_a_id}]
body = {
"router": {
"external_gateways": ext_gws
}
}
self.target_object.add_external_gateways(
self.context, self.router.id, body)
self.assertIsNotNone(self.router.gw_port_id)
result = self.target_object.remove_external_gateways(
self.context, self.router.id, body)
self.assertIsNone(self.router.gw_port_id)
self.assertIsNone(result['router']['external_gateway_info'])
def test_remove_external_gateways_multiple(self):
ext_gws = [
{"network_id": self.ext_net_a_id},
{"network_id": self.ext_net_b_id},
]
body = {
"router": {
"external_gateways": ext_gws
}
}
self.target_object.add_external_gateways(
self.context, self.router.id, body)
self.assertIsNotNone(self.router.gw_port_id)
gw_ports = l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})
self.assertEqual(len(gw_ports), 2)
result = self.target_object.remove_external_gateways(
self.context, self.router.id, body)
self.assertIsNone(self.router.gw_port_id)
self.assertIsNone(result['router']['external_gateway_info'])
gw_ports = l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})
self.assertEqual(len(gw_ports), 0)
def test_remove_external_gateways_remove_compat(self):
'''Test removal of a compatibility gateway port using the new API.
When removing a compatibility gateway port using the new API we need
to make sure that an existing extra gateway port takes it place instead
as a compatibility gateway port.
'''
ext_gws = [
{"network_id": self.ext_net_a_id},
{"network_id": self.ext_net_b_id},
]
add_body = {
"router": {
"external_gateways": ext_gws
}
}
remove_body = {
"router": {
"external_gateways": ext_gws[:1]
}
}
self.target_object.add_external_gateways(
self.context, self.router.id, add_body)
self.assertIsNotNone(self.router.gw_port_id)
old_gw_port_id = self.router.gw_port_id
gw_ports = l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})
self.assertEqual(len(gw_ports), 2)
self.target_object.remove_external_gateways(
self.context, self.router.id, remove_body)
gw_ports = l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})
self.assertEqual(len(gw_ports), 1)
new_router = self.target_object.get_router(self.context,
self.router.id)
self.assertNotEqual(old_gw_port_id, new_router['gw_port_id'])
self.assertEqual(new_router['external_gateway_info']['network_id'],
self.ext_net_b_id)
def test_update_external_gateways_add_pristine_and_remove(self):
'''Test the addition of external gateway ports using the update API.'''
ext_gws = [
{"network_id": self.ext_net_a_id},
{"network_id": self.ext_net_b_id},
]
add_body = {
"router": {
"external_gateways": ext_gws
}
}
result = self.target_object.update_external_gateways(
self.context, self.router.id, add_body)
self.assertIsNotNone(self.router.gw_port_id)
res_gw_a = result['router']['external_gateways'][0]
res_gw_b = result['router']['external_gateways'][1]
self.assertEqual(res_gw_a['network_id'], self.ext_net_a_id)
self.assertEqual(res_gw_b['network_id'], self.ext_net_b_id)
self.assertIsNotNone(res_gw_a['external_fixed_ips'])
self.assertIsNotNone(res_gw_b['external_fixed_ips'])
new_router = self.target_object.get_router(self.context,
self.router.id)
new_gw_info = new_router['external_gateway_info']
self.assertEqual(new_gw_info['network_id'], self.ext_net_a_id)
self.assertIsNotNone(new_gw_info['external_fixed_ips'])
gw_ports = l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})
self.assertEqual(len(gw_ports), 2)
# Reorder gateways.
ext_gws = [
{"network_id": self.ext_net_b_id},
{"network_id": self.ext_net_a_id},
]
update_body = {
"router": {
"external_gateways": ext_gws
}
}
result = self.target_object.update_external_gateways(
self.context, self.router.id, update_body)
self.assertIsNotNone(self.router.gw_port_id)
new_router = self.target_object.get_router(self.context,
self.router.id)
new_gw_info = new_router['external_gateway_info']
self.assertEqual(new_gw_info['network_id'], self.ext_net_b_id)
self.assertIsNotNone(new_gw_info['external_fixed_ips'])
# The compat gateway should now have a different network_id.
res_gw_b = result['router']['external_gateways'][0]
res_gw_a = result['router']['external_gateways'][1]
self.assertEqual(res_gw_b['network_id'], self.ext_net_b_id)
self.assertEqual(res_gw_a['network_id'], self.ext_net_a_id)
self.assertIsNotNone(res_gw_a['external_fixed_ips'])
self.assertIsNotNone(res_gw_b['external_fixed_ips'])
# Remove one gateway.
update_body = {
"router": {
"external_gateways": ext_gws[1:]
}
}
result = self.target_object.update_external_gateways(
self.context, self.router.id, update_body)
self.assertIsNotNone(self.router.gw_port_id)
new_router = self.target_object.get_router(self.context,
self.router.id)
new_gw_info = new_router['external_gateway_info']
self.assertEqual(new_gw_info['network_id'], self.ext_net_a_id)
self.assertIsNotNone(new_gw_info['external_fixed_ips'])
res_gw_a = result['router']['external_gateways'][0]
self.assertEqual(res_gw_a['network_id'], self.ext_net_a_id)
self.assertIsNotNone(res_gw_a['external_fixed_ips'])
# Clear all gateways.
update_body = {
"router": {
"external_gateways": {}
}
}
result = self.target_object.update_external_gateways(
self.context, self.router.id, update_body)
self.assertIsNone(self.router.gw_port_id)
def test_compat_remove_via_update(self):
'''Test the removal of a gateway port using the compat API.
Removal of a compat gateway in the presence of an extra
gateway port should make that extra gateway port a compat
gateway port.
'''
ext_gws = [
{"network_id": self.ext_net_a_id},
{"network_id": self.ext_net_b_id},
]
body = {
"router": {
"external_gateways": ext_gws
}
}
result = self.target_object.add_external_gateways(
self.context, self.router.id, body)
update_body = {
"router": {
"external_gateway_info": {}
}
}
# Now perform an update with an empty gw info to remove the current
# compat gw port.
result = self.target_object.update_router(self.context, self.router.id,
update_body)
# The existing extra gateway port should now take its place.
res_gw = result['external_gateways'][0]
self.assertEqual(res_gw['network_id'], self.ext_net_b_id)
self.assertIsNotNone(res_gw['external_fixed_ips'])
self.assertEqual(len(result['external_gateways']), 1)
new_router = self.target_object.get_router(self.context,
self.router.id)
new_gw_info = new_router['external_gateway_info']
self.assertEqual(new_gw_info['network_id'], self.ext_net_b_id)
self.assertIsNotNone(new_gw_info['external_fixed_ips'])
gw_ports = l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})
self.assertEqual(len(gw_ports), 1)
def test_update_fixed_ip(self):
'''Test updating a fixed IP of an existing port.'''
ext_gws = [
{"network_id": self.ext_net_a_id},
{"network_id": self.ext_net_b_id},
]
body = {
"router": {
"external_gateways": ext_gws
}
}
result = self.target_object.add_external_gateways(
self.context, self.router.id, body)
gw_ports_initial = [o.port_id for o in l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})]
fips = copy.deepcopy(
result['router']['external_gateways'][1]['external_fixed_ips'])
# Append a fixed ip not used in the allocation pool. The existing
# one should be used to find an existing port.
fips.append({'ip_address': '192.0.2.130',
'subnet_id': fips[0]['subnet_id']})
expected_fips = copy.deepcopy(fips)
update_body = {
"router": {
"external_gateways": [
{"network_id": self.ext_net_a_id},
# Use the new set of fixed IPs in the request.
{"network_id": self.ext_net_b_id,
"external_fixed_ips": fips},
]}
}
result = self.target_object.update_external_gateways(
self.context, self.router.id,
update_body)
self.assertCountEqual(
result['router']['external_gateways'][1]['external_fixed_ips'],
expected_fips,
)
gw_ports_final = [o.port_id for o in l3_obj.RouterPort.get_objects(
self.context,
**{'router_id': self.router.id,
'port_type': constants.DEVICE_OWNER_ROUTER_GW})]
# Make sure the ports are not recreated in the process, i.e. port IDs
# stay the same.
self.assertCountEqual(
gw_ports_initial,
gw_ports_final,
)
def test_add_external_gateways_overlapping_subnets(self):
ext_gws = [
{"network_id": self.ext_net_a_id},
]
body = {
"router": {
"external_gateways": ext_gws
}
}
self.target_object.add_external_gateways(self.context, self.router.id,
body)
ext_gws = [
{"network_id": self.ext_net_c_id},
]
body = {
"router": {
"external_gateways": ext_gws
}
}
self.assertRaisesRegex(
exceptions.BadRequest,
'Bad router request: Cidr 192.0.2.0/25 of subnet'
f' {self.ext_sub_c_id} overlaps with cidr 192.0.2.0/25 of '
f'subnet {self.ext_sub_a_id}.',
self.target_object.add_external_gateways, self.context,
self.router.id, body
)