Mark unaddressed ports with none in ip_allocation field

This is the Neutron side of the unaddressed ports blueprint. In order
to allow unaddressed ports, Nova wants the port to explicitly say it
is okay that it doesn't have any IP addresses.

In Neutron, an unaddressed port is one that was created by explicitly
passing [] in fixed_ips to create the port.  A new DB field is added
to the port to distinguish the unaddressed port case from the deferred
IP allocation case where routed networks is involved.

Change-Id: Ia61af4c14e955697a7d3fcc0bf4826a6d9475c98
Implements: blueprint vm-without-l3-address
APIImpact: port now has ip_allocation attribute, set on port create
This commit is contained in:
Carl Baldwin 2016-08-19 15:07:35 +00:00
parent eec6ebd425
commit b6a90df2ac
10 changed files with 132 additions and 17 deletions

View File

@ -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_mixin as rbac_mixin
from neutron.db import rbac_db_models as rbac_db from neutron.db import rbac_db_models as rbac_db
from neutron.db import standardattrdescription_db as stattr_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.extensions import l3
from neutron import ipam from neutron import ipam
from neutron.ipam import exceptions as ipam_exc
from neutron.ipam import subnet_alloc from neutron.ipam import subnet_alloc
from neutron import manager from neutron import manager
from neutron import neutron_plugin_base_v2 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) db_port = self._create_db_port_obj(context, port_data)
p['mac_address'] = db_port['mac_address'] 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 return db_port
def _validate_port_for_update(self, context, db_port, new_port, new_mac): def _validate_port_for_update(self, context, db_port, new_port, new_mac):

View File

@ -660,7 +660,7 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
if query.limit(1).count(): if query.limit(1).count():
# No, must be a deferred IP port because there are matching # No, must be a deferred IP port because there are matching
# subnets. Happens on routed networks when host isn't known. # subnets. Happens on routed networks when host isn't known.
return [] raise ipam_exceptions.DeferIpam()
raise ipam_exceptions.IpAddressGenerationFailureNoMatchingSubnet() raise ipam_exceptions.IpAddressGenerationFailureNoMatchingSubnet()

View File

@ -280,8 +280,12 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
removed = [] removed = []
changes = self._get_changed_ips_for_port( changes = self._get_changed_ips_for_port(
context, original_ips, new_ips, port['device_owner']) context, original_ips, new_ips, port['device_owner'])
subnets = self._ipam_get_subnets( try:
context, network_id=port['network_id'], host=host) 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 # Check if the IP's to add are OK
to_add = self._test_fixed_ips_for_port( to_add = self._test_fixed_ips_for_port(
context, port['network_id'], changes.add, context, port['network_id'], changes.add,

View File

@ -1 +1 @@
6b461a21bcfc 5cd92597d11d

View File

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

View File

@ -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_id = sa.Column(sa.String(attr.DEVICE_ID_MAX_LEN), nullable=False)
device_owner = sa.Column(sa.String(attr.DEVICE_OWNER_MAX_LEN), device_owner = sa.Column(sa.String(attr.DEVICE_OWNER_MAX_LEN),
nullable=False) nullable=False)
ip_allocation = sa.Column(sa.String(16))
__table_args__ = ( __table_args__ = (
sa.Index( sa.Index(
'ix_ports_network_id_mac_address', 'network_id', 'mac_address'), 'ix_ports_network_id_mac_address', 'network_id', 'mac_address'),

View File

@ -84,3 +84,7 @@ class IpamValueInvalid(exceptions.Conflict):
def __init__(self, message=None): def __init__(self, message=None):
self.message = message self.message = message
super(IpamValueInvalid, self).__init__() super(IpamValueInvalid, self).__init__()
class DeferIpam(exceptions.NeutronException):
message = _("Exception used to signal that IP allocation is deferred")

View File

@ -14,8 +14,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from sqlalchemy.orm import session
from neutron._i18n import _ from neutron._i18n import _
from neutron.api.v2 import attributes from neutron.api.v2 import attributes
from neutron.callbacks import events from neutron.callbacks import events
@ -50,15 +48,8 @@ def _extend_port_dict_binding(plugin, port_res, port_db):
return return
value = ip_allocation.IP_ALLOCATION_IMMEDIATE value = ip_allocation.IP_ALLOCATION_IMMEDIATE
if not port_res.get('fixed_ips'): if port_db.get('ip_allocation'):
# NOTE Only routed network ports have deferred allocation. Check if it value = port_db.get('ip_allocation')
# 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
port_res[ip_allocation.IP_ALLOCATION] = value port_res[ip_allocation.IP_ALLOCATION] = value

View File

@ -986,6 +986,46 @@ class TestSegmentAwareIpam(SegmentTestCase):
# Gets bad request because there are no eligible subnets. # Gets bad request because there are no eligible subnets.
self.assertEqual(webob.exc.HTTPBadRequest.code, response.status_int) 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): def test_port_without_ip_not_deferred(self):
"""Ports without addresses on non-routed networks are not deferred""" """Ports without addresses on non-routed networks are not deferred"""
with self.network() as network: with self.network() as network:
@ -1001,6 +1041,18 @@ class TestSegmentAwareIpam(SegmentTestCase):
request = self.new_show_request('ports', port['port']['id']) request = self.new_show_request('ports', port['port']['id'])
response = self.deserialize(self.fmt, request.get_response(self.api)) 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, self.assertEqual(ip_allocation.IP_ALLOCATION_IMMEDIATE,
response['port'][ip_allocation.IP_ALLOCATION]) 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 # Create the subnet and try to update the port to get an IP
with self.subnet(network=network, with self.subnet(network=network,
segment_id=segment['segment']['id']) as subnet: segment_id=segment['segment']['id']) as subnet:
self._validate_deferred_ip_allocation(port['port']['id'])
self._validate_l2_adjacency(network['network']['id'], self._validate_l2_adjacency(network['network']['id'],
is_adjacent=False) is_adjacent=False)
# Try requesting an IP (but the only subnet is on a segment) # Try requesting an IP (but the only subnet is on a segment)

View File

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