Associate subnets to segments through subnet API
Change-Id: Ia1084a94ac659332c126eb9d4787b04a89a4ba90 DocImpact: Need to add segment_id to API docs Partially-Implements: blueprint routed-networks
This commit is contained in:
parent
501b14f4ca
commit
f494de47fc
|
@ -16,7 +16,9 @@
|
|||
"default": "rule:admin_or_owner",
|
||||
|
||||
"create_subnet": "rule:admin_or_network_owner",
|
||||
"create_subnet:segment_id": "rule:admin_only",
|
||||
"get_subnet": "rule:admin_or_owner or rule:shared",
|
||||
"get_subnet:segment_id": "rule:admin_only",
|
||||
"update_subnet": "rule:admin_or_network_owner",
|
||||
"delete_subnet": "rule:admin_or_network_owner",
|
||||
|
||||
|
|
|
@ -32,7 +32,10 @@ from neutron.common import ipv6_utils
|
|||
from neutron.common import utils as common_utils
|
||||
from neutron.db import db_base_plugin_common
|
||||
from neutron.db import models_v2
|
||||
from neutron.db import segments_db
|
||||
from neutron.extensions import segment
|
||||
from neutron.ipam import utils as ipam_utils
|
||||
from neutron.services.segments import exceptions as segment_exc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
@ -308,6 +311,25 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||
msg = _('Exceeded maximum amount of fixed ips per port.')
|
||||
raise exc.InvalidInput(error_message=msg)
|
||||
|
||||
def _validate_segment(self, context, network_id, segment_id):
|
||||
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)
|
||||
if None in associated_segments and len(associated_segments) > 1:
|
||||
raise segment_exc.SubnetsNotAllAssociatedWithSegments(
|
||||
network_id=network_id)
|
||||
|
||||
if segment_id:
|
||||
query = context.session.query(segments_db.NetworkSegment)
|
||||
query = query.filter(segments_db.NetworkSegment.id == segment_id)
|
||||
segment = query.one()
|
||||
if segment.network_id != network_id:
|
||||
raise segment_exc.NetworkIdsDontMatch(
|
||||
subnet_network=network_id,
|
||||
segment_id=segment_id)
|
||||
if segment.is_dynamic:
|
||||
raise segment_exc.SubnetCantAssociateToDynamicSegment()
|
||||
|
||||
def _get_subnet_for_fixed_ip(self, context, fixed, subnets):
|
||||
# Subnets are all the subnets belonging to the same network.
|
||||
if not subnets:
|
||||
|
@ -447,7 +469,14 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||
subnet_args['ip_version'])
|
||||
|
||||
subnet = models_v2.Subnet(**subnet_args)
|
||||
context.session.add(subnet)
|
||||
segment_id = subnet_args.get('segment_id')
|
||||
try:
|
||||
context.session.add(subnet)
|
||||
context.session.flush()
|
||||
except db_exc.DBReferenceError:
|
||||
raise segment_exc.SegmentNotFound(segment_id=segment_id)
|
||||
self._validate_segment(context, network['id'], segment_id)
|
||||
|
||||
# NOTE(changzhi) Store DNS nameservers with order into DB one
|
||||
# by one when create subnet with DNS nameservers
|
||||
if validators.is_attr_set(dns_nameservers):
|
||||
|
@ -502,3 +531,17 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
|
|||
else:
|
||||
fixed_ip_list.append({'subnet_id': subnet['id']})
|
||||
return fixed_ip_list
|
||||
|
||||
def _ipam_get_subnets(self, context, network_id, segment_id):
|
||||
query = self._get_collection_query(context, models_v2.Subnet)
|
||||
query = query.filter(models_v2.Subnet.network_id == network_id)
|
||||
query = query.filter(models_v2.Subnet.segment_id == segment_id)
|
||||
subnets = [self._make_subnet_dict(c, context=context) for c in query]
|
||||
return subnets
|
||||
|
||||
def _make_subnet_args(self, detail, subnet, subnetpool_id):
|
||||
args = super(IpamBackendMixin, self)._make_subnet_args(
|
||||
detail, subnet, subnetpool_id)
|
||||
if validators.is_attr_set(subnet.get(segment.SEGMENT_ID)):
|
||||
args['segment_id'] = subnet[segment.SEGMENT_ID]
|
||||
return args
|
||||
|
|
|
@ -318,8 +318,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||
added = []
|
||||
changes = self._get_changed_ips_for_port(context, original_ips,
|
||||
new_ips, device_owner)
|
||||
net_id_filter = {'network_id': [network_id]}
|
||||
subnets = self._get_subnets(context, filters=net_id_filter)
|
||||
subnets = self._ipam_get_subnets(
|
||||
context, network_id=network_id, segment_id=None)
|
||||
# Check if the IP's to add are OK
|
||||
to_add = self._test_fixed_ips_for_port(context, network_id,
|
||||
changes.add, device_owner,
|
||||
|
@ -351,8 +351,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||
a subnet_id then allocate an IP address accordingly.
|
||||
"""
|
||||
p = port['port']
|
||||
net_id_filter = {'network_id': [p['network_id']]}
|
||||
subnets = self._get_subnets(context, filters=net_id_filter)
|
||||
subnets = self._ipam_get_subnets(
|
||||
context, network_id=p['network_id'], segment_id=None)
|
||||
|
||||
v4, v6_stateful, v6_stateless = self._classify_subnets(
|
||||
context, subnets)
|
||||
|
|
|
@ -203,8 +203,8 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||
a subnet_id then allocate an IP address accordingly.
|
||||
"""
|
||||
p = port['port']
|
||||
net_id_filter = {'network_id': [p['network_id']]}
|
||||
subnets = self._get_subnets(context, filters=net_id_filter)
|
||||
subnets = self._ipam_get_subnets(
|
||||
context, network_id=p['network_id'], segment_id=None)
|
||||
|
||||
v4, v6_stateful, v6_stateless = self._classify_subnets(
|
||||
context, subnets)
|
||||
|
@ -283,8 +283,8 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
|
|||
removed = []
|
||||
changes = self._get_changed_ips_for_port(
|
||||
context, original_ips, new_ips, port['device_owner'])
|
||||
net_id_filter = {'network_id': [port['network_id']]}
|
||||
subnets = self._get_subnets(context, filters=net_id_filter)
|
||||
subnets = self._ipam_get_subnets(
|
||||
context, network_id=port['network_id'], segment_id=None)
|
||||
# Check if the IP's to add are OK
|
||||
to_add = self._test_fixed_ips_for_port(
|
||||
context, port['network_id'], changes.add,
|
||||
|
|
|
@ -1 +1 @@
|
|||
89ab9a816d70
|
||||
c879c5e1ee90
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# Copyright (c) 2016 Hewlett Packard Enterprise Development Company, L.P.
|
||||
#
|
||||
# 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 segment_id to subnet """
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c879c5e1ee90'
|
||||
down_revision = '89ab9a816d70'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('subnets',
|
||||
sa.Column('segment_id', sa.String(length=36), nullable=True))
|
||||
op.create_foreign_key(
|
||||
None, 'subnets', 'networksegments', ['segment_id'], ['id'])
|
|
@ -185,6 +185,8 @@ class Subnet(model_base.HasStandardAttributes, model_base.BASEV2,
|
|||
|
||||
name = sa.Column(sa.String(attr.NAME_MAX_LEN))
|
||||
network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id'))
|
||||
# Added by the segments service plugin
|
||||
segment_id = sa.Column(sa.String(36), sa.ForeignKey('networksegments.id'))
|
||||
subnetpool_id = sa.Column(sa.String(36), index=True)
|
||||
# NOTE: Explicitly specify join conditions for the relationship because
|
||||
# subnetpool_id in subnet might be 'prefix_delegation' when the IPv6 Prefix
|
||||
|
|
|
@ -63,6 +63,13 @@ RESOURCE_ATTRIBUTE_MAP = {
|
|||
'convert_to': attributes.convert_to_int,
|
||||
'is_visible': True},
|
||||
},
|
||||
attributes.SUBNETS: {
|
||||
SEGMENT_ID: {'allow_post': True,
|
||||
'allow_put': False,
|
||||
'default': None,
|
||||
'validate': {'type:uuid_or_none': None},
|
||||
'is_visible': True, },
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -22,11 +22,21 @@ from oslo_log import helpers as log_helpers
|
|||
from oslo_utils import uuidutils
|
||||
from sqlalchemy.orm import exc
|
||||
|
||||
from neutron.api.v2 import attributes
|
||||
from neutron.db import common_db_mixin
|
||||
from neutron.db import segments_db as db
|
||||
from neutron.services.segments import exceptions
|
||||
|
||||
|
||||
def _extend_subnet_dict_binding(plugin, subnet_res, subnet_db):
|
||||
subnet_res['segment_id'] = subnet_db.get('segment_id')
|
||||
|
||||
|
||||
# Register dict extend functions for subnets
|
||||
common_db_mixin.CommonDbMixin.register_dict_extend_funcs(
|
||||
attributes.SUBNETS, [_extend_subnet_dict_binding])
|
||||
|
||||
|
||||
class SegmentDbMixin(common_db_mixin.CommonDbMixin):
|
||||
"""Mixin class to add segment."""
|
||||
|
||||
|
|
|
@ -21,3 +21,18 @@ from neutron_lib import exceptions
|
|||
|
||||
class SegmentNotFound(exceptions.NotFound):
|
||||
message = _("Segment %(segment_id)s could not be found.")
|
||||
|
||||
|
||||
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 "
|
||||
"any segment.")
|
||||
|
||||
|
||||
class SubnetCantAssociateToDynamicSegment(exceptions.BadRequest):
|
||||
message = _("A subnet cannot be associated with a dynamic segment.")
|
||||
|
||||
|
||||
class NetworkIdsDontMatch(exceptions.BadRequest):
|
||||
message = _("The subnet's network id, '%(subnet_network)s', doesn't match "
|
||||
"the network_id of segment '%(segment_id)s'")
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
"default": "rule:admin_or_owner",
|
||||
|
||||
"create_subnet": "rule:admin_or_network_owner",
|
||||
"create_subnet:segment_id": "rule:admin_only",
|
||||
"get_subnet": "rule:admin_or_owner or rule:shared",
|
||||
"get_subnet:segment_id": "rule:admin_only",
|
||||
"update_subnet": "rule:admin_or_network_owner",
|
||||
"delete_subnet": "rule:admin_or_network_owner",
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ from neutron.common import utils
|
|||
from neutron import context
|
||||
from neutron.db import api as db_api
|
||||
from neutron.db import db_base_plugin_common
|
||||
from neutron.db import ipam_backend_mixin
|
||||
from neutron.db import ipam_non_pluggable_backend as non_ipam
|
||||
from neutron.db import l3_db
|
||||
from neutron.db import models_v2
|
||||
|
@ -324,7 +325,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
|||
if cidr:
|
||||
data['subnet']['cidr'] = cidr
|
||||
for arg in ('ip_version', 'tenant_id', 'subnetpool_id', 'prefixlen',
|
||||
'enable_dhcp', 'allocation_pools',
|
||||
'enable_dhcp', 'allocation_pools', 'segment_id',
|
||||
'dns_nameservers', 'host_routes',
|
||||
'shared', 'ipv6_ra_mode', 'ipv6_address_mode'):
|
||||
# Arg must be present and not null (but can be false)
|
||||
|
@ -449,11 +450,12 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
|||
allocation_pools=None, ip_version=4, enable_dhcp=True,
|
||||
dns_nameservers=None, host_routes=None, shared=None,
|
||||
ipv6_ra_mode=None, ipv6_address_mode=None,
|
||||
tenant_id=None, set_context=False):
|
||||
tenant_id=None, set_context=False, segment_id=None):
|
||||
res = self._create_subnet(fmt,
|
||||
net_id=network['network']['id'],
|
||||
cidr=cidr,
|
||||
subnetpool_id=subnetpool_id,
|
||||
segment_id=segment_id,
|
||||
gateway_ip=gateway,
|
||||
tenant_id=(tenant_id or
|
||||
network['network']['tenant_id']),
|
||||
|
@ -607,6 +609,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
|||
gateway_ip=constants.ATTR_NOT_SPECIFIED,
|
||||
cidr='10.0.0.0/24',
|
||||
subnetpool_id=None,
|
||||
segment_id=None,
|
||||
fmt=None,
|
||||
ip_version=4,
|
||||
allocation_pools=None,
|
||||
|
@ -631,6 +634,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
|||
enable_dhcp,
|
||||
dns_nameservers,
|
||||
host_routes,
|
||||
segment_id=segment_id,
|
||||
shared=shared,
|
||||
ipv6_ra_mode=ipv6_ra_mode,
|
||||
ipv6_address_mode=ipv6_address_mode,
|
||||
|
@ -1292,8 +1296,8 @@ fixed_ips=ip_address%%3D%s&fixed_ips=ip_address%%3D%s&fixed_ips=subnet_id%%3D%s
|
|||
data = {'port': {'fixed_ips': [{'subnet_id':
|
||||
subnet['subnet']['id']}]}}
|
||||
# mock _get_subnets, to return this subnet
|
||||
mock.patch.object(db_base_plugin_common.DbBasePluginCommon,
|
||||
'_get_subnets',
|
||||
mock.patch.object(ipam_backend_mixin.IpamBackendMixin,
|
||||
'_ipam_get_subnets',
|
||||
return_value=[subnet['subnet']]).start()
|
||||
# Delete subnet, to mock the subnet as stale.
|
||||
self._delete('subnets', subnet['subnet']['id'])
|
||||
|
|
|
@ -20,8 +20,8 @@ from oslo_config import cfg
|
|||
|
||||
from neutron.common import constants
|
||||
from neutron.common import ipv6_utils
|
||||
from neutron.db import db_base_plugin_common
|
||||
from neutron.db import db_base_plugin_v2
|
||||
from neutron.db import ipam_backend_mixin
|
||||
from neutron.db import ipam_non_pluggable_backend as non_ipam
|
||||
from neutron.db import models_v2
|
||||
from neutron.tests import base
|
||||
|
@ -143,8 +143,8 @@ class TestIpamNonPluggableBackend(base.BaseTestCase):
|
|||
# were not actually created, so no ipam_subnet exists
|
||||
cfg.CONF.set_override("ipam_driver", None)
|
||||
plugin = db_base_plugin_v2.NeutronDbPluginV2()
|
||||
with mock.patch.object(db_base_plugin_common.DbBasePluginCommon,
|
||||
'_get_subnets') as get_subnets:
|
||||
with mock.patch.object(ipam_backend_mixin.IpamBackendMixin,
|
||||
'_ipam_get_subnets') as get_subnets:
|
||||
with mock.patch.object(non_ipam.IpamNonPluggableBackend,
|
||||
'_check_unique_ip') as check_unique:
|
||||
context = mock.Mock()
|
||||
|
|
|
@ -582,7 +582,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
|||
fixed_ips_mock = mock.Mock(return_value=changes.add)
|
||||
mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
|
||||
mocks['ipam']._get_changed_ips_for_port = changes_mock
|
||||
mocks['ipam']._get_subnets = mock.Mock()
|
||||
mocks['ipam']._ipam_get_subnets = mock.Mock()
|
||||
mocks['ipam']._test_fixed_ips_for_port = fixed_ips_mock
|
||||
mocks['ipam']._update_ips_for_pd_subnet = mock.Mock(return_value=[])
|
||||
|
||||
|
@ -592,9 +592,8 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
|||
mocks['ipam']._update_ips_for_port(context, port_dict,
|
||||
original_ips, new_ips, mac)
|
||||
mocks['driver'].get_address_request_factory.assert_called_once_with()
|
||||
net_id_filter = {'network_id': [port_dict['network_id']]}
|
||||
mocks['ipam']._get_subnets.assert_called_once_with(
|
||||
context, filters=net_id_filter)
|
||||
mocks['ipam']._ipam_get_subnets.assert_called_once_with(
|
||||
context, network_id=port_dict['network_id'], segment_id=None)
|
||||
# Validate port_dict is passed into address_factory
|
||||
address_factory.get_request.assert_called_once_with(context,
|
||||
port_dict,
|
||||
|
@ -623,7 +622,7 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
|
|||
get_subnets_mock = mock.Mock(return_value=subnets)
|
||||
get_subnet_mock = mock.Mock(return_value=subnets[0])
|
||||
mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
|
||||
mocks['ipam']._get_subnets = get_subnets_mock
|
||||
mocks['ipam']._ipam_get_subnets = get_subnets_mock
|
||||
mocks['ipam']._get_subnet = get_subnet_mock
|
||||
|
||||
mocks['ipam'].allocate_ips_for_port_and_store(context,
|
||||
|
|
|
@ -12,10 +12,14 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from neutron_lib import constants
|
||||
from oslo_utils import uuidutils
|
||||
import webob.exc
|
||||
|
||||
from neutron.api.v2 import attributes
|
||||
from neutron import context
|
||||
from neutron.db import db_base_plugin_v2
|
||||
from neutron.db import segments_db
|
||||
from neutron.extensions import segment as ext_segment
|
||||
from neutron.services.segments import db
|
||||
from neutron.tests.unit.db import test_db_base_plugin_v2
|
||||
|
@ -156,3 +160,104 @@ class TestSegment(SegmentTestCase):
|
|||
segmentation_id=200)
|
||||
res = self._list('segments')
|
||||
self.assertEqual(2, len(res['segments']))
|
||||
|
||||
|
||||
class TestSegmentSubnetAssociation(SegmentTestCase):
|
||||
def test_basic_association(self):
|
||||
with self.network() as network:
|
||||
net = network['network']
|
||||
|
||||
segment = self._test_create_segment(network_id=net['id'])
|
||||
segment_id = segment['segment']['id']
|
||||
|
||||
with self.subnet(network=network, segment_id=segment_id) as subnet:
|
||||
subnet = subnet['subnet']
|
||||
|
||||
request = self.new_show_request('subnets', subnet['id'])
|
||||
response = request.get_response(self.api)
|
||||
res = self.deserialize(self.fmt, response)
|
||||
self.assertEqual(segment_id,
|
||||
res['subnet']['segment_id'])
|
||||
|
||||
def test_association_network_mismatch(self):
|
||||
with self.network() as network1:
|
||||
with self.network() as network2:
|
||||
net = network1['network']
|
||||
|
||||
segment = self._test_create_segment(network_id=net['id'])
|
||||
|
||||
res = self._create_subnet(self.fmt,
|
||||
net_id=network2['network']['id'],
|
||||
tenant_id=network2['network']['tenant_id'],
|
||||
gateway_ip=constants.ATTR_NOT_SPECIFIED,
|
||||
cidr='10.0.0.0/24',
|
||||
segment_id=segment['segment']['id'])
|
||||
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
|
||||
|
||||
def test_association_segment_not_found(self):
|
||||
with self.network() as network:
|
||||
net = network['network']
|
||||
|
||||
segment_id = uuidutils.generate_uuid()
|
||||
|
||||
res = self._create_subnet(self.fmt,
|
||||
net_id=net['id'],
|
||||
tenant_id=net['tenant_id'],
|
||||
gateway_ip=constants.ATTR_NOT_SPECIFIED,
|
||||
cidr='10.0.0.0/24',
|
||||
segment_id=segment_id)
|
||||
self.assertEqual(webob.exc.HTTPNotFound.code, res.status_int)
|
||||
|
||||
def test_only_some_subnets_associated_not_allowed(self):
|
||||
with self.network() as network:
|
||||
with self.subnet(network=network):
|
||||
net = network['network']
|
||||
|
||||
segment = self._test_create_segment(network_id=net['id'])
|
||||
|
||||
res = self._create_subnet(self.fmt,
|
||||
net_id=net['id'],
|
||||
tenant_id=net['tenant_id'],
|
||||
gateway_ip=constants.ATTR_NOT_SPECIFIED,
|
||||
cidr='10.0.1.0/24',
|
||||
segment_id=segment['segment']['id'])
|
||||
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
|
||||
|
||||
def test_association_to_dynamic_segment_not_allowed(self):
|
||||
cxt = context.get_admin_context()
|
||||
with self.network() as network:
|
||||
net = network['network']
|
||||
|
||||
# Can't create a dynamic segment through the API
|
||||
segment = {segments_db.NETWORK_TYPE: 'phys_net',
|
||||
segments_db.PHYSICAL_NETWORK: 'net_type',
|
||||
segments_db.SEGMENTATION_ID: 200}
|
||||
segments_db.add_network_segment(cxt.session,
|
||||
network_id=net['id'],
|
||||
segment=segment,
|
||||
is_dynamic=True)
|
||||
|
||||
res = self._create_subnet(self.fmt,
|
||||
net_id=net['id'],
|
||||
tenant_id=net['tenant_id'],
|
||||
gateway_ip=constants.ATTR_NOT_SPECIFIED,
|
||||
cidr='10.0.0.0/24',
|
||||
segment_id=segment['id'])
|
||||
self.assertEqual(webob.exc.HTTPBadRequest.code, res.status_int)
|
||||
|
||||
def test_port_create_with_segment_subnets(self):
|
||||
with self.network() as network:
|
||||
net = network['network']
|
||||
|
||||
segment = self._test_create_segment(network_id=net['id'])
|
||||
segment_id = segment['segment']['id']
|
||||
|
||||
with self.subnet(network=network, segment_id=segment_id) as subnet:
|
||||
subnet = subnet['subnet']
|
||||
|
||||
response = self._create_port(self.fmt,
|
||||
net_id=net['id'],
|
||||
tenant_id=net['tenant_id'])
|
||||
res = self.deserialize(self.fmt, response)
|
||||
# Don't allocate IPs in this case because it doesn't have binding info
|
||||
self.assertEqual(0, len(res['port']['fixed_ips']))
|
||||
|
|
Loading…
Reference in New Issue