Routed Networks - peer-subnet/segment host-routes (2/2)

Ensure that host routes are maintained for each subnet within
a network. Subnets associated with different segments on the
same network get host_routes entries added/removed as subnets
are created, deleted or updated.

This change handle the host_routes for the peer subnets on the
same network when a subnet is created or deleted.

Also adds a shim api extension.

APIImpact: Host routes are now calculated for routed networks.
Closes-Bug: #1766380
Change-Id: Iafbabe6352283e7f1a535a7b147bd81fb32f0ed1
This commit is contained in:
Harald Jensås 2018-06-09 02:46:56 +02:00
parent 885c1213f9
commit 8361b8b5ae
6 changed files with 150 additions and 19 deletions

View File

@ -0,0 +1,32 @@
# 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.
"""
TODO(hjensas): This module should be deleted once neutron-lib containing
Change-Id: Ibd1b565a04a6d979b6e56ca5469af644894d6b4c is released.
"""
from neutron_lib.api.definitions import segment
ALIAS = 'segments-peer-subnet-host-routes'
IS_SHIM_EXTENSION = True
IS_STANDARD_ATTR_EXTENSION = False
NAME = 'Segments peer-subnet host routes'
DESCRIPTION = 'Add host routes to subnets on a routed network (segments)'
UPDATED_TIMESTAMP = '2018-06-12T10:00:00-00:00'
RESOURCE_ATTRIBUTE_MAP = {}
SUB_RESOURCE_ATTRIBUTE_MAP = {}
ACTION_MAP = {}
REQUIRED_EXTENSIONS = [segment.ALIAS]
OPTIONAL_EXTENSIONS = []
ACTION_STATUS = {}

View File

@ -0,0 +1,18 @@
# 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.extensions import _segments_peer_subnet_host_routes_lib as apidef
from neutron_lib.api import extensions
class Segments_peer_subnet_host_routes(extensions.APIExtensionDescriptor):
api_definition = apidef

View File

@ -38,7 +38,6 @@ from oslo_utils import excutils
from neutron._i18n import _
from neutron.db import _resource_extend as resource_extend
from neutron.db import models_v2
from neutron.extensions import segment
from neutron.notifiers import batch_notifier
from neutron.objects import network as net_obj
@ -64,7 +63,8 @@ class Plugin(db.SegmentDbMixin, segment.SegmentPluginBase):
supported_extension_aliases = ["segment", "ip_allocation",
l2adj_apidef.ALIAS,
"standard-attr-segment",
"subnet-segmentid-writable"]
"subnet-segmentid-writable",
'segments-peer-subnet-host-routes']
__native_pagination_support = True
__native_sorting_support = True
@ -437,12 +437,11 @@ class NovaSegmentNotifier(object):
@registry.has_registry_receivers
class SegmentHostRoutes(object):
def _get_network(self, context, network_id):
return context.session.query(models_v2.Network).filter(
models_v2.Network.id == network_id).one()
def _get_subnets(self, context, network_id):
return subnet_obj.Subnet.get_objects(context, network_id=network_id)
def _calculate_routed_network_host_routes(self, context, ip_version,
network=None, subnet_id=None,
network_id=None, subnet_id=None,
segment_id=None,
host_routes=None,
gateway_ip=None,
@ -456,7 +455,7 @@ class SegmentHostRoutes(object):
and AFTER_DELETE.
:param ip_version: IP version (4/6).
:param network: Network.
:param network_id: Network ID.
:param subnet_id: UUID of the subnet.
:param segment_id: Segement ID associated with the subnet.
:param host_routes: Current host_routes of the subnet.
@ -479,13 +478,13 @@ class SegmentHostRoutes(object):
if delete_route in host_routes:
host_routes.remove(delete_route)
for subnet in network.subnets:
for subnet in self._get_subnets(context, network_id):
if (subnet.id == subnet_id or subnet.segment_id == segment_id or
subnet.ip_version != ip_version):
continue
subnet_ip_net = netaddr.IPNetwork(subnet.cidr)
if old_gateway_ip:
old_route = {'destination': subnet.cidr,
old_route = {'destination': str(subnet.cidr),
'nexthop': old_gateway_ip}
if old_route in host_routes:
host_routes.remove(old_route)
@ -514,6 +513,38 @@ class SegmentHostRoutes(object):
set((route['destination'],
route['nexthop']) for route in calc_host_routes)))
def _update_routed_network_host_routes(self, context, network_id,
deleted_cidr=None):
"""Update host routes on subnets on a routed network after event
Host routes on the subnets on a routed network may need updates after
any CREATE or DELETE event.
:param network_id: Network ID
:param deleted_cidr: The cidr of a deleted subnet.
"""
for subnet in self._get_subnets(context, network_id):
host_routes = [{'destination': str(route.destination),
'nexthop': route.nexthop}
for route in subnet.host_routes]
calc_host_routes = self._calculate_routed_network_host_routes(
context=context,
ip_version=subnet.ip_version,
network_id=subnet.network_id,
subnet_id=subnet.id,
segment_id=subnet.segment_id,
host_routes=copy.deepcopy(host_routes),
gateway_ip=subnet.gateway_ip,
deleted_cidr=deleted_cidr)
if self._host_routes_need_update(host_routes, calc_host_routes):
LOG.debug(
"Updating host routes for subnet %s on routed network %s",
(subnet.id, subnet.network_id))
plugin = directory.get_plugin()
plugin.update_subnet(context, subnet.id,
{'subnet': {
'host_routes': calc_host_routes}})
@registry.receives(resources.SUBNET, [events.BEFORE_CREATE])
def host_routes_before_create(self, resource, event, trigger, context,
subnet, **kwargs):
@ -524,11 +555,10 @@ class SegmentHostRoutes(object):
else:
host_routes = []
if segment_id is not None and validators.is_attr_set(gateway_ip):
network = self._get_network(context, subnet['network_id'])
calc_host_routes = self._calculate_routed_network_host_routes(
context=context,
ip_version=netaddr.IPNetwork(subnet['cidr']).version,
network=network,
network_id=subnet['network_id'],
segment_id=subnet['segment_id'],
host_routes=copy.deepcopy(host_routes),
gateway_ip=gateway_ip)
@ -546,11 +576,10 @@ class SegmentHostRoutes(object):
host_routes = subnet.get('host_routes', original_subnet['host_routes'])
if (segment_id and (host_routes != original_subnet['host_routes'] or
gateway_ip != original_subnet['gateway_ip'])):
network = self._get_network(context, original_subnet['network_id'])
calc_host_routes = self._calculate_routed_network_host_routes(
context=context,
ip_version=netaddr.IPNetwork(original_subnet['cidr']).version,
network=network,
network_id=original_subnet['network_id'],
segment_id=segment_id,
host_routes=copy.deepcopy(host_routes),
gateway_ip=gateway_ip,
@ -558,3 +587,23 @@ class SegmentHostRoutes(object):
gateway_ip != original_subnet['gateway_ip']) else None)
if self._host_routes_need_update(host_routes, calc_host_routes):
subnet['host_routes'] = calc_host_routes
@registry.receives(resources.SUBNET, [events.AFTER_CREATE])
def host_routes_after_create(self, resource, event, trigger, **kwargs):
context = kwargs['context']
subnet = kwargs['subnet']
# If there are other subnets on the network and subnet has segment_id
# ensure host routes for all subnets are updated.
if (len(self._get_subnets(context, subnet['network_id'])) > 1 and
subnet.get('segment_id')):
self._update_routed_network_host_routes(context,
subnet['network_id'])
@registry.receives(resources.SUBNET, [events.AFTER_DELETE])
def host_routes_after_delete(self, resource, event, trigger, context,
subnet, **kwargs):
# If this is a routed network, remove any routes to this subnet on
# this networks remaining subnets.
if subnet.get('segment_id'):
self._update_routed_network_host_routes(
context, subnet['network_id'], deleted_cidr=subnet['cidr'])

View File

@ -42,6 +42,7 @@ NETWORK_API_EXTENSIONS+=",router_availability_zone"
NETWORK_API_EXTENSIONS+=",security-group"
NETWORK_API_EXTENSIONS+=",port-security-groups-filtering"
NETWORK_API_EXTENSIONS+=",segment"
NETWORK_API_EXTENSIONS+=",segments-peer-subnet-host-routes"
NETWORK_API_EXTENSIONS+=",service-type"
NETWORK_API_EXTENSIONS+=",sorting"
NETWORK_API_EXTENSIONS+=",standard-attr-description"

View File

@ -2498,9 +2498,7 @@ class TestSegmentHostRoutes(TestSegmentML2):
sub_res = self.deserialize(self.fmt, raw_res)['subnet']
self.assertIn(sub_res['cidr'], cidrs)
self.assertIn(sub_res['gateway_ip'], gateway_ips)
# TODO(hjensas): Remove the conditinal in next patch in series.
if len(sub_res['host_routes']) > 0:
self.assertIn(sub_res['host_routes'][0], host_routes)
self.assertIn(sub_res['host_routes'][0], host_routes)
def test_host_routes_two_subnets_with_same_segment_association(self):
"""Creates two subnets associated to the same segment.
@ -2540,6 +2538,33 @@ class TestSegmentHostRoutes(TestSegmentML2):
self.assertEqual([], res_subnet0['subnet']['host_routes'])
self.assertEqual([], res_subnet1['subnet']['host_routes'])
def test_host_routes_create_two_subnets_then_delete_one(self):
"""Delete subnet after creating two subnets associated same segment.
Host routes with destination to the subnet that is deleted are removed
from the remaining subnets.
"""
gateway_ips = ['10.0.1.1', '10.0.2.1']
cidrs = ['10.0.1.0/24', '10.0.2.0/24']
net, subnet0, subnet1 = self._create_subnets_segments(gateway_ips,
cidrs)
sh_req = self.new_show_request('subnets', subnet1['id'])
raw_res = sh_req.get_response(self.api)
sub_res = self.deserialize(self.fmt, raw_res)
self.assertEqual([{'destination': cidrs[0],
'nexthop': gateway_ips[1]}],
sub_res['subnet']['host_routes'])
del_req = self.new_delete_request('subnets', subnet0['id'])
del_req.get_response(self.api)
sh_req = self.new_show_request('subnets', subnet1['id'])
raw_res = sh_req.get_response(self.api)
sub_res = self.deserialize(self.fmt, raw_res)
self.assertEqual([], sub_res['subnet']['host_routes'])
def test_host_routes_two_subnets_then_change_gateway_ip(self):
gateway_ips = ['10.0.1.1', '10.0.2.1']
cidrs = ['10.0.1.0/24', '10.0.2.0/24']
@ -2557,9 +2582,7 @@ class TestSegmentHostRoutes(TestSegmentML2):
sub_res = self.deserialize(self.fmt, raw_res)['subnet']
self.assertIn(sub_res['cidr'], cidrs)
self.assertIn(sub_res['gateway_ip'], gateway_ips)
# TODO(hjensas): Remove the conditinal in next patch in series.
if len(sub_res['host_routes']) > 0:
self.assertIn(sub_res['host_routes'][0], host_routes)
self.assertIn(sub_res['host_routes'][0], host_routes)
new_gateway_ip = '10.0.1.254'
data = {'subnet': {'gateway_ip': new_gateway_ip,

View File

@ -0,0 +1,8 @@
---
features:
- |
Adds host routes for subnets on the same network when using routed
networks. Static routes will be configured for subnets associated with
other segments on the same network. This ensures that traffic within an L3
routed network stays within the network even when the default route is on
a different interface.