diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py index b1669e9aa1b..cd32061c193 100644 --- a/neutron/db/db_base_plugin_v2.py +++ b/neutron/db/db_base_plugin_v2.py @@ -47,8 +47,10 @@ from neutron.db import models_v2 from neutron.db import rbac_db_mixin as rbac_mixin from neutron.db import rbac_db_models as rbac_db from neutron.db import standardattrdescription_db as stattr_db +from neutron.extensions import ip_allocation as ipa from neutron.extensions import l3 from neutron import ipam +from neutron.ipam import exceptions as ipam_exc from neutron.ipam import subnet_alloc from neutron import manager from neutron import neutron_plugin_base_v2 @@ -1138,7 +1140,17 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon, db_port = self._create_db_port_obj(context, port_data) p['mac_address'] = db_port['mac_address'] - self.ipam.allocate_ips_for_port_and_store(context, port, port_id) + try: + self.ipam.allocate_ips_for_port_and_store( + context, port, port_id) + db_port['ip_allocation'] = ipa.IP_ALLOCATION_IMMEDIATE + except ipam_exc.DeferIpam: + db_port['ip_allocation'] = ipa.IP_ALLOCATION_DEFERRED + fixed_ips = p['fixed_ips'] + if validators.is_attr_set(fixed_ips) and not fixed_ips: + # [] was passed explicitly as fixed_ips. An unaddressed port. + db_port['ip_allocation'] = ipa.IP_ALLOCATION_NONE + return db_port def _validate_port_for_update(self, context, db_port, new_port, new_mac): diff --git a/neutron/db/ipam_backend_mixin.py b/neutron/db/ipam_backend_mixin.py index d30b4ffc730..3974d208408 100644 --- a/neutron/db/ipam_backend_mixin.py +++ b/neutron/db/ipam_backend_mixin.py @@ -660,7 +660,7 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon): if query.limit(1).count(): # No, must be a deferred IP port because there are matching # subnets. Happens on routed networks when host isn't known. - return [] + raise ipam_exceptions.DeferIpam() raise ipam_exceptions.IpAddressGenerationFailureNoMatchingSubnet() diff --git a/neutron/db/ipam_pluggable_backend.py b/neutron/db/ipam_pluggable_backend.py index ad6e9cdcd6a..05b6291657e 100644 --- a/neutron/db/ipam_pluggable_backend.py +++ b/neutron/db/ipam_pluggable_backend.py @@ -280,8 +280,12 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin): removed = [] changes = self._get_changed_ips_for_port( context, original_ips, new_ips, port['device_owner']) - subnets = self._ipam_get_subnets( - context, network_id=port['network_id'], host=host) + try: + subnets = self._ipam_get_subnets( + context, network_id=port['network_id'], host=host) + except ipam_exc.DeferIpam: + subnets = [] + # Check if the IP's to add are OK to_add = self._test_fixed_ips_for_port( context, port['network_id'], changes.add, diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index 23f7e806fa7..67207acbf06 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -6b461a21bcfc +5cd92597d11d diff --git a/neutron/db/migration/alembic_migrations/versions/newton/expand/5cd92597d11d_add_ip_allocation_to_port.py b/neutron/db/migration/alembic_migrations/versions/newton/expand/5cd92597d11d_add_ip_allocation_to_port.py new file mode 100644 index 00000000000..02df1c2436a --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/newton/expand/5cd92597d11d_add_ip_allocation_to_port.py @@ -0,0 +1,28 @@ +# 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. +# + +"""Add ip_allocation to port """ + +# revision identifiers, used by Alembic. +revision = '5cd92597d11d' +down_revision = '6b461a21bcfc' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('ports', + sa.Column('ip_allocation', + sa.String(length=16), + nullable=True)) diff --git a/neutron/db/models_v2.py b/neutron/db/models_v2.py index 2b9a5e2e60b..e274ffac7f1 100644 --- a/neutron/db/models_v2.py +++ b/neutron/db/models_v2.py @@ -133,6 +133,8 @@ class Port(standard_attr.HasStandardAttributes, model_base.BASEV2, device_id = sa.Column(sa.String(attr.DEVICE_ID_MAX_LEN), nullable=False) device_owner = sa.Column(sa.String(attr.DEVICE_OWNER_MAX_LEN), nullable=False) + ip_allocation = sa.Column(sa.String(16)) + __table_args__ = ( sa.Index( 'ix_ports_network_id_mac_address', 'network_id', 'mac_address'), diff --git a/neutron/ipam/exceptions.py b/neutron/ipam/exceptions.py index 29a4a947cbb..c268199a98c 100644 --- a/neutron/ipam/exceptions.py +++ b/neutron/ipam/exceptions.py @@ -84,3 +84,7 @@ class IpamValueInvalid(exceptions.Conflict): def __init__(self, message=None): self.message = message super(IpamValueInvalid, self).__init__() + + +class DeferIpam(exceptions.NeutronException): + message = _("Exception used to signal that IP allocation is deferred") diff --git a/neutron/services/segments/plugin.py b/neutron/services/segments/plugin.py index 26df22b451a..c2ff2224bc4 100644 --- a/neutron/services/segments/plugin.py +++ b/neutron/services/segments/plugin.py @@ -14,8 +14,6 @@ # License for the specific language governing permissions and limitations # under the License. -from sqlalchemy.orm import session - from neutron._i18n import _ from neutron.api.v2 import attributes from neutron.callbacks import events @@ -50,15 +48,8 @@ def _extend_port_dict_binding(plugin, port_res, port_db): return value = ip_allocation.IP_ALLOCATION_IMMEDIATE - if not port_res.get('fixed_ips'): - # NOTE Only routed network ports have deferred allocation. Check if it - # is routed by looking for subnets associated with segments. - object_session = session.Session.object_session(port_db) - query = object_session.query(models_v2.Subnet) - query = query.filter_by(network_id=port_db.network_id) - query = query.filter(models_v2.Subnet.segment_id.isnot(None)) - if query.count(): - value = ip_allocation.IP_ALLOCATION_DEFERRED + if port_db.get('ip_allocation'): + value = port_db.get('ip_allocation') port_res[ip_allocation.IP_ALLOCATION] = value diff --git a/neutron/tests/unit/extensions/test_segment.py b/neutron/tests/unit/extensions/test_segment.py index 0bba352937f..be2d84fd8b9 100644 --- a/neutron/tests/unit/extensions/test_segment.py +++ b/neutron/tests/unit/extensions/test_segment.py @@ -986,6 +986,46 @@ class TestSegmentAwareIpam(SegmentTestCase): # Gets bad request because there are no eligible subnets. self.assertEqual(webob.exc.HTTPBadRequest.code, response.status_int) + def _create_port_and_show(self, network, **kwargs): + response = self._create_port( + self.fmt, + net_id=network['network']['id'], + tenant_id=network['network']['tenant_id'], + **kwargs) + port = self.deserialize(self.fmt, response) + request = self.new_show_request('ports', port['port']['id']) + return self.deserialize(self.fmt, request.get_response(self.api)) + + def test_port_create_with_no_fixed_ips_no_ipam_on_routed_network(self): + """Ports requesting no fixed_ips not deferred, even on routed net""" + with self.network() as network: + segment = self._test_create_segment( + network_id=network['network']['id'], + physical_network='physnet', + network_type=p_constants.TYPE_VLAN) + with self.subnet(network=network, + segment_id=segment['segment']['id']): + pass + + # Create an unbound port requesting no IP addresses + response = self._create_port_and_show(network, fixed_ips=[]) + self.assertEqual([], response['port']['fixed_ips']) + self.assertEqual(ip_allocation.IP_ALLOCATION_NONE, + response['port'][ip_allocation.IP_ALLOCATION]) + + def test_port_create_with_no_fixed_ips_no_ipam(self): + """Ports without addresses on non-routed networks are not deferred""" + with self.network() as network: + with self.subnet(network=network): + pass + + # Create an unbound port requesting no IP addresses + response = self._create_port_and_show(network, fixed_ips=[]) + + self.assertEqual([], response['port']['fixed_ips']) + self.assertEqual(ip_allocation.IP_ALLOCATION_NONE, + response['port'][ip_allocation.IP_ALLOCATION]) + def test_port_without_ip_not_deferred(self): """Ports without addresses on non-routed networks are not deferred""" with self.network() as network: @@ -1001,6 +1041,18 @@ class TestSegmentAwareIpam(SegmentTestCase): request = self.new_show_request('ports', port['port']['id']) response = self.deserialize(self.fmt, request.get_response(self.api)) + self.assertEqual([], response['port']['fixed_ips']) + self.assertEqual(ip_allocation.IP_ALLOCATION_IMMEDIATE, + response['port'][ip_allocation.IP_ALLOCATION]) + + def test_port_without_ip_not_deferred_no_binding(self): + """Ports without addresses on non-routed networks are not deferred""" + with self.network() as network: + pass + + # Create a unbound port with no IP address (since there is no subnet) + response = self._create_port_and_show(network) + self.assertEqual([], response['port']['fixed_ips']) self.assertEqual(ip_allocation.IP_ALLOCATION_IMMEDIATE, response['port'][ip_allocation.IP_ALLOCATION]) @@ -1026,7 +1078,6 @@ class TestSegmentAwareIpam(SegmentTestCase): # Create the subnet and try to update the port to get an IP with self.subnet(network=network, segment_id=segment['segment']['id']) as subnet: - self._validate_deferred_ip_allocation(port['port']['id']) self._validate_l2_adjacency(network['network']['id'], is_adjacent=False) # Try requesting an IP (but the only subnet is on a segment) diff --git a/releasenotes/notes/add-port-ip-allocation-attr-294a580641998240.yaml b/releasenotes/notes/add-port-ip-allocation-attr-294a580641998240.yaml new file mode 100644 index 00000000000..8ce2b8d3daf --- /dev/null +++ b/releasenotes/notes/add-port-ip-allocation-attr-294a580641998240.yaml @@ -0,0 +1,23 @@ +--- +prelude: > + Add ip_allocation attribute to port resources +features: + - The port resource now has an ip_allocation attribute. + The value of this attribute will be set to + 'immediate', 'deferred', or 'none' at the time the + port is created. It will not be changed when the port + is updated. + + 'immediate' means that the port is expected to have + an IP address and Neutron attempted IP allocation on + port creation. 'deferred' means that the port is + expected to have an IP address but Neutron deferred + IP allocation until a port update provides the host + to which the port will be bound. 'none' means that + the port was created explicitly with no addresses by + passing [] in fixed_ips when creating it. +upgrade: + - All existing ports are considered to have 'immediate' + IP allocation. Any ports that do not have this + attribute should also be considered to have immediate + IP allocation.