From 8d580dc0379842a6a83e922d71f6bc1ffbc85d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Jens=C3=A5s?= Date: Tue, 22 May 2018 01:47:36 +0200 Subject: [PATCH] Routed Networks - peer-subnet/segment host-routes (1/2) Ensure that host routes are maintained for each subnet within a network. Subnets associated with different segments on the same network get's host_routes enties added/removed as subnets are created, deleted or updated. This change handle the host_routes for the subnet that is created or updated. Partial-Bug: #1766380 Change-Id: If6792d121e7b8e1ab4c7a548982a42e69023da2b --- neutron/services/segments/plugin.py | 131 ++++++++++++ neutron/tests/unit/extensions/test_segment.py | 187 ++++++++++++++++++ 2 files changed, 318 insertions(+) diff --git a/neutron/services/segments/plugin.py b/neutron/services/segments/plugin.py index 5a4adbfc17f..42b25693058 100644 --- a/neutron/services/segments/plugin.py +++ b/neutron/services/segments/plugin.py @@ -14,6 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. +import copy + from keystoneauth1 import loading as ks_loading import netaddr from neutron_lib.api.definitions import ip_allocation as ipalloc_apidef @@ -21,6 +23,7 @@ from neutron_lib.api.definitions import l2_adjacency as l2adj_apidef from neutron_lib.api.definitions import network as net_def from neutron_lib.api.definitions import port as port_def from neutron_lib.api.definitions import subnet as subnet_def +from neutron_lib.api import validators from neutron_lib.callbacks import events from neutron_lib.callbacks import registry from neutron_lib.callbacks import resources @@ -35,6 +38,7 @@ 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 @@ -67,6 +71,7 @@ class Plugin(db.SegmentDbMixin, segment.SegmentPluginBase): def __init__(self): self.nova_updater = NovaSegmentNotifier() + self.segment_host_routes = SegmentHostRoutes() @staticmethod @resource_extend.extends([net_def.COLLECTION_NAME]) @@ -427,3 +432,129 @@ class NovaSegmentNotifier(object): ip['ip_address']).version == constants.IP_VERSION_4: ipv4_subnet_ids.append(ip['subnet_id']) return ipv4_subnet_ids + + +@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 _calculate_routed_network_host_routes(self, context, ip_version, + network=None, subnet_id=None, + segment_id=None, + host_routes=None, + gateway_ip=None, + old_gateway_ip=None, + deleted_cidr=None): + """Calculate host routes for routed network. + + This method is used to calculate the host routes for routed networks + both when handling the user create or update request and when making + updates to subnets on the network in response to events: AFTER_CREATE + and AFTER_DELETE. + + :param ip_version: IP version (4/6). + :param network: Network. + :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. + :param gateway_ip: The subnets gateway IP address. + :param old_gateway_ip: The old gateway IP address of the subnet when it + is changed on update. + :param deleted_cidr: The cidr of a deleted subnet. + :returns Host routes with routes for the other subnet's on the routed + network appended unless a route to the destination already + exists. + """ + if host_routes is None: + host_routes = [] + dest_ip_nets = [netaddr.IPNetwork(route['destination']) for + route in host_routes] + + # Drop routes to the deleted cidr, when the subnet was deleted. + if deleted_cidr: + delete_route = {'destination': deleted_cidr, 'nexthop': gateway_ip} + if delete_route in host_routes: + host_routes.remove(delete_route) + + for subnet in network.subnets: + 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, + 'nexthop': old_gateway_ip} + if old_route in host_routes: + host_routes.remove(old_route) + dest_ip_nets.remove(subnet_ip_net) + if gateway_ip: + # Use netaddr here in case the user provided a summary route + # (supernet route). I.e subnet.cidr = 10.0.1.0/24 and + # the user provided a host route for 10.0.0.0/16. We don't + # need to append a route in this case. + if not any(subnet_ip_net in ip_net for ip_net in dest_ip_nets): + host_routes.append({'destination': subnet.cidr, + 'nexthop': gateway_ip}) + + return host_routes + + def _host_routes_need_update(self, host_routes, calc_host_routes): + """Compare host routes and calculated host routes + + :param host_routes: Current host routes + :param calc_host_routes: Host routes + calculated host routes for + routed network + :returns True if host_routes and calc_host_routes are not equal + """ + return ((set((route['destination'], + route['nexthop']) for route in host_routes) != + set((route['destination'], + route['nexthop']) for route in calc_host_routes))) + + @registry.receives(resources.SUBNET, [events.BEFORE_CREATE]) + def host_routes_before_create(self, resource, event, trigger, context, + subnet, **kwargs): + segment_id = subnet.get('segment_id') + gateway_ip = subnet.get('gateway_ip') + if validators.is_attr_set(subnet.get('host_routes')): + host_routes = subnet.get('host_routes') + 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, + segment_id=subnet['segment_id'], + host_routes=copy.deepcopy(host_routes), + gateway_ip=gateway_ip) + if (not host_routes or + self._host_routes_need_update(host_routes, + calc_host_routes)): + subnet['host_routes'] = calc_host_routes + + @registry.receives(resources.SUBNET, [events.BEFORE_UPDATE]) + def host_routes_before_update(self, resource, event, trigger, **kwargs): + context = kwargs['context'] + subnet, original_subnet = kwargs['request'], kwargs['original_subnet'] + segment_id = subnet.get('segment_id', original_subnet['segment_id']) + gateway_ip = subnet.get('gateway_ip', original_subnet['gateway_ip']) + 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, + segment_id=segment_id, + host_routes=copy.deepcopy(host_routes), + gateway_ip=gateway_ip, + old_gateway_ip=original_subnet['gateway_ip'] if ( + 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 diff --git a/neutron/tests/unit/extensions/test_segment.py b/neutron/tests/unit/extensions/test_segment.py index 267e688aeb0..06210f41cbf 100644 --- a/neutron/tests/unit/extensions/test_segment.py +++ b/neutron/tests/unit/extensions/test_segment.py @@ -2428,3 +2428,190 @@ class PlacementAPIClientTestCase(base.DietTestCase): self.mock_request.side_effect = ks_exc.EndpointNotFound self.assertRaises(placement_exc.PlacementEndpointNotFound, self.client.list_aggregates, rp_uuid) + + +class TestSegmentHostRoutes(TestSegmentML2): + + VLAN_MIN = 200 + VLAN_MAX = 209 + + def setUp(self): + # NOTE(mlavalle): ml2_type_vlan requires to be registered before used. + # This piece was refactored and removed from .config, so it causes + # a problem, when tests are executed with pdb. + # There is no problem when tests are running without debugger. + driver_type.register_ml2_drivers_vlan_opts() + cfg.CONF.set_override( + 'network_vlan_ranges', + ['physnet:%s:%s' % (self.VLAN_MIN, self.VLAN_MAX), + 'physnet0:%s:%s' % (self.VLAN_MIN, self.VLAN_MAX), + 'physnet1:%s:%s' % (self.VLAN_MIN, self.VLAN_MAX), + 'physnet2:%s:%s' % (self.VLAN_MIN, self.VLAN_MAX)], + group='ml2_type_vlan') + super(TestSegmentHostRoutes, self).setUp() + + def _create_subnets_segments(self, gateway_ips, cidrs): + with self.network() as network: + net = network['network'] + segment0 = self._test_create_segment( + network_id=net['id'], + physical_network='physnet1', + network_type=constants.TYPE_VLAN, + segmentation_id=201)['segment'] + segment1 = self._test_create_segment( + network_id=net['id'], + physical_network='physnet2', + network_type=constants.TYPE_VLAN, + segmentation_id=202)['segment'] + + with self.subnet(network=network, + segment_id=segment0['id'], + gateway_ip=gateway_ips[0], + cidr=cidrs[0]) as subnet0, \ + self.subnet(network=network, + segment_id=segment1['id'], + gateway_ip=gateway_ips[1], + cidr=cidrs[1]) as subnet1: + pass + + return net, subnet0['subnet'], subnet1['subnet'] + + def test_host_routes_two_subnets_with_segments_association(self): + """Creates two subnets associated to different segments. + + Since the two subnets are associated with different segments on the + same network host routes will be created. + """ + gateway_ips = ['10.0.1.1', '10.0.2.1'] + cidrs = ['10.0.1.0/24', '10.0.2.0/24'] + host_routes = [{'destination': cidrs[1], 'nexthop': gateway_ips[0]}, + {'destination': cidrs[0], 'nexthop': gateway_ips[1]}] + net, subnet0, subnet1 = self._create_subnets_segments(gateway_ips, + cidrs) + + net_req = self.new_show_request('networks', net['id']) + raw_res = net_req.get_response(self.api) + net_res = self.deserialize(self.fmt, raw_res) + for subnet_id in net_res['network']['subnets']: + sub_req = self.new_show_request('subnets', subnet_id) + raw_res = sub_req.get_response(self.api) + 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) + + def test_host_routes_two_subnets_with_same_segment_association(self): + """Creates two subnets associated to the same segment. + + Since the two subnets are both associated with the same segment no host + routes will be created. + """ + gateway_ips = ['10.0.1.1', '10.0.2.1'] + cidrs = ['10.0.1.0/24', '10.0.2.0/24'] + with self.network() as network: + net = network['network'] + segment = self._test_create_segment( + network_id=net['id'], + physical_network='physnet1', + network_type=constants.TYPE_VLAN, + segmentation_id=201)['segment'] + + with self.subnet(network=network, + segment_id=segment['id'], + gateway_ip=gateway_ips[0], + cidr=cidrs[0]) as subnet0, \ + self.subnet(network=network, + segment_id=segment['id'], + gateway_ip=gateway_ips[1], + cidr=cidrs[1]) as subnet1: + subnet0 = subnet0['subnet'] + subnet1 = subnet1['subnet'] + + req = self.new_show_request('subnets', subnet0['id']) + res = req.get_response(self.api) + res_subnet0 = self.deserialize(self.fmt, res) + + req = self.new_show_request('subnets', subnet1['id']) + res = req.get_response(self.api) + res_subnet1 = self.deserialize(self.fmt, res) + + self.assertEqual([], res_subnet0['subnet']['host_routes']) + self.assertEqual([], res_subnet1['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'] + host_routes = [{'destination': cidrs[1], 'nexthop': gateway_ips[0]}, + {'destination': cidrs[0], 'nexthop': gateway_ips[1]}] + net, subnet0, subnet1 = self._create_subnets_segments(gateway_ips, + cidrs) + + net_req = self.new_show_request('networks', net['id']) + raw_res = net_req.get_response(self.api) + net_res = self.deserialize(self.fmt, raw_res) + for subnet_id in net_res['network']['subnets']: + sub_req = self.new_show_request('subnets', subnet_id) + raw_res = sub_req.get_response(self.api) + 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) + + new_gateway_ip = '10.0.1.254' + data = {'subnet': {'gateway_ip': new_gateway_ip, + 'allocation_pools': [{'start': '10.0.1.1', + 'end': '10.0.1.253'}]}} + self.new_update_request( + 'subnets', data, subnet0['id']).get_response(self.api) + + sh_req = self.new_show_request('subnets', subnet0['id']) + raw_res = sh_req.get_response(self.api) + sub_res = self.deserialize(self.fmt, raw_res) + + self.assertEqual([{'destination': cidrs[1], + 'nexthop': new_gateway_ip}], + sub_res['subnet']['host_routes']) + + def test_host_routes_two_subnets_summary_route_in_request(self): + gateway_ips = ['10.0.1.1', '10.0.2.1'] + cidrs = ['10.0.1.0/24', '10.0.2.0/24'] + summary_net = '10.0.0.0/16' + host_routes = [{'destination': summary_net, 'nexthop': gateway_ips[0]}, + {'destination': summary_net, 'nexthop': gateway_ips[1]}] + + with self.network() as network: + net = network['network'] + segment0 = self._test_create_segment( + network_id=net['id'], + physical_network='physnet1', + network_type=constants.TYPE_VLAN, + segmentation_id=201)['segment'] + segment1 = self._test_create_segment( + network_id=net['id'], + physical_network='physnet2', + network_type=constants.TYPE_VLAN, + segmentation_id=202)['segment'] + + self.subnet(network=network, segment_id=segment0['id'], + gateway_ip=gateway_ips[0], cidr=cidrs[0], + host_routes=[host_routes[0]]) + self.subnet(network=network, segment_id=segment1['id'], + gateway_ip=gateway_ips[1], + cidr=cidrs[1], host_routes=[host_routes[1]]) + + net_req = self.new_show_request('networks', net['id']) + raw_res = net_req.get_response(self.api) + net_res = self.deserialize(self.fmt, raw_res) + + for subnet_id in net_res['network']['subnets']: + sub_req = self.new_show_request('subnets', subnet_id) + raw_res = sub_req.get_response(self.api) + sub_res = self.deserialize(self.fmt, raw_res)['subnet'] + self.assertIn(sub_res['cidr'], cidrs) + self.assertIn(sub_res['gateway_ip'], gateway_ips) + self.assertEqual(len(sub_res['host_routes']), 1) + self.assertIn(sub_res['host_routes'][0], host_routes)