Allow setting network-segment on subnet update

To enable the possibility to migrate a non-routed network to a
routed network allow updating the segment_id of a subnet.

Only allow the operation if:
 - The network only has one segment
 - The network only has one subnet
 - The current segment_id == None

APIImpact: The segment_id attribute of subnets now allows put operation.
Closes-Bug: #1692490
Depends-On: Iffda823a149a1143f46ee9a05e9640b34bf42c51
Change-Id: I1aee29dfb59e9769ec0f1cb1f5d2933bc5dc0dc5
This commit is contained in:
Harald Jensas 2017-11-27 20:57:25 +01:00 committed by Harald Jensås
parent edc909a267
commit b6d117fcd5
10 changed files with 262 additions and 9 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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']:

View File

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

View File

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