diff --git a/doc/source/admin/config-routed-networks.rst b/doc/source/admin/config-routed-networks.rst index 7b5ad66edf3..9127ef77978 100644 --- a/doc/source/admin/config-routed-networks.rst +++ b/doc/source/admin/config-routed-networks.rst @@ -616,3 +616,68 @@ subnets L3 agent when: * The "segments" plugin is enabled; this plugin is needed for routed provided networks. * 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``. diff --git a/neutron/agent/linux/dhcp.py b/neutron/agent/linux/dhcp.py index 8280a402e42..a92ff96e421 100644 --- a/neutron/agent/linux/dhcp.py +++ b/neutron/agent/linux/dhcp.py @@ -358,11 +358,9 @@ class DhcpLocalProcess(DhcpBase, metaclass=abc.ABCMeta): self._remove_config_files() def _destroy_namespace_and_port(self): - segmentation_id = ( - self.segment.segmentation_id if self.segment else None) try: self.device_manager.destroy( - self.network, self.interface_name, segmentation_id) + self.network, self.interface_name, self.segment) except RuntimeError: LOG.warning('Failed trying to delete interface: %s', self.interface_name) diff --git a/neutron/objects/subnet.py b/neutron/objects/subnet.py index 2f875430379..9853556d9ac 100644 --- a/neutron/objects/subnet.py +++ b/neutron/objects/subnet.py @@ -16,6 +16,7 @@ from neutron_lib import constants as const from neutron_lib.db import model_query from neutron_lib.objects import common_types from neutron_lib.utils import net as net_utils +from oslo_log import log as logging from oslo_utils import versionutils 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.services.segments import exceptions as segment_exc +LOG = logging.getLogger(__name__) + @base.NeutronObjectRegistry.register class DNSNameServer(base.NeutronDbObject): @@ -342,16 +345,15 @@ class Subnet(base.NeutronDbObject): # The host is known. Consider both routed and non-routed networks results = cls._query_filter_by_segment_host_mapping(query, host).all() - # For now, we're using a simplifying assumption that a host will only - # touch one segment in a given routed network. Raise exception - # otherwise. This restriction may be relaxed as use cases for multiple - # mappings are understood. + # For now, we know that OVS agent is supporting multi-segments. segment_ids = {subnet.segment_id for subnet, mapping in results if mapping} + if len(segment_ids) > 1: - raise segment_exc.HostConnectedToMultipleSegments( - host=host, network_id=network_id) + LOG.info("The network '%s' has multiple segments, " + "this is currently supported by OVS agent only.", + network_id) return [subnet for subnet, _mapping in results] diff --git a/neutron/services/segments/exceptions.py b/neutron/services/segments/exceptions.py index 4b8a43464c2..2c464bf82c8 100644 --- a/neutron/services/segments/exceptions.py +++ b/neutron/services/segments/exceptions.py @@ -49,12 +49,6 @@ class NetworkIdsDontMatch(exceptions.BadRequest): "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): message = _("Host %(host)s is not connected to any segments on routed " "provider network '%(network_id)s'. It should be connected " diff --git a/neutron/tests/fullstack/resources/client.py b/neutron/tests/fullstack/resources/client.py index 96a70ee03f7..938fdd46eb9 100644 --- a/neutron/tests/fullstack/resources/client.py +++ b/neutron/tests/fullstack/resources/client.py @@ -153,7 +153,7 @@ class ClientFixture(fixtures.Fixture): cidr=None, gateway_ip=None, name=None, enable_dhcp=True, ipv6_address_mode='slaac', ipv6_ra_mode='slaac', subnetpool_id=None, ip_version=None, - host_routes=None): + host_routes=None, segment=None): resource_type = 'subnet' name = name or utils.get_rand_name(prefix=resource_type) @@ -173,6 +173,8 @@ class ClientFixture(fixtures.Fixture): spec['cidr'] = cidr if host_routes: spec['host_routes'] = host_routes + if segment: + spec['segment_id'] = segment return self._create_resource(resource_type, spec) diff --git a/neutron/tests/fullstack/test_multisegs.py b/neutron/tests/fullstack/test_multisegs.py new file mode 100644 index 00000000000..8fb77561024 --- /dev/null +++ b/neutron/tests/fullstack/test_multisegs.py @@ -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) diff --git a/neutron/tests/unit/extensions/test_segment.py b/neutron/tests/unit/extensions/test_segment.py index 6b92e6f4b7f..049205497f0 100644 --- a/neutron/tests/unit/extensions/test_segment.py +++ b/neutron/tests/unit/extensions/test_segment.py @@ -1220,11 +1220,10 @@ class TestSegmentAwareIpam(SegmentAwareIpamTestCase): tenant_id=network['network']['tenant_id'], arg_list=(portbindings.HOST_ID,), **{portbindings.HOST_ID: 'fakehost'}) - res = self.deserialize(self.fmt, response) + self.deserialize(self.fmt, response) - self.assertEqual(webob.exc.HTTPConflict.code, response.status_int) - self.assertEqual(segment_exc.HostConnectedToMultipleSegments.__name__, - res['NeutronError']['type']) + # multi segments supported since Antelope. + self.assertEqual(webob.exc.HTTPCreated.code, response.status_int) def test_port_update_with_fixed_ips_ok_if_no_binding_host(self): """No binding host information is provided, subnets on segments""" @@ -1552,12 +1551,10 @@ class TestSegmentAwareIpam(SegmentAwareIpamTestCase): port_id = port['port']['id'] port_req = self.new_update_request('ports', data, port_id) 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 - self.assertEqual(webob.exc.HTTPConflict.code, response.status_int) - self.assertEqual(segment_exc.HostConnectedToMultipleSegments.__name__, - res['NeutronError']['type']) + # multi segments supported since Antelope. + self.assertEqual(webob.exc.HTTPOk.code, response.status_int) def test_port_update_allocate_no_segments(self): """Binding information is provided, subnet created after port""" diff --git a/releasenotes/notes/multisegs-support-for-phynets-f3c710139e26558c.yaml b/releasenotes/notes/multisegs-support-for-phynets-f3c710139e26558c.yaml new file mode 100644 index 00000000000..6609b02ecef --- /dev/null +++ b/releasenotes/notes/multisegs-support-for-phynets-f3c710139e26558c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Extend routed provider networks to allow provisioning more than + one segment per physical network.