segment: enable multisegments support for host

This updates the exception with a log message informing that
multi-segments is supported by OVS only at that point.

This also add fullstack tests that validates multisegs deployment on a
physnet.

Closes-Bug: #1956435
Partial-Bug: #1764738
Signed-off-by: Sahid Orentino Ferdjaoui <sahid.ferdjaoui@industrialdiscipline.com>
Change-Id: I3811a4ca28906dd29100c602de7fa4a3595393ab
This commit is contained in:
Sahid Orentino Ferdjaoui 2022-11-14 21:58:13 +01:00
parent 7c449f1833
commit be0996c308
8 changed files with 235 additions and 25 deletions

View File

@ -616,3 +616,68 @@ subnets L3 agent when:
* The "segments" plugin is enabled; this plugin is needed for routed provided * The "segments" plugin is enabled; this plugin is needed for routed provided
networks. networks.
* The network is connected to a segment. * The network is connected to a segment.
Multiple routed provider segments per host
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Starting with Antelope, the support of routed provider networks has
been enhanced to handle multiple segments per host. The main
consequence will be for an operator to extend the IP pool without
creating multiple networks and/or increasing broadcast domain..
.. note::
The present support is only available for OVS agent at this point.
#. On a given provided network, create a second segment. In this
example, the second segment uses the ``provider1`` physical network
with VLAN ID 2020.
.. code-block:: console
$ openstack network segment create --physical-network provider1 \
--network-type vlan --segment 2020 --network multisegment1 segment1-2
+------------------+--------------------------------------+
| Field | Value |
+------------------+--------------------------------------+
| description | None |
| headers | |
| id | 333b7925-9a89-4489-9992-e164c8cc8764 |
| name | segment1-2 |
| network_id | 6ab19caa-dda9-4b3d-abc4-5b8f435b98d9 |
| network_type | vlan |
| physical_network | provider1 |
| revision_number | 1 |
| segmentation_id | 2020 |
| tags | [] |
+------------------+--------------------------------------+
#. Create subnets on the ``segment1-2`` segment. In this example, the IPv4
subnet uses 203.0.114.0/24.
.. code-block:: console
$ openstack subnet create \
--network multisegment1 --network-segment segment1-2 \
--ip-version 4 --subnet-range 203.0.114.0/24 \
multisegment1-segment1-2
+-------------------+--------------------------------------+
| Field | Value |
+-------------------+--------------------------------------+
| allocation_pools | 203.0.114.2-203.0.114.254 |
| cidr | 203.0.114.0/24 |
| enable_dhcp | True |
| gateway_ip | 203.0.114.1 |
| id | c428797a-6f8e-4cb1-b394-c404318a2762 |
| ip_version | 4 |
| name | multisegment1-segment1-2 |
| network_id | 6ab19caa-dda9-4b3d-abc4-5b8f435b98d9 |
| revision_number | 1 |
| segment_id | 333b7925-9a89-4489-9992-e164c8cc8764 |
| tags | [] |
+-------------------+--------------------------------------+
Considering that, for a subnet of the given provider network
``provider1`` running out of available IP, Neutron will automatically
switch to the subnet ``multisegment1-segment1-2``.

View File

@ -358,11 +358,9 @@ class DhcpLocalProcess(DhcpBase, metaclass=abc.ABCMeta):
self._remove_config_files() self._remove_config_files()
def _destroy_namespace_and_port(self): def _destroy_namespace_and_port(self):
segmentation_id = (
self.segment.segmentation_id if self.segment else None)
try: try:
self.device_manager.destroy( self.device_manager.destroy(
self.network, self.interface_name, segmentation_id) self.network, self.interface_name, self.segment)
except RuntimeError: except RuntimeError:
LOG.warning('Failed trying to delete interface: %s', LOG.warning('Failed trying to delete interface: %s',
self.interface_name) self.interface_name)

View File

@ -16,6 +16,7 @@ from neutron_lib import constants as const
from neutron_lib.db import model_query from neutron_lib.db import model_query
from neutron_lib.objects import common_types from neutron_lib.objects import common_types
from neutron_lib.utils import net as net_utils from neutron_lib.utils import net as net_utils
from oslo_log import log as logging
from oslo_utils import versionutils from oslo_utils import versionutils
from oslo_versionedobjects import fields as obj_fields from oslo_versionedobjects import fields as obj_fields
@ -32,6 +33,8 @@ from neutron.objects import network
from neutron.objects import rbac_db from neutron.objects import rbac_db
from neutron.services.segments import exceptions as segment_exc from neutron.services.segments import exceptions as segment_exc
LOG = logging.getLogger(__name__)
@base.NeutronObjectRegistry.register @base.NeutronObjectRegistry.register
class DNSNameServer(base.NeutronDbObject): class DNSNameServer(base.NeutronDbObject):
@ -342,16 +345,15 @@ class Subnet(base.NeutronDbObject):
# The host is known. Consider both routed and non-routed networks # The host is known. Consider both routed and non-routed networks
results = cls._query_filter_by_segment_host_mapping(query, host).all() results = cls._query_filter_by_segment_host_mapping(query, host).all()
# For now, we're using a simplifying assumption that a host will only # For now, we know that OVS agent is supporting multi-segments.
# touch one segment in a given routed network. Raise exception
# otherwise. This restriction may be relaxed as use cases for multiple
# mappings are understood.
segment_ids = {subnet.segment_id segment_ids = {subnet.segment_id
for subnet, mapping in results for subnet, mapping in results
if mapping} if mapping}
if len(segment_ids) > 1: if len(segment_ids) > 1:
raise segment_exc.HostConnectedToMultipleSegments( LOG.info("The network '%s' has multiple segments, "
host=host, network_id=network_id) "this is currently supported by OVS agent only.",
network_id)
return [subnet for subnet, _mapping in results] return [subnet for subnet, _mapping in results]

View File

@ -49,12 +49,6 @@ class NetworkIdsDontMatch(exceptions.BadRequest):
"the network_id of segment '%(segment_id)s'") "the network_id of segment '%(segment_id)s'")
class HostConnectedToMultipleSegments(exceptions.Conflict):
message = _("Host %(host)s is connected to multiple segments on routed "
"provider network '%(network_id)s'. It should be connected "
"to one.")
class HostNotConnectedToAnySegment(exceptions.Conflict): class HostNotConnectedToAnySegment(exceptions.Conflict):
message = _("Host %(host)s is not connected to any segments on routed " message = _("Host %(host)s is not connected to any segments on routed "
"provider network '%(network_id)s'. It should be connected " "provider network '%(network_id)s'. It should be connected "

View File

@ -153,7 +153,7 @@ class ClientFixture(fixtures.Fixture):
cidr=None, gateway_ip=None, name=None, enable_dhcp=True, cidr=None, gateway_ip=None, name=None, enable_dhcp=True,
ipv6_address_mode='slaac', ipv6_ra_mode='slaac', ipv6_address_mode='slaac', ipv6_ra_mode='slaac',
subnetpool_id=None, ip_version=None, subnetpool_id=None, ip_version=None,
host_routes=None): host_routes=None, segment=None):
resource_type = 'subnet' resource_type = 'subnet'
name = name or utils.get_rand_name(prefix=resource_type) name = name or utils.get_rand_name(prefix=resource_type)
@ -173,6 +173,8 @@ class ClientFixture(fixtures.Fixture):
spec['cidr'] = cidr spec['cidr'] = cidr
if host_routes: if host_routes:
spec['host_routes'] = host_routes spec['host_routes'] = host_routes
if segment:
spec['segment_id'] = segment
return self._create_resource(resource_type, spec) return self._create_resource(resource_type, spec)

View File

@ -0,0 +1,147 @@
# 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_lib import constants
from oslo_utils import uuidutils
from neutron.common import utils as common_utils
from neutron.tests.fullstack import base
from neutron.tests.fullstack.resources import environment
from neutron.tests.fullstack.resources import machine
from neutron.tests.unit import testlib_api
load_tests = testlib_api.module_load_tests
class TestMultiSegs(base.BaseFullStackTestCase):
scenarios = [
('Open vSwitch Agent', {'l2_agent_type': constants.AGENT_TYPE_OVS})]
num_hosts = 1
agent_down_time = 30
network_type = "vlan"
def setUp(self):
host_descriptions = [
environment.HostDescription(
dhcp_agent=True, l2_agent_type=constants.AGENT_TYPE_OVS),
]
env = environment.Environment(
environment.EnvironmentDescription(
network_type=self.network_type,
mech_drivers='openvswitch',
l2_pop=False,
arp_responder=False,
agent_down_time=self.agent_down_time,
service_plugins='router,segments',
api_workers=1,
),
host_descriptions)
super(TestMultiSegs, self).setUp(env)
self.project_id = uuidutils.generate_uuid()
def _spawn_vm(self, neutron_port=None):
vm = self.useFixture(
machine.FakeFullstackMachine(
self.environment.hosts[0],
self.network['id'],
self.project_id,
self.safe_client,
neutron_port=neutron_port,
use_dhcp=True))
vm.block_until_boot()
vm.block_until_dhcp_config_done()
return vm
def test_multi_segs_network(self):
ovs_physnet = None
agents = self.client.list_agents()
for agent in agents['agents']:
if agent['binary'] == 'neutron-openvswitch-agent':
ovs_physnet = list(
agent['configurations']['bridge_mappings'].keys())[0]
self.network = self.safe_client.create_network(
tenant_id=self.project_id,
network_type=self.network_type,
segmentation_id=1010,
physical_network=ovs_physnet)
self.segment1 = self.safe_client.client.list_segments()['segments'][0]
self.segment2 = self.safe_client.create_segment(
project_id=self.project_id,
network=self.network['id'],
network_type=self.network_type,
name='segment2',
segmentation_id=1011,
physical_network=ovs_physnet)
# Let's validate segments created on network
net = self.safe_client.client.show_network(
self.network['id'])['network']
self.assertEqual(
ovs_physnet, net['segments'][0]['provider:physical_network'])
self.assertEqual(
ovs_physnet, net['segments'][1]['provider:physical_network'])
self.assertEqual(
1010, net['segments'][0]['provider:segmentation_id'])
self.assertEqual(
1011, net['segments'][1]['provider:segmentation_id'])
self.subnet1 = self.safe_client.create_subnet(
self.project_id,
self.network['id'],
cidr='10.0.11.0/24',
gateway_ip='10.0.11.1',
name='subnet-test1',
enable_dhcp=True,
segment=self.segment1['id'])
self.port1 = self.safe_client.create_port(
network_id=self.network['id'],
tenant_id=self.project_id,
hostname=self.environment.hosts[0].hostname,
fixed_ips=[{'subnet_id': self.subnet1['id']}])
self.subnet2 = self.safe_client.create_subnet(
self.project_id,
self.network['id'],
cidr='10.0.12.0/24',
gateway_ip='10.0.12.1',
name='subnet-test2',
enable_dhcp=True,
segment=self.segment2['id'])
self.port2 = self.safe_client.create_port(
network_id=self.network['id'],
tenant_id=self.project_id,
hostname=self.environment.hosts[0].hostname,
fixed_ips=[{'subnet_id': self.subnet2['id']}])
def _is_dhcp_ports_ready():
dhcp_ports = self.safe_client.list_ports(**{
'device_owner': 'network:dhcp',
'network_id': self.network['id']})
if len(dhcp_ports) != 2:
return False
if dhcp_ports[0]['status'] != 'ACTIVE':
return False
if dhcp_ports[1]['status'] != 'ACTIVE':
return False
return True
common_utils.wait_until_true(_is_dhcp_ports_ready)
self.vm1 = self._spawn_vm(neutron_port=self.port1)
self.vm2 = self._spawn_vm(neutron_port=self.port2)

View File

@ -1220,11 +1220,10 @@ class TestSegmentAwareIpam(SegmentAwareIpamTestCase):
tenant_id=network['network']['tenant_id'], tenant_id=network['network']['tenant_id'],
arg_list=(portbindings.HOST_ID,), arg_list=(portbindings.HOST_ID,),
**{portbindings.HOST_ID: 'fakehost'}) **{portbindings.HOST_ID: 'fakehost'})
res = self.deserialize(self.fmt, response) self.deserialize(self.fmt, response)
self.assertEqual(webob.exc.HTTPConflict.code, response.status_int) # multi segments supported since Antelope.
self.assertEqual(segment_exc.HostConnectedToMultipleSegments.__name__, self.assertEqual(webob.exc.HTTPCreated.code, response.status_int)
res['NeutronError']['type'])
def test_port_update_with_fixed_ips_ok_if_no_binding_host(self): def test_port_update_with_fixed_ips_ok_if_no_binding_host(self):
"""No binding host information is provided, subnets on segments""" """No binding host information is provided, subnets on segments"""
@ -1552,12 +1551,10 @@ class TestSegmentAwareIpam(SegmentAwareIpamTestCase):
port_id = port['port']['id'] port_id = port['port']['id']
port_req = self.new_update_request('ports', data, port_id) port_req = self.new_update_request('ports', data, port_id)
response = port_req.get_response(self.api) response = port_req.get_response(self.api)
res = self.deserialize(self.fmt, response) self.deserialize(self.fmt, response)
# Gets conflict because it can't map the host to a segment # multi segments supported since Antelope.
self.assertEqual(webob.exc.HTTPConflict.code, response.status_int) self.assertEqual(webob.exc.HTTPOk.code, response.status_int)
self.assertEqual(segment_exc.HostConnectedToMultipleSegments.__name__,
res['NeutronError']['type'])
def test_port_update_allocate_no_segments(self): def test_port_update_allocate_no_segments(self):
"""Binding information is provided, subnet created after port""" """Binding information is provided, subnet created after port"""

View File

@ -0,0 +1,5 @@
---
features:
- |
Extend routed provider networks to allow provisioning more than
one segment per physical network.