Merge "Routed Networks - peer-subnet/segment host-routes (1/2)"
This commit is contained in:
commit
885c1213f9
|
@ -14,6 +14,8 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
from keystoneauth1 import loading as ks_loading
|
from keystoneauth1 import loading as ks_loading
|
||||||
import netaddr
|
import netaddr
|
||||||
from neutron_lib.api.definitions import ip_allocation as ipalloc_apidef
|
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 network as net_def
|
||||||
from neutron_lib.api.definitions import port as port_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.definitions import subnet as subnet_def
|
||||||
|
from neutron_lib.api import validators
|
||||||
from neutron_lib.callbacks import events
|
from neutron_lib.callbacks import events
|
||||||
from neutron_lib.callbacks import registry
|
from neutron_lib.callbacks import registry
|
||||||
from neutron_lib.callbacks import resources
|
from neutron_lib.callbacks import resources
|
||||||
|
@ -35,6 +38,7 @@ from oslo_utils import excutils
|
||||||
|
|
||||||
from neutron._i18n import _
|
from neutron._i18n import _
|
||||||
from neutron.db import _resource_extend as resource_extend
|
from neutron.db import _resource_extend as resource_extend
|
||||||
|
from neutron.db import models_v2
|
||||||
from neutron.extensions import segment
|
from neutron.extensions import segment
|
||||||
from neutron.notifiers import batch_notifier
|
from neutron.notifiers import batch_notifier
|
||||||
from neutron.objects import network as net_obj
|
from neutron.objects import network as net_obj
|
||||||
|
@ -67,6 +71,7 @@ class Plugin(db.SegmentDbMixin, segment.SegmentPluginBase):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.nova_updater = NovaSegmentNotifier()
|
self.nova_updater = NovaSegmentNotifier()
|
||||||
|
self.segment_host_routes = SegmentHostRoutes()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@resource_extend.extends([net_def.COLLECTION_NAME])
|
@resource_extend.extends([net_def.COLLECTION_NAME])
|
||||||
|
@ -427,3 +432,129 @@ class NovaSegmentNotifier(object):
|
||||||
ip['ip_address']).version == constants.IP_VERSION_4:
|
ip['ip_address']).version == constants.IP_VERSION_4:
|
||||||
ipv4_subnet_ids.append(ip['subnet_id'])
|
ipv4_subnet_ids.append(ip['subnet_id'])
|
||||||
return ipv4_subnet_ids
|
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
|
||||||
|
|
|
@ -2428,3 +2428,190 @@ class PlacementAPIClientTestCase(base.DietTestCase):
|
||||||
self.mock_request.side_effect = ks_exc.EndpointNotFound
|
self.mock_request.side_effect = ks_exc.EndpointNotFound
|
||||||
self.assertRaises(placement_exc.PlacementEndpointNotFound,
|
self.assertRaises(placement_exc.PlacementEndpointNotFound,
|
||||||
self.client.list_aggregates, rp_uuid)
|
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)
|
||||||
|
|
Loading…
Reference in New Issue