Merge "Allow Multiple External Gateways"
This commit is contained in:
commit
6626fd9c9c
@ -348,7 +348,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
|
||||
states=(original, updated)))
|
||||
return updated
|
||||
|
||||
def _create_router_gw_port(self, context, router, network_id, ext_ips):
|
||||
def _create_router_gw_port(self, context, router, network_id, ext_ips,
|
||||
update_gw_port=True):
|
||||
# Port has no 'tenant-id', as it is hidden from user
|
||||
port_data = {'tenant_id': '', # intentionally not set
|
||||
'network_id': network_id,
|
||||
@ -374,6 +375,7 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
|
||||
self._core_plugin, context.elevated(), gw_port['id']):
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
router = self._get_router(context, router['id'])
|
||||
if update_gw_port:
|
||||
router.gw_port = self._core_plugin._get_port(
|
||||
context.elevated(), gw_port['id'])
|
||||
router_port = l3_obj.RouterPort(
|
||||
@ -512,9 +514,11 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
|
||||
# raise the underlying exception
|
||||
raise e.errors[0].error
|
||||
|
||||
self._check_for_dup_router_subnets(context, router,
|
||||
new_network_id,
|
||||
subnets)
|
||||
self._check_for_dup_router_subnets(
|
||||
context, router,
|
||||
subnets,
|
||||
constants.DEVICE_OWNER_ROUTER_GW
|
||||
)
|
||||
self._create_router_gw_port(context, router,
|
||||
new_network_id, ext_ips)
|
||||
|
||||
@ -676,7 +680,7 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
|
||||
query_field=l3_models.Router.id.key)
|
||||
|
||||
def _check_for_dup_router_subnets(self, context, router,
|
||||
network_id, new_subnets):
|
||||
new_subnets, new_device_owner):
|
||||
# It's possible these ports are on the same network, but
|
||||
# different subnets.
|
||||
new_subnet_ids = {s['id'] for s in new_subnets}
|
||||
@ -796,8 +800,7 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
|
||||
|
||||
if subnets:
|
||||
self._check_for_dup_router_subnets(context, router,
|
||||
port['network_id'],
|
||||
subnets)
|
||||
subnets, port['device_owner'])
|
||||
|
||||
# Keep the restriction against multiple IPv4 subnets
|
||||
if len([s for s in subnets if s['ip_version'] == 4]) > 1:
|
||||
@ -903,8 +906,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase,
|
||||
% subnet_id)
|
||||
raise n_exc.BadRequest(resource='router', msg=msg)
|
||||
self._validate_subnet_address_mode(subnet)
|
||||
self._check_for_dup_router_subnets(context, router,
|
||||
subnet['network_id'], [subnet])
|
||||
self._check_for_dup_router_subnets(context, router, [subnet],
|
||||
constants.DEVICE_OWNER_ROUTER_INTF)
|
||||
fixed_ip = {'ip_address': subnet['gateway_ip'],
|
||||
'subnet_id': subnet['id']}
|
||||
|
||||
|
565
neutron/db/l3_extra_gws_db.py
Normal file
565
neutron/db/l3_extra_gws_db.py
Normal file
@ -0,0 +1,565 @@
|
||||
# Copyright (c) 2023 Canonical Ltd.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import copy
|
||||
|
||||
import netaddr
|
||||
|
||||
from neutron._i18n import _
|
||||
from neutron.db import l3_db
|
||||
from neutron.db import l3_gwmode_db
|
||||
from neutron.objects import ports as port_obj
|
||||
from neutron.objects import router as l3_obj
|
||||
from neutron_lib.api.definitions import l3 as l3_apidef
|
||||
from neutron_lib.api.definitions import l3_ext_gw_multihoming
|
||||
from neutron_lib.api import extensions
|
||||
from neutron_lib.callbacks import events
|
||||
from neutron_lib.callbacks import registry
|
||||
from neutron_lib.callbacks import resources
|
||||
from neutron_lib import constants
|
||||
from neutron_lib.db import api as db_api
|
||||
from neutron_lib.db import resource_extend
|
||||
from neutron_lib.exceptions import l3 as l3_exc
|
||||
from neutron_lib.exceptions import l3_ext_gw_multihoming as mh_exc
|
||||
from neutron_lib.plugins import constants as plugin_constants
|
||||
from neutron_lib.plugins import directory
|
||||
|
||||
|
||||
def format_gateway_info(gw_port):
|
||||
return {
|
||||
'network_id': gw_port.network_id,
|
||||
'external_fixed_ips': [{
|
||||
'ip_address': str(alloc.ip_address),
|
||||
'subnet_id': alloc.subnet_id,
|
||||
} for alloc in gw_port.fixed_ips]
|
||||
}
|
||||
|
||||
|
||||
@resource_extend.has_resource_extenders
|
||||
class ExtraGatewaysDbOnlyMixin(l3_gwmode_db.L3_NAT_dbonly_mixin):
|
||||
"""A mixin class to expose a router's extra external gateways."""
|
||||
|
||||
@staticmethod
|
||||
@resource_extend.extends([l3_apidef.ROUTERS])
|
||||
def _extend_router_dict_extra_gateways(router_res, router_db):
|
||||
l3_plugin = directory.get_plugin(plugin_constants.L3)
|
||||
if not extensions.is_extension_supported(
|
||||
l3_plugin, l3_ext_gw_multihoming.ALIAS):
|
||||
return
|
||||
|
||||
external_gateways = []
|
||||
for gw_port in [
|
||||
rp.port
|
||||
for rp in router_db.attached_ports
|
||||
if rp.port.device_owner == constants.DEVICE_OWNER_ROUTER_GW]:
|
||||
if gw_port.id == router_db.gw_port_id:
|
||||
external_gateways.insert(0, format_gateway_info(gw_port))
|
||||
else:
|
||||
external_gateways.append(format_gateway_info(gw_port))
|
||||
|
||||
router_res[l3_ext_gw_multihoming.EXTERNAL_GATEWAYS] = external_gateways
|
||||
|
||||
@registry.receives(resources.ROUTER, [events.BEFORE_DELETE])
|
||||
def _delete_router_remove_external_gateways(self, resource, event,
|
||||
trigger, payload):
|
||||
self._remove_all_gateways(payload.context, payload.resource_id)
|
||||
|
||||
def _add_external_gateways(
|
||||
self, context, router_id, gw_info_list, payload):
|
||||
"""Add external gateways to a router."""
|
||||
added_gateways = []
|
||||
if not gw_info_list:
|
||||
return added_gateways
|
||||
|
||||
# If a router already has extra gateways specified then they need to
|
||||
# be changed via the update API.
|
||||
router_db = self._get_router(context, router_id)
|
||||
|
||||
if any(rp.port.device_owner == constants.DEVICE_OWNER_ROUTER_GW
|
||||
for rp in router_db.attached_ports):
|
||||
# Matching for gateway ports with the same network_id and set of
|
||||
# fixed_ips is not needed since an IP allocation would fail in this
|
||||
# case. And if fixed IPs don't overlap or are not specified a new
|
||||
# port will simply be created.
|
||||
extra_gw_info = gw_info_list
|
||||
else:
|
||||
compat_gw_info = gw_info_list[0]
|
||||
compat_payload = copy.deepcopy(payload)
|
||||
compat_payload['router'].pop('external_gateways')
|
||||
compat_payload['external_gateway_info'] = compat_gw_info
|
||||
|
||||
# Update the first router gateway since we treat it in a special
|
||||
# way for compatibility.
|
||||
self._update_router_gw_info(context, router_id, compat_gw_info,
|
||||
compat_payload)
|
||||
added_gateways.append(compat_gw_info)
|
||||
|
||||
extra_gw_info = gw_info_list[1:]
|
||||
|
||||
# Go over extra gateway ports and add them to the router.
|
||||
for gw_info in extra_gw_info:
|
||||
# The ``_validate_gw_info`` and ``_create_extra_gw_port`` methods
|
||||
# need an updated version of the router_db object, both as a
|
||||
# result of the ``_update_router_gw_info`` call above, and as
|
||||
# ports are added.
|
||||
router_db = self._get_router(context, router_id)
|
||||
|
||||
# Here we do not need to check for external gateway port IP changes
|
||||
# as there are no ports yet.
|
||||
ext_ips = gw_info.get('external_fixed_ips', [])
|
||||
|
||||
network_id = self._validate_gw_info(context, gw_info,
|
||||
ext_ips, router_db)
|
||||
self._create_extra_gw_port(context, router_db,
|
||||
network_id, ext_ips)
|
||||
added_gateways.append(gw_info)
|
||||
|
||||
return added_gateways
|
||||
|
||||
def _create_extra_gw_port(self, context, router_db, new_network_id,
|
||||
ext_ips):
|
||||
with db_api.CONTEXT_READER.using(context):
|
||||
# This function should only be used when we have a compat port id
|
||||
# added using the compat API that expects one gateway only.
|
||||
if not router_db.gw_port:
|
||||
raise mh_exc.UnableToAddExtraGateways(
|
||||
router_id=router_db.id,
|
||||
reason=_('router does not have a compatibility gateway '
|
||||
'port'))
|
||||
|
||||
if not new_network_id:
|
||||
return
|
||||
|
||||
subnets = self._core_plugin.get_subnets_by_network(context,
|
||||
new_network_id)
|
||||
# TODO(dmitriis): publish an events.BEFORE_CREATE event for a new
|
||||
# resource type e.g. resources.ROUTER_EXTRA_GATEWAY. Semantically
|
||||
# this is a different resource from resources.ROUTER_GATEWAY.
|
||||
self._check_for_dup_router_subnets(
|
||||
context, router_db,
|
||||
subnets,
|
||||
constants.DEVICE_OWNER_ROUTER_GW
|
||||
)
|
||||
self._create_router_gw_port(context, router_db,
|
||||
new_network_id, ext_ips,
|
||||
update_gw_port=False)
|
||||
|
||||
# TODO(dmitriis): publish an events.AFTER_CREATE event for a new
|
||||
# resource type e.g. resources.ROUTER_EXTRA_GATEWAY. Semantically
|
||||
# this is a different resource from resources.ROUTER_GATEWAY.
|
||||
|
||||
def _check_for_dup_router_subnets(self, context, router_db,
|
||||
new_subnets, new_device_owner):
|
||||
"""Check for overlapping subnets on different networks.
|
||||
|
||||
This method overrides the one in the base class so the logic will be
|
||||
triggered for both the compatibility code that might alter the state
|
||||
of a single gateway port in the presence of multiple gateway ports
|
||||
(without an override it could result in overlap errors that are not
|
||||
relevant with the code base supporting multiple gateway ports attached
|
||||
to the same network).
|
||||
|
||||
It is possible to have multiple gateway ports attached to the same
|
||||
external network which will cause subnets of ports to overlap but will
|
||||
not cause issues with routing. However, attaching multiple gateway
|
||||
ports to different networks with overlapping subnet ranges will cause
|
||||
routing issues. This function checks for that kind of overlap in
|
||||
addition to the compatibility cases such as an overlap between
|
||||
internal and external network subnets. This is done using the
|
||||
device owner field of a port that is planned to be created by the
|
||||
caller: specifically, based on that this argument the method can
|
||||
tell if new subnets are meant to be associated with a gateway port
|
||||
or an internal port.
|
||||
|
||||
:param context: neutron API request context
|
||||
:type context: neutron_lib.context.Context
|
||||
:param router_db: The router db object to do a check for.
|
||||
:type router: neutron.db.models.l3.Router
|
||||
:param new_subnets: A list of new subnets to be added to the router
|
||||
:type new_subnets: list[neutron.db.models_v2.Subnet]
|
||||
:param new_device_owner: A device owner field for the port that is
|
||||
going to be created with new subnets.
|
||||
"""
|
||||
router_subnets = []
|
||||
ext_subnets = set()
|
||||
for p in (rp.port for rp in router_db.attached_ports):
|
||||
for ip in p['fixed_ips']:
|
||||
existing_port_owner = p.get('device_owner')
|
||||
if existing_port_owner == constants.DEVICE_OWNER_ROUTER_GW:
|
||||
ext_subts = self._core_plugin.get_subnets(
|
||||
context.elevated(),
|
||||
filters={'network_id': [p['network_id']]})
|
||||
for sub in ext_subts:
|
||||
router_subnets.append(sub['id'])
|
||||
ext_subnets.add(sub['id'])
|
||||
else:
|
||||
router_subnets.append(ip['subnet_id'])
|
||||
if not router_subnets:
|
||||
return
|
||||
|
||||
# Ignore temporary Prefix Delegation CIDRs
|
||||
new_subnets = [s for s in new_subnets
|
||||
if s['cidr'] != constants.PROVISIONAL_IPV6_PD_PREFIX]
|
||||
id_filter = {'id': router_subnets}
|
||||
subnets = self._core_plugin.get_subnets(context.elevated(),
|
||||
filters=id_filter)
|
||||
for sub in subnets:
|
||||
for new_s in new_subnets:
|
||||
# Overlapping subnet ranges are a problem if there is an
|
||||
# overlap between subnets on different external networks,
|
||||
# between internal and external networks or internal networks
|
||||
# (including the case where an attempt to add multiple internal
|
||||
# ports on the same subnet is made for the same router).
|
||||
if not (new_s['id'] in ext_subnets and
|
||||
new_device_owner == constants.DEVICE_OWNER_ROUTER_GW):
|
||||
self._raise_on_subnets_overlap(sub, new_s)
|
||||
|
||||
def _match_requested_gateway_ports(self, context, router_id,
|
||||
gw_info_list):
|
||||
"""Match indirect references to gateway ports to the actual ports.
|
||||
|
||||
Returns 3 parameters:
|
||||
|
||||
1. A dictionary which maps matched gateway port ids to
|
||||
external_gateway_info dictionaries as they were passed in
|
||||
2. A dict with partial matches on fixed ips
|
||||
3. A list of gateway info dictionaries for which there aren't any
|
||||
existing gateway ports.
|
||||
"""
|
||||
matched_port_ids = {}
|
||||
part_matched_port_ids = {}
|
||||
nonexistent_port_info = []
|
||||
for gw_info in gw_info_list:
|
||||
net_id = gw_info['network_id']
|
||||
# Find any gateways that might be attached to the same network.
|
||||
gw_ports = port_obj.Port.get_ports_by_router_and_network(
|
||||
context, router_id, constants.DEVICE_OWNER_ROUTER_GW, net_id)
|
||||
|
||||
if not gw_ports:
|
||||
nonexistent_port_info.append(gw_info)
|
||||
continue
|
||||
|
||||
if not gw_info.get('external_fixed_ips'):
|
||||
# Allow for one case where external_fixed_ips are not specified
|
||||
# in the request but there is only one gateway port attached to
|
||||
# particular network on a router - there is no ambiguity about
|
||||
# which port do we want to find in this case.
|
||||
if len(gw_ports) == 1:
|
||||
gw_port = gw_ports[0]
|
||||
part_matched_port_ids[gw_port['id']] = gw_info
|
||||
continue
|
||||
# Matching to specific fixed IPs of gateway ports is done
|
||||
# based on the parameters of a request, otherwise it would
|
||||
# be unclear which one of the gateway ports to match to.
|
||||
raise mh_exc.UnableToMatchGateways(
|
||||
router_id=router_id,
|
||||
reason=_(
|
||||
'multiple gateway ports are attached to the same '
|
||||
'network %s but external_fixed_ips parameter '
|
||||
'is not specified in the request') % net_id)
|
||||
|
||||
for gw_port in gw_ports:
|
||||
current_set = set([a.ip_address for a in gw_port['fixed_ips']])
|
||||
target_set = set([netaddr.IPAddress(d['ip_address'])
|
||||
for d in gw_info['external_fixed_ips']])
|
||||
# If there is an intersection - it's a partial match.
|
||||
if current_set & target_set:
|
||||
part_matched_port_ids[gw_port['id']] = gw_info
|
||||
# It can also be a full match.
|
||||
if current_set == target_set:
|
||||
matched_port_ids[gw_port['id']] = gw_info
|
||||
break
|
||||
else:
|
||||
raise mh_exc.UnableToMatchGateways(
|
||||
router_id=router_id,
|
||||
reason=_('could not match a gateway port attached to '
|
||||
'network %s based on the specified fixed IPs '
|
||||
'%s') % (net_id,
|
||||
gw_info['external_fixed_ips']))
|
||||
return matched_port_ids, part_matched_port_ids, nonexistent_port_info
|
||||
|
||||
def _replace_compat_gw_port(self, context, router_db, new_gw_port_id):
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
router_db['gw_port_id'] = new_gw_port_id
|
||||
|
||||
def _remove_external_gateways(self, context, router_id, gw_info_list,
|
||||
payload):
|
||||
"""Remove external gateways from a router."""
|
||||
removed_gateways = []
|
||||
if not gw_info_list:
|
||||
return removed_gateways
|
||||
|
||||
gw_ports = l3_obj.RouterPort.get_gw_port_ids_by_router_id(context,
|
||||
router_id)
|
||||
if not gw_ports:
|
||||
raise mh_exc.UnableToRemoveGateways(
|
||||
router_id=router_id,
|
||||
reason=_('the router does not have any external gateways'))
|
||||
|
||||
# The `_validate_gw_info` method takes a DB object.
|
||||
router_db = self._get_router(context, router_id)
|
||||
|
||||
# Go over extra gateways and validate the specified information.
|
||||
for gw_info in gw_info_list:
|
||||
ext_ips = gw_info.get(
|
||||
'external_fixed_ips', [])
|
||||
self._validate_gw_info(context, gw_info, ext_ips, router_db)
|
||||
|
||||
found_gw_port_ids, part_matches, nonexistent_port_info = (
|
||||
self._match_requested_gateway_ports(context, router_id,
|
||||
gw_info_list))
|
||||
if nonexistent_port_info:
|
||||
raise mh_exc.UnableToMatchGateways(
|
||||
router_id=router_id,
|
||||
reason=_('could not match gateway port IDs for gateway info '
|
||||
'with networks %s') % (
|
||||
', '.join(i['network_id']
|
||||
for i in nonexistent_port_info)))
|
||||
|
||||
# If the compatibility gw_port_id is to be removed, do it after
|
||||
# the removal of extra gateway ports but stash up some information.
|
||||
compat_gw_port_info = part_matches.pop(router_db['gw_port_id'])
|
||||
|
||||
# Actually remove extra gateways first.
|
||||
for extra_gw_port_id in part_matches.keys():
|
||||
self._delete_extra_gw_port(context, router_id, extra_gw_port_id)
|
||||
removed_gateways.append(part_matches[extra_gw_port_id])
|
||||
|
||||
# If the matched gateway port ID includes the compatibility one, handle
|
||||
# its removal in a compatible way.
|
||||
if compat_gw_port_info:
|
||||
# Removal is done by making an empty update using the
|
||||
# compatibility interface. This allows reusing pre-removal checks
|
||||
# like the FIP presence check.
|
||||
self._update_router_gw_info(context, router_id, {}, {})
|
||||
removed_gateways.append(compat_gw_port_info)
|
||||
|
||||
# If there are any ports remaining besides the compatibility one
|
||||
# and its removal was done, make sure the remaining port becomes
|
||||
# the compatibility port. This is not atomic but the extra GW port
|
||||
# should not be removed in the process.
|
||||
gw_ports = l3_obj.RouterPort.get_gw_port_ids_by_router_id(context,
|
||||
router_id)
|
||||
if not router_db['gw_port_id'] and len(gw_ports) > 0:
|
||||
new_gw_port_id = gw_ports[0]
|
||||
new_network_id = port_obj.Port.get_object(
|
||||
context, id=new_gw_port_id).network_id
|
||||
# Replace the gw_port_id on the router object with an existing one.
|
||||
self._replace_compat_gw_port(context, router_db, new_gw_port_id)
|
||||
# Generate a compatibility payload.
|
||||
synthetic_payload = copy.deepcopy(payload)
|
||||
synthetic_payload['router'].pop('external_gateways')
|
||||
# Here we only need a network_id because the fixed IPs are already
|
||||
# assigned and do not need to be changed.
|
||||
info = {
|
||||
'network_id': new_network_id
|
||||
}
|
||||
synthetic_payload['router']['external_gateway_info'] = info
|
||||
# Finally update the compatibility gateway port.
|
||||
self._update_router_gw_info(
|
||||
context, router_id, info, synthetic_payload)
|
||||
|
||||
return removed_gateways
|
||||
|
||||
def _router_extra_gw_port_has_floating_ips(self, context, router_id,
|
||||
gw_port):
|
||||
return l3_obj.FloatingIP.count(context, **{
|
||||
'router_id': [router_id],
|
||||
'floating_network_id': gw_port.network_id,
|
||||
})
|
||||
|
||||
def _delete_extra_gw_port(self, context, router_id, gw_port_id):
|
||||
admin_ctx = context.elevated()
|
||||
gw_port = port_obj.Port.get_object(context, id=gw_port_id)
|
||||
fip_count = self._router_extra_gw_port_has_floating_ips(context,
|
||||
router_id,
|
||||
gw_port)
|
||||
if fip_count:
|
||||
# Check that there are still other gateway ports attached to the
|
||||
# same network, otherwise this gateway port cannot be deleted.
|
||||
gw_ports = port_obj.Port.get_ports_by_router_and_network(
|
||||
context, router_id, constants.DEVICE_OWNER_ROUTER_GW,
|
||||
gw_port.network_id)
|
||||
if len(gw_ports) < 2:
|
||||
raise l3_exc.RouterExternalGatewayInUseByFloatingIp(
|
||||
router_id=router_id, net_id=gw_port.network_id)
|
||||
|
||||
# TODO(dmitriis): publish an events.BEFORE_DELETE event for a new
|
||||
# resource type e.g. resources.ROUTER_EXTRA_GATEWAY. Semantically this
|
||||
# is a different resource from resources.ROUTER_GATEWAY.
|
||||
|
||||
if db_api.is_session_active(admin_ctx.session):
|
||||
admin_ctx.GUARD_TRANSACTION = False
|
||||
self._core_plugin.delete_port(
|
||||
admin_ctx, gw_port_id, l3_port_check=False)
|
||||
|
||||
# TODO(dmitriis): publish an events.AFTER_DELETE event for a new
|
||||
# resource type e.g. resources.ROUTER_EXTRA_GATEWAY. Semantically this
|
||||
# is a different resource from resources.ROUTER_GATEWAY.
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def add_external_gateways(self, context, router_id, body):
|
||||
gateways = body['router'].get('external_gateways',
|
||||
constants.ATTR_NOT_SPECIFIED)
|
||||
if gateways == constants.ATTR_NOT_SPECIFIED:
|
||||
return self._get_router(context, router_id)
|
||||
|
||||
external_gateways = self._add_external_gateways(
|
||||
context, router_id, gateways, body)
|
||||
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
router = self.update_router(
|
||||
context, router_id, {
|
||||
'router': {
|
||||
'external_gateways': external_gateways}})
|
||||
return {'router': router}
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def remove_external_gateways(self, context, router_id, body):
|
||||
gateways = body['router'].get('external_gateways',
|
||||
constants.ATTR_NOT_SPECIFIED)
|
||||
if gateways == constants.ATTR_NOT_SPECIFIED:
|
||||
return self._get_router(context, router_id)
|
||||
|
||||
external_gateways = self._remove_external_gateways(
|
||||
context, router_id, gateways, body)
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
router = self.update_router(
|
||||
context,
|
||||
router_id,
|
||||
{'router':
|
||||
{'external_gateways': external_gateways}})
|
||||
return {'router': router}
|
||||
|
||||
def _remove_all_gateways(self, context, router_id):
|
||||
router_db = self._get_router(context, router_id)
|
||||
compat_gw_port_id = router_db['gw_port_id']
|
||||
gw_ports = l3_obj.RouterPort.get_gw_port_ids_by_router_id(context,
|
||||
router_id)
|
||||
for gw_port_id in gw_ports:
|
||||
if gw_port_id != compat_gw_port_id:
|
||||
self._delete_extra_gw_port(context, router_id, gw_port_id)
|
||||
if compat_gw_port_id:
|
||||
# Remove the compatibility gw port using the compatibility API
|
||||
self._update_router_gw_info(context, router_id, {}, {}, router_db)
|
||||
|
||||
def _update_external_gateways(self, context, router_id, gw_info_list,
|
||||
payload):
|
||||
# An empty list means "remove all gateways".
|
||||
if not gw_info_list:
|
||||
self._remove_all_gateways(context, router_id)
|
||||
return {}
|
||||
|
||||
# The `_validate_gw_info` method takes a DB object.
|
||||
router_db = self._get_router(context, router_id)
|
||||
|
||||
# Go over extra gateways and validate the specified information.
|
||||
for gw_info in gw_info_list:
|
||||
ext_ips = gw_info.get(
|
||||
'external_fixed_ips', [])
|
||||
self._validate_gw_info(context, gw_info, ext_ips, router_db)
|
||||
|
||||
# Find a match for the first gateway in the list.
|
||||
found_gw_port_ids, part_matches, nonexistent_port_info = (
|
||||
self._match_requested_gateway_ports(context, router_id,
|
||||
gw_info_list[:1]))
|
||||
# If there is already an existing extra gateway port matching what was
|
||||
# requested in the update for the compatibility gw port, simply update
|
||||
# the compatibility gw_port_id.
|
||||
if part_matches:
|
||||
# Replace the gw_port_id on the router object with an existing one.
|
||||
self._replace_compat_gw_port(context, router_db,
|
||||
list(part_matches.keys())[0])
|
||||
|
||||
# The first gw info dict is special as it designates a compat gw. So
|
||||
# we simply try to make an update using the compatibility API.
|
||||
self._update_router_gw_info(context, router_id, gw_info_list[0], {})
|
||||
|
||||
# Find a match for the rest of the gateway list.
|
||||
found_gw_port_ids, part_matches, nonexistent_port_info = (
|
||||
self._match_requested_gateway_ports(context, router_id,
|
||||
gw_info_list[1:]))
|
||||
router = l3_obj.Router.get_object(context, id=router_id)
|
||||
|
||||
# For partial matches, we need to update the set of fixed IPs for
|
||||
# existing ports.
|
||||
for gw_port_id, gw_info in part_matches.items():
|
||||
# There can be partial matches without any fixed IPs specified,
|
||||
# So we check and skip those.
|
||||
fixed_ips = gw_info.get('external_fixed_ips')
|
||||
if not fixed_ips:
|
||||
continue
|
||||
self._core_plugin.update_port(
|
||||
context.elevated(),
|
||||
gw_port_id,
|
||||
{'port': {'fixed_ips': fixed_ips}})
|
||||
|
||||
gw_ports = l3_obj.RouterPort.get_gw_port_ids_by_router_id(context,
|
||||
router_id)
|
||||
# Identify the set of ports to remove based on the ones that could not
|
||||
# be matched based on the supplied external gateways in the request.
|
||||
ports_to_remove = set(gw_ports).difference(
|
||||
set(found_gw_port_ids.keys())).difference(set([router.gw_port_id]))
|
||||
|
||||
for gw_port_id in ports_to_remove:
|
||||
self._remove_external_gateways(
|
||||
context, router_id, [v for k, v in found_gw_port_ids.items()
|
||||
if k == gw_port_id], {})
|
||||
|
||||
if nonexistent_port_info:
|
||||
synthetic_payload = {
|
||||
'router': {
|
||||
'external_gateways': nonexistent_port_info}}
|
||||
|
||||
self._add_external_gateways(context, router_id,
|
||||
nonexistent_port_info,
|
||||
synthetic_payload)
|
||||
return gw_info_list
|
||||
|
||||
@db_api.retry_if_session_inactive()
|
||||
def update_external_gateways(self, context, router_id, body):
|
||||
gateways = body['router'].get('external_gateways',
|
||||
constants.ATTR_NOT_SPECIFIED)
|
||||
if gateways == constants.ATTR_NOT_SPECIFIED:
|
||||
return self._get_router(context, router_id)
|
||||
|
||||
external_gateways = self._update_external_gateways(
|
||||
context, router_id, gateways, body)
|
||||
|
||||
with db_api.CONTEXT_WRITER.using(context):
|
||||
router = self.update_router(
|
||||
context,
|
||||
router_id,
|
||||
{'router':
|
||||
{'external_gateways': external_gateways}})
|
||||
return {'router': router}
|
||||
|
||||
def _update_router_gw_info(self, context, router_id,
|
||||
info, request_body, router=None):
|
||||
router_db = super()._update_router_gw_info(context, router_id, info,
|
||||
request_body, router)
|
||||
# If a compatibility port got removed as a result of a router update
|
||||
# (by passing empty info for external_gateway_info) replace it with
|
||||
# one of the existing ones.
|
||||
gw_ports = l3_obj.RouterPort.get_gw_port_ids_by_router_id(context,
|
||||
router_id)
|
||||
if gw_ports and not router_db['gw_port_id']:
|
||||
new_gw_port_id = gw_ports[0]
|
||||
self._replace_compat_gw_port(context, router_db, new_gw_port_id)
|
||||
return router_db
|
||||
|
||||
|
||||
class ExtraGatewaysMixinDbMixin(ExtraGatewaysDbOnlyMixin,
|
||||
l3_db.L3_NAT_db_mixin):
|
||||
pass
|
22
neutron/extensions/l3_extra_gws.py
Normal file
22
neutron/extensions/l3_extra_gws.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Copyright 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 neutron_lib.api.definitions import l3_ext_gw_multihoming as apidef
|
||||
from neutron_lib.api import extensions
|
||||
|
||||
|
||||
class L3_extra_gws(extensions.APIExtensionDescriptor):
|
||||
|
||||
api_definition = apidef
|
@ -39,6 +39,7 @@ from neutron.common import utils as common_utils
|
||||
from neutron.db.availability_zone import router as router_az_db
|
||||
from neutron.db import dns_db
|
||||
from neutron.db import extraroute_db
|
||||
from neutron.db import l3_extra_gws_db
|
||||
from neutron.db import l3_fip_pools_db
|
||||
from neutron.db import l3_fip_port_details
|
||||
from neutron.db import l3_fip_qos
|
||||
@ -67,6 +68,7 @@ class OVNL3RouterPlugin(service_base.ServicePluginBase,
|
||||
l3_fip_qos.FloatingQoSDbMixin,
|
||||
l3_gateway_ip_qos.L3_gw_ip_qos_db_mixin,
|
||||
l3_fip_pools_db.FloatingIPPoolsMixin,
|
||||
l3_extra_gws_db.ExtraGatewaysDbOnlyMixin,
|
||||
):
|
||||
"""Implementation of the OVN L3 Router Service Plugin.
|
||||
|
||||
|
@ -15,6 +15,7 @@ NETWORK_API_EXTENSIONS+=",dns-integration"
|
||||
NETWORK_API_EXTENSIONS+=",dvr"
|
||||
NETWORK_API_EXTENSIONS+=",empty-string-filtering"
|
||||
NETWORK_API_EXTENSIONS+=",ext-gw-mode"
|
||||
NETWORK_API_EXTENSIONS+=",external-gateway-multihoming"
|
||||
NETWORK_API_EXTENSIONS+=",external-net"
|
||||
NETWORK_API_EXTENSIONS+=",extra_dhcp_opt"
|
||||
NETWORK_API_EXTENSIONS+=",extraroute"
|
||||
|
671
neutron/tests/unit/db/test_l3_extra_gws_db.py
Normal file
671
neutron/tests/unit/db/test_l3_extra_gws_db.py
Normal file
@ -0,0 +1,671 @@
|
||||
# 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
|
||||
)
|
@ -0,0 +1,6 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added support for the ``external-gateway-multihoming`` API extension. The
|
||||
L3 service plugins supporting it can now create multiple gateway ports per
|
||||
router. At the time of writing this is limited to the OVN L3 plugin.
|
Loading…
Reference in New Issue
Block a user