From 8361b8b5aebad4df3c1012952d9a87b936fef326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Jens=C3=A5s?= Date: Sat, 9 Jun 2018 02:46:56 +0200 Subject: [PATCH] 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 --- .../_segments_peer_subnet_host_routes_lib.py | 32 ++++++++ .../segments_peer_subnet_host_routes.py | 18 +++++ neutron/services/segments/plugin.py | 75 +++++++++++++++---- .../tests/contrib/hooks/api_all_extensions | 1 + neutron/tests/unit/extensions/test_segment.py | 35 +++++++-- ...-networks-hostroutes-a13a9885f0db4f69.yaml | 8 ++ 6 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 neutron/extensions/_segments_peer_subnet_host_routes_lib.py create mode 100644 neutron/extensions/segments_peer_subnet_host_routes.py create mode 100644 releasenotes/notes/routed-networks-hostroutes-a13a9885f0db4f69.yaml diff --git a/neutron/extensions/_segments_peer_subnet_host_routes_lib.py b/neutron/extensions/_segments_peer_subnet_host_routes_lib.py new file mode 100644 index 00000000000..d0e53bb2ad5 --- /dev/null +++ b/neutron/extensions/_segments_peer_subnet_host_routes_lib.py @@ -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 = {} diff --git a/neutron/extensions/segments_peer_subnet_host_routes.py b/neutron/extensions/segments_peer_subnet_host_routes.py new file mode 100644 index 00000000000..32855e79628 --- /dev/null +++ b/neutron/extensions/segments_peer_subnet_host_routes.py @@ -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 diff --git a/neutron/services/segments/plugin.py b/neutron/services/segments/plugin.py index 42b25693058..1af8996b464 100644 --- a/neutron/services/segments/plugin.py +++ b/neutron/services/segments/plugin.py @@ -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']) diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index 0045964345d..b8d6b142ff2 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -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" diff --git a/neutron/tests/unit/extensions/test_segment.py b/neutron/tests/unit/extensions/test_segment.py index 06210f41cbf..2cc50eba8bd 100644 --- a/neutron/tests/unit/extensions/test_segment.py +++ b/neutron/tests/unit/extensions/test_segment.py @@ -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, diff --git a/releasenotes/notes/routed-networks-hostroutes-a13a9885f0db4f69.yaml b/releasenotes/notes/routed-networks-hostroutes-a13a9885f0db4f69.yaml new file mode 100644 index 00000000000..f87a4d8a6ae --- /dev/null +++ b/releasenotes/notes/routed-networks-hostroutes-a13a9885f0db4f69.yaml @@ -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.