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
This commit is contained in:
Harald Jensås 2018-05-22 01:47:36 +02:00
parent 9cb68ce777
commit 8d580dc037
2 changed files with 318 additions and 0 deletions

View File

@ -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

View File

@ -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)