diff --git a/doc/source/admin/config-routed-networks.rst b/doc/source/admin/config-routed-networks.rst index 19eaf2c8022..f378240f6ce 100644 --- a/doc/source/admin/config-routed-networks.rst +++ b/doc/source/admin/config-routed-networks.rst @@ -471,3 +471,75 @@ segment contains one IPv4 subnet and one IPv6 subnet. | status | DOWN | | tags | [] | +-----------------------+--------------------------------------+ + +Migrating non-routed networks to routed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Migration of existing non-routed networks is only possible if there is only one +segment and one subnet on the network. To migrate a candidate network, update +the subnet and set ``id`` of the existing network segment as ``segment_id``. + +.. note:: + + In the case where there are multiple subnets or segments it is not + possible to safely migrate. The reason for this is that in non-routed + networks addresses from the subnet's allocation pools are assigned to + ports without considering to which network segment the port is bound. + +Example +------- + +The following steps migrate an existing non-routed network with one subnet and +one segment to a routed one. + +#. Source the administrative project credentials. +#. Get the ``id`` of the current network segment on the network that is being + migrated. + + .. code-block:: console + + $ openstack network segment list --network my_network + +--------------------------------------+------+--------------------------------------+--------------+---------+ + | ID | Name | Network | Network Type | Segment | + +--------------------------------------+------+--------------------------------------+--------------+---------+ + | 81e5453d-4c9f-43a5-8ddf-feaf3937e8c7 | None | 45e84575-2918-471c-95c0-018b961a2984 | flat | None | + +--------------------------------------+------+--------------------------------------+--------------+---------+ + +#. Get the ``id`` or ``name`` of the current subnet on the network. + + .. code-block:: console + + $ openstack subnet list --network my_network + +--------------------------------------+-----------+--------------------------------------+---------------+ + | ID | Name | Network | Subnet | + +--------------------------------------+-----------+--------------------------------------+---------------+ + | 71d931d2-0328-46ae-93bc-126caf794307 | my_subnet | 45e84575-2918-471c-95c0-018b961a2984 | 172.24.4.0/24 | + +--------------------------------------+-----------+--------------------------------------+---------------+ + +#. Verify the current ``segment_id`` of the subnet is ``None``. + + .. code-block:: console + + $ openstack subnet show my_subnet --c segment_id + +------------+-------+ + | Field | Value | + +------------+-------+ + | segment_id | None | + +------------+-------+ + +#. Update the ``segment_id`` of the subnet. + + .. code-block:: console + + $ openstack subnet set --network-segment 81e5453d-4c9f-43a5-8ddf-feaf3937e8c7 my_subnet + +#. Verify that the subnet is now associated with the desired network segment. + + .. code-block:: console + + $ openstack subnet show my_subnet --c segment_id + +------------+--------------------------------------+ + | Field | Value | + +------------+--------------------------------------+ + | segment_id | 81e5453d-4c9f-43a5-8ddf-feaf3937e8c7 | + +------------+--------------------------------------+ diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index efce6b1dfe5..34573a77030 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -869,6 +869,9 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, s['project_id'] = subnet_obj.project_id s['tenant_id'] = subnet_obj.project_id s['subnetpool_id'] = subnet_obj.subnetpool_id + # Fill 'network_id' field with the current value since this is expected + # by _validate_segment() in ipam_pluggable_backend. + s['network_id'] = subnet_obj.network_id self._validate_subnet(context, s, cur_subnet=subnet_obj) db_pools = [netaddr.IPRange(p.start, p.end) for p in subnet_obj.allocation_pools] diff --git a/neutron/db/ipam_backend_mixin.py b/neutron/db/ipam_backend_mixin.py index 26fc6ac526d..d4c835b806e 100644 --- a/neutron/db/ipam_backend_mixin.py +++ b/neutron/db/ipam_backend_mixin.py @@ -316,7 +316,7 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon): pool_2=r_range, subnet_cidr=subnet_cidr) - def _validate_segment(self, context, network_id, segment_id): + def _validate_segment(self, context, network_id, segment_id, action=None): query = context.session.query(models_v2.Subnet.segment_id) query = query.filter(models_v2.Subnet.network_id == network_id) associated_segments = set(row.segment_id for row in query) @@ -324,6 +324,23 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon): raise segment_exc.SubnetsNotAllAssociatedWithSegments( network_id=network_id) + if action == 'update': + # Check the current state of segments and subnets on the network + # before allowing migration from non-routed to routed network. + if query.count() > 1: + raise segment_exc.SubnetsNotAllAssociatedWithSegments( + network_id=network_id) + if (None not in associated_segments and + segment_id not in associated_segments): + raise segment_exc.SubnetSegmentAssociationChangeNotAllowed() + + query = context.session.query(segment_model.NetworkSegment.id) + query = query.filter( + segment_model.NetworkSegment.network_id == network_id) + if query.count() > 1: + raise segment_exc.NoUpdateSubnetWhenMultipleSegmentsOnNetwork( + network_id=network_id) + if segment_id: segment = network_obj.NetworkSegment.get_object(context, id=segment_id) diff --git a/neutron/db/ipam_pluggable_backend.py b/neutron/db/ipam_pluggable_backend.py index f04c6e77d1e..24387df216d 100644 --- a/neutron/db/ipam_pluggable_backend.py +++ b/neutron/db/ipam_pluggable_backend.py @@ -428,6 +428,9 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin): port['fixed_ips']) def update_db_subnet(self, context, id, s, old_pools): + if 'segment_id' in s: + self._validate_segment(context, s['network_id'], s['segment_id'], + action='update') # 'allocation_pools' is removed from 's' in # _update_subnet_allocation_pools (ipam_backend_mixin), # so create unchanged copy for ipam driver diff --git a/neutron/extensions/subnet_segmentid_writable.py b/neutron/extensions/subnet_segmentid_writable.py new file mode 100644 index 00000000000..9fd58bfd0ad --- /dev/null +++ b/neutron/extensions/subnet_segmentid_writable.py @@ -0,0 +1,20 @@ +# 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.api.definitions import subnet_segmentid_writable as apidef +from neutron_lib.api import extensions + + +class Subnet_segmentid_writable(extensions.APIExtensionDescriptor): + """Extension class supporting writable subnet segment_id.""" + + api_definition = apidef diff --git a/neutron/objects/subnet.py b/neutron/objects/subnet.py index b018a05d349..f4771377815 100644 --- a/neutron/objects/subnet.py +++ b/neutron/objects/subnet.py @@ -198,7 +198,7 @@ class Subnet(base.NeutronDbObject): foreign_keys = {'Network': {'network_id': 'id'}} - fields_no_update = ['project_id', 'network_id', 'segment_id'] + fields_no_update = ['project_id', 'network_id'] fields_need_translation = { 'host_routes': 'routes' diff --git a/neutron/services/segments/exceptions.py b/neutron/services/segments/exceptions.py index ef7c13bb41e..a5ccc0372d2 100644 --- a/neutron/services/segments/exceptions.py +++ b/neutron/services/segments/exceptions.py @@ -23,6 +23,13 @@ class SegmentNotFound(exceptions.NotFound): message = _("Segment %(segment_id)s could not be found.") +class NoUpdateSubnetWhenMultipleSegmentsOnNetwork( + exceptions.BadRequest): + message = _("The network '%(network_id)s' has multiple segments, it is " + "only possible to associate an existing subnet with a segment " + "on networks with a single segment.") + + class SubnetsNotAllAssociatedWithSegments(exceptions.BadRequest): message = _("All of the subnets on network '%(network_id)s' must either " "all be associated with segments or all not associated with " @@ -33,6 +40,11 @@ class SubnetCantAssociateToDynamicSegment(exceptions.BadRequest): message = _("A subnet cannot be associated with a dynamic segment.") +class SubnetSegmentAssociationChangeNotAllowed(exceptions.BadRequest): + message = _("A subnet that is already associated with a segment cannot " + "have its segment association changed.") + + class NetworkIdsDontMatch(exceptions.BadRequest): message = _("The subnet's network id, '%(subnet_network)s', doesn't match " "the network_id of segment '%(segment_id)s'") diff --git a/neutron/services/segments/plugin.py b/neutron/services/segments/plugin.py index 53716124f47..5a4adbfc17f 100644 --- a/neutron/services/segments/plugin.py +++ b/neutron/services/segments/plugin.py @@ -59,7 +59,8 @@ class Plugin(db.SegmentDbMixin, segment.SegmentPluginBase): supported_extension_aliases = ["segment", "ip_allocation", l2adj_apidef.ALIAS, - "standard-attr-segment"] + "standard-attr-segment", + "subnet-segmentid-writable"] __native_pagination_support = True __native_sorting_support = True @@ -173,12 +174,7 @@ class NovaSegmentNotifier(object): 'update routed networks IPv4 inventories') return - @registry.receives(resources.SUBNET, [events.AFTER_CREATE]) - def _notify_subnet_created(self, resource, event, trigger, context, - subnet, **kwargs): - segment_id = subnet.get('segment_id') - if not segment_id or subnet['ip_version'] != constants.IP_VERSION_4: - return + def _notify_subnet(self, context, subnet, segment_id): total, reserved = self._calculate_inventory_total_and_reserved(subnet) if total: segment_host_mappings = net_obj.SegmentHostMapping.get_objects( @@ -188,6 +184,14 @@ class NovaSegmentNotifier(object): reserved=reserved, segment_host_mappings=segment_host_mappings)) + @registry.receives(resources.SUBNET, [events.AFTER_CREATE]) + def _notify_subnet_created(self, resource, event, trigger, context, + subnet, **kwargs): + segment_id = subnet.get('segment_id') + if not segment_id or subnet['ip_version'] != constants.IP_VERSION_4: + return + self._notify_subnet(context, subnet, segment_id) + def _create_or_update_nova_inventory(self, event): try: self._update_nova_inventory(event) @@ -261,8 +265,13 @@ class NovaSegmentNotifier(object): def _notify_subnet_updated(self, resource, event, trigger, context, subnet, original_subnet, **kwargs): segment_id = subnet.get('segment_id') + original_segment_id = original_subnet.get('segment_id') if not segment_id or subnet['ip_version'] != constants.IP_VERSION_4: return + if original_segment_id != segment_id: + # Migration to routed network, treat as create + self._notify_subnet(context, subnet, segment_id) + return filters = {'segment_id': [segment_id], 'ip_version': [constants.IP_VERSION_4]} if not subnet['allocation_pools']: diff --git a/neutron/tests/unit/extensions/test_segment.py b/neutron/tests/unit/extensions/test_segment.py index 409bd910a1b..2e42e8d3bbd 100644 --- a/neutron/tests/unit/extensions/test_segment.py +++ b/neutron/tests/unit/extensions/test_segment.py @@ -521,6 +521,86 @@ class TestSegmentSubnetAssociation(SegmentTestCase): segment_id=segment['id']) self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int) + def test_associate_existing_subnet_with_segment(self): + with self.network() as network: + net = network['network'] + + segment = self._test_create_segment(network_id=net['id'], + physical_network='phys_net', + segmentation_id=200)['segment'] + with self.subnet(network=network, segment_id=None) as subnet: + subnet = subnet['subnet'] + + data = {'subnet': {'segment_id': segment['id']}} + request = self.new_update_request('subnets', data, subnet['id']) + response = request.get_response(self.api) + res = self.deserialize(self.fmt, response) + + self.assertEqual(webob.exc.HTTPOk.code, response.status_int) + self.assertEqual(res['subnet']['segment_id'], segment['id']) + + def test_associate_existing_subnet_fail_if_multiple_segments(self): + with self.network() as network: + net = network['network'] + + segment1 = self._test_create_segment(network_id=net['id'], + physical_network='phys_net1', + segmentation_id=201)['segment'] + self._test_create_segment(network_id=net['id'], + physical_network='phys_net2', + segmentation_id=202)['segment'] + + with self.subnet(network=network, segment_id=None) as subnet: + subnet = subnet['subnet'] + + data = {'subnet': {'segment_id': segment1['id']}} + request = self.new_update_request('subnets', data, subnet['id']) + response = request.get_response(self.api) + + self.assertEqual(webob.exc.HTTPBadRequest.code, response.status_int) + + def test_associate_existing_subnet_fail_if_multiple_subnets(self): + with self.network() as network: + net = network['network'] + + segment1 = self._test_create_segment(network_id=net['id'], + physical_network='phys_net1', + segmentation_id=201)['segment'] + + with self.subnet(network=network, segment_id=None, + ip_version='4', cidr='10.0.0.0/24') as subnet1, \ + self.subnet(network=network, segment_id=None, + ip_version='4', cidr='10.0.1.0/24') as subnet2: + subnet1 = subnet1['subnet'] + subnet2 = subnet2['subnet'] + + data = {'subnet': {'segment_id': segment1['id']}} + request = self.new_update_request('subnets', data, subnet1['id']) + response = request.get_response(self.api) + + self.assertEqual(webob.exc.HTTPBadRequest.code, response.status_int) + + def test_change_existing_subnet_segment_association_not_allowed(self): + with self.network() as network: + net = network['network'] + + segment1 = self._test_create_segment(network_id=net['id'], + physical_network='phys_net2', + segmentation_id=201)['segment'] + + with self.subnet(network=network, segment_id=segment1['id']) as subnet: + subnet = subnet['subnet'] + + segment2 = self._test_create_segment(network_id=net['id'], + physical_network='phys_net2', + segmentation_id=202)['segment'] + + data = {'subnet': {'segment_id': segment2['id']}} + request = self.new_update_request('subnets', data, subnet['id']) + response = request.get_response(self.api) + + self.assertEqual(webob.exc.HTTPBadRequest.code, response.status_int) + class HostSegmentMappingTestCase(SegmentTestCase): _mechanism_drivers = ['logger'] @@ -1620,6 +1700,33 @@ class TestNovaSegmentNotifier(SegmentAwareIpamTestCase): def test_first_subnet_association_with_segment(self): self._test_first_subnet_association_with_segment() + def test_update_subnet_association_with_segment(self, cidr='10.0.0.0/24', + allocation_pools=None): + with self.network() as network: + segment_id = self._list('segments')['segments'][0]['id'] + network_id = network['network']['id'] + + self._setup_host_mappings([(segment_id, 'fakehost')]) + self.mock_p_client.get_inventory.side_effect = ( + placement_exc.PlacementResourceProviderNotFound( + resource_provider=segment_id, + resource_class=seg_plugin.IPV4_RESOURCE_CLASS)) + aggregate = mock.MagicMock() + aggregate.uuid = uuidutils.generate_uuid() + aggregate.id = 1 + self.mock_n_client.aggregates.create.return_value = aggregate + ip_version = netaddr.IPNetwork(cidr).version + with self.subnet(network=network, cidr=cidr, ip_version=ip_version, + allocation_pools=allocation_pools, + segment_id=None) as subnet: + self._validate_l2_adjacency(network_id, is_adjacent=True) + data = {'subnet': {'segment_id': segment_id}} + self.new_update_request('subnets', data, subnet['subnet']['id']) + self.new_update_request( + 'subnets', data, subnet['subnet']['id']).get_response(self.api) + self._validate_l2_adjacency(network_id, is_adjacent=False) + self._assert_inventory_creation(segment_id, aggregate, subnet) + def _assert_inventory_update(self, segment_id, inventory, subnet=None, original_subnet=None): self.batch_notifier._notify() diff --git a/releasenotes/notes/allow-update-subnet-segment-id-association-1fb02ace27e85bb8.yaml b/releasenotes/notes/allow-update-subnet-segment-id-association-1fb02ace27e85bb8.yaml new file mode 100644 index 00000000000..95e49729301 --- /dev/null +++ b/releasenotes/notes/allow-update-subnet-segment-id-association-1fb02ace27e85bb8.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Add support for setting the ``segment_id`` for an existing + subnet. This enables users to convert a non-routed network + with no subnet/segment association to a routed one. It is + only possible to do this migration if both of the following + conditions are met - the current ``segment_id`` is ``None`` + and the network contains a single segment and subnet. +