Merge "Allow setting network-segment on subnet update"
This commit is contained in:
commit
605ae950f9
|
@ -471,3 +471,75 @@ segment contains one IPv4 subnet and one IPv6 subnet.
|
||||||
| status | DOWN |
|
| status | DOWN |
|
||||||
| tags | [] |
|
| 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 |
|
||||||
|
+------------+--------------------------------------+
|
||||||
|
|
|
@ -869,6 +869,9 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||||
s['project_id'] = subnet_obj.project_id
|
s['project_id'] = subnet_obj.project_id
|
||||||
s['tenant_id'] = subnet_obj.project_id
|
s['tenant_id'] = subnet_obj.project_id
|
||||||
s['subnetpool_id'] = subnet_obj.subnetpool_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)
|
self._validate_subnet(context, s, cur_subnet=subnet_obj)
|
||||||
db_pools = [netaddr.IPRange(p.start, p.end)
|
db_pools = [netaddr.IPRange(p.start, p.end)
|
||||||
for p in subnet_obj.allocation_pools]
|
for p in subnet_obj.allocation_pools]
|
||||||
|
|
|
@ -316,7 +316,7 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
||||||
pool_2=r_range,
|
pool_2=r_range,
|
||||||
subnet_cidr=subnet_cidr)
|
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 = context.session.query(models_v2.Subnet.segment_id)
|
||||||
query = query.filter(models_v2.Subnet.network_id == network_id)
|
query = query.filter(models_v2.Subnet.network_id == network_id)
|
||||||
associated_segments = set(row.segment_id for row in query)
|
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(
|
raise segment_exc.SubnetsNotAllAssociatedWithSegments(
|
||||||
network_id=network_id)
|
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:
|
if segment_id:
|
||||||
segment = network_obj.NetworkSegment.get_object(context,
|
segment = network_obj.NetworkSegment.get_object(context,
|
||||||
id=segment_id)
|
id=segment_id)
|
||||||
|
|
|
@ -428,6 +428,9 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
||||||
port['fixed_ips'])
|
port['fixed_ips'])
|
||||||
|
|
||||||
def update_db_subnet(self, context, id, s, old_pools):
|
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
|
# 'allocation_pools' is removed from 's' in
|
||||||
# _update_subnet_allocation_pools (ipam_backend_mixin),
|
# _update_subnet_allocation_pools (ipam_backend_mixin),
|
||||||
# so create unchanged copy for ipam driver
|
# so create unchanged copy for ipam driver
|
||||||
|
|
|
@ -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
|
|
@ -198,7 +198,7 @@ class Subnet(base.NeutronDbObject):
|
||||||
|
|
||||||
foreign_keys = {'Network': {'network_id': 'id'}}
|
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 = {
|
fields_need_translation = {
|
||||||
'host_routes': 'routes'
|
'host_routes': 'routes'
|
||||||
|
|
|
@ -23,6 +23,13 @@ class SegmentNotFound(exceptions.NotFound):
|
||||||
message = _("Segment %(segment_id)s could not be found.")
|
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):
|
class SubnetsNotAllAssociatedWithSegments(exceptions.BadRequest):
|
||||||
message = _("All of the subnets on network '%(network_id)s' must either "
|
message = _("All of the subnets on network '%(network_id)s' must either "
|
||||||
"all be associated with segments or all not associated with "
|
"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.")
|
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):
|
class NetworkIdsDontMatch(exceptions.BadRequest):
|
||||||
message = _("The subnet's network id, '%(subnet_network)s', doesn't match "
|
message = _("The subnet's network id, '%(subnet_network)s', doesn't match "
|
||||||
"the network_id of segment '%(segment_id)s'")
|
"the network_id of segment '%(segment_id)s'")
|
||||||
|
|
|
@ -59,7 +59,8 @@ class Plugin(db.SegmentDbMixin, segment.SegmentPluginBase):
|
||||||
|
|
||||||
supported_extension_aliases = ["segment", "ip_allocation",
|
supported_extension_aliases = ["segment", "ip_allocation",
|
||||||
l2adj_apidef.ALIAS,
|
l2adj_apidef.ALIAS,
|
||||||
"standard-attr-segment"]
|
"standard-attr-segment",
|
||||||
|
"subnet-segmentid-writable"]
|
||||||
|
|
||||||
__native_pagination_support = True
|
__native_pagination_support = True
|
||||||
__native_sorting_support = True
|
__native_sorting_support = True
|
||||||
|
@ -173,12 +174,7 @@ class NovaSegmentNotifier(object):
|
||||||
'update routed networks IPv4 inventories')
|
'update routed networks IPv4 inventories')
|
||||||
return
|
return
|
||||||
|
|
||||||
@registry.receives(resources.SUBNET, [events.AFTER_CREATE])
|
def _notify_subnet(self, context, subnet, segment_id):
|
||||||
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
|
|
||||||
total, reserved = self._calculate_inventory_total_and_reserved(subnet)
|
total, reserved = self._calculate_inventory_total_and_reserved(subnet)
|
||||||
if total:
|
if total:
|
||||||
segment_host_mappings = net_obj.SegmentHostMapping.get_objects(
|
segment_host_mappings = net_obj.SegmentHostMapping.get_objects(
|
||||||
|
@ -188,6 +184,14 @@ class NovaSegmentNotifier(object):
|
||||||
reserved=reserved,
|
reserved=reserved,
|
||||||
segment_host_mappings=segment_host_mappings))
|
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):
|
def _create_or_update_nova_inventory(self, event):
|
||||||
try:
|
try:
|
||||||
self._update_nova_inventory(event)
|
self._update_nova_inventory(event)
|
||||||
|
@ -261,8 +265,13 @@ class NovaSegmentNotifier(object):
|
||||||
def _notify_subnet_updated(self, resource, event, trigger, context,
|
def _notify_subnet_updated(self, resource, event, trigger, context,
|
||||||
subnet, original_subnet, **kwargs):
|
subnet, original_subnet, **kwargs):
|
||||||
segment_id = subnet.get('segment_id')
|
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:
|
if not segment_id or subnet['ip_version'] != constants.IP_VERSION_4:
|
||||||
return
|
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],
|
filters = {'segment_id': [segment_id],
|
||||||
'ip_version': [constants.IP_VERSION_4]}
|
'ip_version': [constants.IP_VERSION_4]}
|
||||||
if not subnet['allocation_pools']:
|
if not subnet['allocation_pools']:
|
||||||
|
|
|
@ -521,6 +521,86 @@ class TestSegmentSubnetAssociation(SegmentTestCase):
|
||||||
segment_id=segment['id'])
|
segment_id=segment['id'])
|
||||||
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
|
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):
|
class HostSegmentMappingTestCase(SegmentTestCase):
|
||||||
_mechanism_drivers = ['logger']
|
_mechanism_drivers = ['logger']
|
||||||
|
@ -1620,6 +1700,33 @@ class TestNovaSegmentNotifier(SegmentAwareIpamTestCase):
|
||||||
def test_first_subnet_association_with_segment(self):
|
def test_first_subnet_association_with_segment(self):
|
||||||
self._test_first_subnet_association_with_segment()
|
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,
|
def _assert_inventory_update(self, segment_id, inventory, subnet=None,
|
||||||
original_subnet=None):
|
original_subnet=None):
|
||||||
self.batch_notifier._notify()
|
self.batch_notifier._notify()
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue