]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
Subnet allocation from a subnet pool
authorRyan Tidwell <ryan.tidwell@hp.com>
Thu, 19 Feb 2015 23:29:08 +0000 (15:29 -0800)
committerRyan Tidwell <ryan.tidwell@hp.com>
Tue, 31 Mar 2015 20:44:14 +0000 (13:44 -0700)
Contains API changes, model changes, and logic required to enable a subnet to
be allocated from a subnet pool. Users can request a subnet allocation by
supplying subnetpool_id and optionally prefixlen or cidr. If cidr is
specified, an attempt is made to allocate the given CIDR from the pool. If
prefixlen is specified, an attempt is made to allocate any CIDR with the
given prefix length from the pool. If neither is specified, a CIDR is chosen
from the pool using the default prefix length for the pool.

ApiImpact
Partially-Implements: blueprint subnet-allocation
Change-Id: I59a221f4f434718fb77bd132dbbe1ff50fce4b0c

14 files changed:
neutron/api/v2/attributes.py
neutron/common/constants.py
neutron/common/exceptions.py
neutron/common/utils.py
neutron/db/db_base_plugin_v2.py
neutron/db/migration/alembic_migrations/versions/268fb5e99aa2_subnetpool_allocation.py [new file with mode: 0644]
neutron/db/migration/alembic_migrations/versions/HEAD
neutron/db/models_v2.py
neutron/extensions/subnetallocation.py [new file with mode: 0644]
neutron/ipam/subnet_alloc.py
neutron/plugins/ml2/plugin.py
neutron/tests/unit/ipam/test_subnet_alloc.py [new file with mode: 0644]
neutron/tests/unit/test_common_utils.py
neutron/tests/unit/test_db_plugin.py

index 08085c5bd7e7228f271093e11474d7696bf4a9ad..1ccbf779d63d600b6f901cd396e32d6d71e161fd 100644 (file)
@@ -778,8 +778,24 @@ RESOURCE_ATTRIBUTE_MAP = {
                        'required_by_policy': True,
                        'validate': {'type:uuid': None},
                        'is_visible': True},
-        'cidr': {'allow_post': True, 'allow_put': False,
-                 'validate': {'type:subnet': None},
+        'subnetpool_id': {'allow_post': True,
+                          'allow_put': False,
+                          'default': ATTR_NOT_SPECIFIED,
+                          'required_by_policy': False,
+                          'validate': {'type:uuid': None},
+                          'is_visible': True},
+        'prefixlen': {'allow_post': True,
+                      'allow_put': False,
+                      'validate': {'type:non_negative': None},
+                      'convert_to': convert_to_int,
+                      'default': ATTR_NOT_SPECIFIED,
+                      'required_by_policy': False,
+                      'is_visible': False},
+        'cidr': {'allow_post': True,
+                 'allow_put': False,
+                 'default': ATTR_NOT_SPECIFIED,
+                 'validate': {'type:subnet_or_none': None},
+                 'required_by_policy': False,
                  'is_visible': True},
         'gateway_ip': {'allow_post': True, 'allow_put': True,
                        'default': ATTR_NOT_SPECIFIED,
index 4839dde3e666fa7b364d6fa9e2f78fb1274b1464..662f447581e3a9cca46818e794d53457a983ba81 100644 (file)
@@ -115,6 +115,7 @@ DHCP_AGENT_SCHEDULER_EXT_ALIAS = 'dhcp_agent_scheduler'
 LBAAS_AGENT_SCHEDULER_EXT_ALIAS = 'lbaas_agent_scheduler'
 L3_DISTRIBUTED_EXT_ALIAS = 'dvr'
 L3_HA_MODE_EXT_ALIAS = 'l3-ha'
+SUBNET_ALLOCATION_EXT_ALIAS = 'subnet_allocation'
 
 # Protocol names and numbers for Security Groups/Firewalls
 PROTO_NAME_TCP = 'tcp'
index 6bd99a88dd49ef0d0adbe26a94976ff00b80d993..521f9922d6164af24985cedcb545e77905eb112f 100644 (file)
@@ -431,3 +431,21 @@ class IllegalSubnetPoolPrefixBounds(BadRequest):
 
 class IllegalSubnetPoolPrefixUpdate(BadRequest):
     message = _("Illegal update to prefixes: %(msg)s")
+
+
+class SubnetAllocationError(NeutronException):
+    message = _("Failed to allocate subnet: %(reason)s")
+
+
+class MinPrefixSubnetAllocationError(BadRequest):
+    message = _("Unable to allocate subnet with prefix length %(prefixlen)s, "
+                "minimum allowed prefix is %(min_prefixlen)s")
+
+
+class MaxPrefixSubnetAllocationError(BadRequest):
+    message = _("Unable to allocate subnet with prefix length %(prefixlen)s, "
+                "maximum allowed prefix is %(max_prefixlen)s")
+
+
+class SubnetPoolDeleteError(BadRequest):
+    message = _("Unable to delete subnet pool: %(reason)s")
index 868995809fb74bfaa8487462c324a81ba750f2b6..2502c4d719442da243851d9055628bd27d530001 100644 (file)
@@ -410,3 +410,11 @@ def is_cidr_host(cidr):
     if net.version == 4:
         return net.prefixlen == q_const.IPv4_BITS
     return net.prefixlen == q_const.IPv6_BITS
+
+
+def ip_version_from_int(ip_version_int):
+    if ip_version_int == 4:
+        return q_const.IPv4
+    if ip_version_int == 6:
+        return q_const.IPv6
+    raise ValueError(_('Illegal IP version number'))
index 841242532fe4e3c8d5f36e2d9fbf6a584a51a9da..ea3461b2a1307627ff6d48bdef336a948aa6233c 100644 (file)
@@ -15,6 +15,7 @@
 
 import netaddr
 from oslo_config import cfg
+from oslo_db import api as oslo_db_api
 from oslo_db import exception as db_exc
 from oslo_log import log as logging
 from oslo_utils import excutils
@@ -29,11 +30,13 @@ from neutron.common import exceptions as n_exc
 from neutron.common import ipv6_utils
 from neutron.common import utils
 from neutron import context as ctx
+from neutron.db import api as db_api
 from neutron.db import common_db_mixin
 from neutron.db import models_v2
 from neutron.db import sqlalchemyutils
 from neutron.extensions import l3
 from neutron.i18n import _LE, _LI
+from neutron import ipam
 from neutron.ipam import subnet_alloc
 from neutron import manager
 from neutron import neutron_plugin_base_v2
@@ -131,6 +134,10 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
         subnet_qry = context.session.query(models_v2.Subnet)
         return subnet_qry.filter_by(network_id=network_id).all()
 
+    def _get_subnets_by_subnetpool(self, context, subnetpool_id):
+        subnet_qry = context.session.query(models_v2.Subnet)
+        return subnet_qry.filter_by(subnetpool_id=subnetpool_id).all()
+
     def _get_all_subnets(self, context):
         # NOTE(salvatore-orlando): This query might end up putting
         # a lot of stress on the db. Consider adding a cache layer
@@ -845,6 +852,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
                'network_id': subnet['network_id'],
                'ip_version': subnet['ip_version'],
                'cidr': subnet['cidr'],
+               'subnetpool_id': subnet.get('subnetpool_id'),
                'allocation_pools': [{'start': pool['first_ip'],
                                      'end': pool['last_ip']}
                                     for pool in subnet['allocation_pools']],
@@ -1022,7 +1030,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
 
         ip_ver = s['ip_version']
 
-        if 'cidr' in s:
+        if attributes.is_attr_set(s.get('cidr')):
             self._validate_ip_version(ip_ver, s['cidr'], 'cidr')
 
         if attributes.is_attr_set(s.get('gateway_ip')):
@@ -1109,81 +1117,185 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
                     external_gateway_info}}
                 l3plugin.update_router(context, id, info)
 
-    def create_subnet(self, context, subnet):
-
-        net = netaddr.IPNetwork(subnet['subnet']['cidr'])
-        # turn the CIDR into a proper subnet
-        subnet['subnet']['cidr'] = '%s/%s' % (net.network, net.prefixlen)
+    def _save_subnet(self, context,
+                     network,
+                     subnet_args,
+                     dns_nameservers,
+                     host_routes,
+                     allocation_pools):
 
-        s = subnet['subnet']
+        if not attributes.is_attr_set(allocation_pools):
+            allocation_pools = self._allocate_pools_for_subnet(context,
+                                                               subnet_args)
+        else:
+            self._validate_allocation_pools(allocation_pools,
+                                            subnet_args['cidr'])
+            if subnet_args['gateway_ip']:
+                self._validate_gw_out_of_pools(subnet_args['gateway_ip'],
+                                               allocation_pools)
+
+        self._validate_subnet_cidr(context, network, subnet_args['cidr'])
+
+        subnet = models_v2.Subnet(**subnet_args)
+        context.session.add(subnet)
+        if attributes.is_attr_set(dns_nameservers):
+            for addr in dns_nameservers:
+                ns = models_v2.DNSNameServer(address=addr,
+                                             subnet_id=subnet.id)
+                context.session.add(ns)
+
+        if attributes.is_attr_set(host_routes):
+            for rt in host_routes:
+                route = models_v2.SubnetRoute(
+                    subnet_id=subnet.id,
+                    destination=rt['destination'],
+                    nexthop=rt['nexthop'])
+                context.session.add(route)
+
+        for pool in allocation_pools:
+            ip_pool = models_v2.IPAllocationPool(subnet=subnet,
+                                                 first_ip=pool['start'],
+                                                 last_ip=pool['end'])
+            context.session.add(ip_pool)
+            ip_range = models_v2.IPAvailabilityRange(
+                ipallocationpool=ip_pool,
+                first_ip=pool['start'],
+                last_ip=pool['end'])
+            context.session.add(ip_range)
 
-        if s['gateway_ip'] is attributes.ATTR_NOT_SPECIFIED:
-            s['gateway_ip'] = str(netaddr.IPAddress(net.first + 1))
+        return subnet
 
-        if s['allocation_pools'] == attributes.ATTR_NOT_SPECIFIED:
-            s['allocation_pools'] = self._allocate_pools_for_subnet(context, s)
+    def _make_subnet_args(self, context, shared, detail,
+                          subnet, subnetpool_id=None):
+        args = {'tenant_id': detail.tenant_id,
+                'id': detail.subnet_id,
+                'name': subnet['name'],
+                'network_id': subnet['network_id'],
+                'ip_version': subnet['ip_version'],
+                'cidr': str(detail.subnet.cidr),
+                'subnetpool_id': subnetpool_id,
+                'enable_dhcp': subnet['enable_dhcp'],
+                'gateway_ip': self._gateway_ip_str(subnet, detail.subnet),
+                'shared': shared}
+        if subnet['ip_version'] == 6 and subnet['enable_dhcp']:
+            if attributes.is_attr_set(subnet['ipv6_ra_mode']):
+                args['ipv6_ra_mode'] = subnet['ipv6_ra_mode']
+            if attributes.is_attr_set(subnet['ipv6_address_mode']):
+                args['ipv6_address_mode'] = subnet['ipv6_address_mode']
+        return args
+
+    def _make_subnet_request(self, tenant_id, subnet, subnetpool):
+        cidr = subnet.get('cidr')
+        subnet_id = subnet.get('id', uuidutils.generate_uuid())
+        is_any_subnetpool_request = not attributes.is_attr_set(cidr)
+
+        if is_any_subnetpool_request:
+            prefixlen = subnet['prefixlen']
+            if not attributes.is_attr_set(prefixlen):
+                prefixlen = int(subnetpool['default_prefixlen'])
+
+            return ipam.AnySubnetRequest(
+                          tenant_id,
+                          subnet_id,
+                          utils.ip_version_from_int(subnetpool['ip_version']),
+                          prefixlen)
         else:
-            self._validate_allocation_pools(s['allocation_pools'], s['cidr'])
-            if s['gateway_ip'] is not None:
-                self._validate_gw_out_of_pools(s['gateway_ip'],
-                                               s['allocation_pools'])
+            return ipam.SpecificSubnetRequest(tenant_id,
+                                              subnet_id,
+                                              cidr)
+
+    def _gateway_ip_str(self, subnet, cidr_net):
+        if subnet.get('gateway_ip') is attributes.ATTR_NOT_SPECIFIED:
+                return str(cidr_net.network + 1)
+        return subnet.get('gateway_ip')
+
+    @oslo_db_api.wrap_db_retry(max_retries=db_api.MAX_RETRIES,
+                               retry_on_request=True,
+                               retry_on_deadlock=True)
+    def _create_subnet_from_pool(self, context, subnet):
+        s = subnet['subnet']
+        tenant_id = self._get_tenant_id_for_create(context, s)
+        has_allocpool = attributes.is_attr_set(s['allocation_pools'])
+        is_any_subnetpool_request = not attributes.is_attr_set(s['cidr'])
+        if is_any_subnetpool_request and has_allocpool:
+            reason = _("allocation_pools allowed only "
+                       "for specific subnet requests.")
+            raise n_exc.BadRequest(resource='subnets', msg=reason)
 
-        self._validate_subnet(context, s)
+        with context.session.begin(subtransactions=True):
+            subnetpool = self._get_subnetpool(context, s['subnetpool_id'])
+            network = self._get_network(context, s["network_id"])
+            allocator = subnet_alloc.SubnetAllocator(subnetpool)
+            req = self._make_subnet_request(tenant_id, s, subnetpool)
+
+            ipam_subnet = allocator.allocate_subnet(context.session, req)
+            detail = ipam_subnet.get_details()
+            subnet = self._save_subnet(context,
+                                       network,
+                                       self._make_subnet_args(
+                                              context,
+                                              network.shared,
+                                              detail,
+                                              s,
+                                              subnetpool_id=subnetpool['id']),
+                                       s['dns_nameservers'],
+                                       s['host_routes'],
+                                       s['allocation_pools'])
+        if network.external:
+            self._update_router_gw_ports(context,
+                                         subnet['id'],
+                                         subnet['network_id'])
+        return self._make_subnet_dict(subnet)
 
+    def _create_subnet_from_implicit_pool(self, context, subnet):
+        s = subnet['subnet']
+        self._validate_subnet(context, s)
         tenant_id = self._get_tenant_id_for_create(context, s)
-        subnet_id = s.get('id') or uuidutils.generate_uuid()
+        id = s.get('id', uuidutils.generate_uuid())
+        detail = ipam.SpecificSubnetRequest(tenant_id,
+                                            id,
+                                            s['cidr'])
         with context.session.begin(subtransactions=True):
             network = self._get_network(context, s["network_id"])
             self._validate_subnet_cidr(context, network, s['cidr'])
-            # The 'shared' attribute for subnets is for internal plugin
-            # use only. It is not exposed through the API
-            args = {'tenant_id': tenant_id,
-                    'id': subnet_id,
-                    'name': s['name'],
-                    'network_id': s['network_id'],
-                    'ip_version': s['ip_version'],
-                    'cidr': s['cidr'],
-                    'enable_dhcp': s['enable_dhcp'],
-                    'gateway_ip': s['gateway_ip'],
-                    'shared': network.shared}
-            if s['ip_version'] == 6 and s['enable_dhcp']:
-                if attributes.is_attr_set(s['ipv6_ra_mode']):
-                    args['ipv6_ra_mode'] = s['ipv6_ra_mode']
-                if attributes.is_attr_set(s['ipv6_address_mode']):
-                    args['ipv6_address_mode'] = s['ipv6_address_mode']
-            subnet = models_v2.Subnet(**args)
-
-            context.session.add(subnet)
-            if s['dns_nameservers'] is not attributes.ATTR_NOT_SPECIFIED:
-                for addr in s['dns_nameservers']:
-                    ns = models_v2.DNSNameServer(address=addr,
-                                                 subnet_id=subnet.id)
-                    context.session.add(ns)
-
-            if s['host_routes'] is not attributes.ATTR_NOT_SPECIFIED:
-                for rt in s['host_routes']:
-                    route = models_v2.SubnetRoute(
-                        subnet_id=subnet.id,
-                        destination=rt['destination'],
-                        nexthop=rt['nexthop'])
-                    context.session.add(route)
-
-            for pool in s['allocation_pools']:
-                ip_pool = models_v2.IPAllocationPool(subnet=subnet,
-                                                     first_ip=pool['start'],
-                                                     last_ip=pool['end'])
-                context.session.add(ip_pool)
-                ip_range = models_v2.IPAvailabilityRange(
-                    ipallocationpool=ip_pool,
-                    first_ip=pool['start'],
-                    last_ip=pool['end'])
-                context.session.add(ip_range)
-
+            subnet = self._save_subnet(context,
+                                       network,
+                                       self._make_subnet_args(context,
+                                                              network.shared,
+                                                              detail,
+                                                              s),
+                                       s['dns_nameservers'],
+                                       s['host_routes'],
+                                       s['allocation_pools'])
         if network.external:
-            self._update_router_gw_ports(context, subnet_id, s['network_id'])
-
+            self._update_router_gw_ports(context,
+                                         subnet['id'],
+                                         subnet['network_id'])
         return self._make_subnet_dict(subnet)
 
+    def create_subnet(self, context, subnet):
+
+        s = subnet['subnet']
+        cidr = s.get('cidr', attributes.ATTR_NOT_SPECIFIED)
+        prefixlen = s.get('prefixlen', attributes.ATTR_NOT_SPECIFIED)
+        has_cidr = attributes.is_attr_set(cidr)
+        has_prefixlen = attributes.is_attr_set(prefixlen)
+
+        if has_cidr and has_prefixlen:
+            msg = _('cidr and prefixlen must not be supplied together')
+            raise n_exc.BadRequest(resource='subnets', msg=msg)
+
+        if has_cidr:
+            # turn the CIDR into a proper subnet
+            net = netaddr.IPNetwork(s['cidr'])
+            subnet['subnet']['cidr'] = '%s/%s' % (net.network, net.prefixlen)
+
+        subnetpool_id = s.get('subnetpool_id', attributes.ATTR_NOT_SPECIFIED)
+        if not attributes.is_attr_set(subnetpool_id):
+            # Create subnet from the implicit(AKA null) pool
+            return self._create_subnet_from_implicit_pool(context, subnet)
+        return self._create_subnet_from_pool(context, subnet)
+
     def _update_subnet_dns_nameservers(self, context, id, s):
         old_dns_list = self._get_dns_by_subnet(context, id)
         new_dns_addr_set = set(s["dns_nameservers"])
@@ -1387,6 +1499,7 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
 
     def create_subnetpool(self, context, subnetpool):
         """Create a subnetpool"""
+
         sp = subnetpool['subnetpool']
         sp_reader = subnet_alloc.SubnetPoolReader(sp)
         tenant_id = self._get_tenant_id_for_create(context, sp)
@@ -1490,6 +1603,10 @@ class NeutronDbPluginV2(neutron_plugin_base_v2.NeutronPluginBaseV2,
         """Delete a subnetpool."""
         with context.session.begin(subtransactions=True):
             subnetpool = self._get_subnetpool(context, id)
+            subnets = self._get_subnets_by_subnetpool(context, id)
+            if subnets:
+                reason = _("Subnet pool has existing allocations")
+                raise n_exc.SubnetPoolDeleteError(reason=reason)
             context.session.delete(subnetpool)
 
     def _check_mac_addr_update(self, context, port, new_mac, device_owner):
diff --git a/neutron/db/migration/alembic_migrations/versions/268fb5e99aa2_subnetpool_allocation.py b/neutron/db/migration/alembic_migrations/versions/268fb5e99aa2_subnetpool_allocation.py
new file mode 100644 (file)
index 0000000..681429d
--- /dev/null
@@ -0,0 +1,33 @@
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
+# All rights reserved.
+#
+#    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.
+#
+
+"""Initial operations in support of subnet allocation from a pool
+
+"""
+
+revision = '268fb5e99aa2'
+down_revision = '034883111f'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+    op.add_column('subnets',
+                  sa.Column('subnetpool_id',
+                            sa.String(length=36),
+                            nullable=True,
+                            index=True))
index 1761684630713717614672cd7992d6c9fea4b9db..bcef6fb15d8086118c62eefe7c1c6a3dcec42dc3 100644 (file)
@@ -1 +1 @@
-034883111f
+268fb5e99aa2
index ee0ddb2ea15412a638922bad7eb1e54f83c074f2..5bf2541f3b9fc9b705c212b804918743c95be4b4 100644 (file)
@@ -184,6 +184,7 @@ class Subnet(model_base.BASEV2, HasId, HasTenant):
 
     name = sa.Column(sa.String(attr.NAME_MAX_LEN))
     network_id = sa.Column(sa.String(36), sa.ForeignKey('networks.id'))
+    subnetpool_id = sa.Column(sa.String(36), index=True)
     ip_version = sa.Column(sa.Integer, nullable=False)
     cidr = sa.Column(sa.String(64), nullable=False)
     gateway_ip = sa.Column(sa.String(64))
diff --git a/neutron/extensions/subnetallocation.py b/neutron/extensions/subnetallocation.py
new file mode 100644 (file)
index 0000000..fd8035c
--- /dev/null
@@ -0,0 +1,53 @@
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
+# All rights reserved.
+#
+#    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.api import extensions
+from neutron.common import constants
+
+
+class Subnetallocation(extensions.ExtensionDescriptor):
+    """Extension class supporting subnet allocation."""
+
+    @classmethod
+    def get_name(cls):
+        return "Subnet Allocation"
+
+    @classmethod
+    def get_alias(cls):
+        return constants.SUBNET_ALLOCATION_EXT_ALIAS
+
+    @classmethod
+    def get_description(cls):
+        return "Enables allocation of subnets from a subnet pool"
+
+    @classmethod
+    def get_namespace(cls):
+        return ("http://docs.openstack.org/ext/"
+                "%s/api/v1.0" % constants.SUBNET_ALLOCATION_EXT_ALIAS)
+
+    @classmethod
+    def get_updated(cls):
+        return "2015-03-30T10:00:00-00:00"
+
+    def get_required_extensions(self):
+        return ["router"]
+
+    @classmethod
+    def get_resources(cls):
+        """Returns Ext Resources."""
+        return []
+
+    def get_extended_resources(self, version):
+        return {}
index 876727dd4342fd8dc2e8c4d4841b46481f2d8c53..b366e241d7fd825f03e41379141a858804e0684e 100644 (file)
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import operator
+
 import netaddr
 from neutron.api.v2 import attributes
 from neutron.common import constants
 from neutron.common import exceptions as n_exc
+from neutron.db import models_v2
+import neutron.ipam as ipam
+from neutron.ipam import driver
 from neutron.openstack.common import uuidutils
 
 
+class SubnetAllocator(driver.Pool):
+    """Class for handling allocation of subnet prefixes from a subnet pool.
+
+       This class leverages the pluggable IPAM interface where possible to
+       make merging into IPAM framework easier in future cycles.
+    """
+
+    def __init__(self, subnetpool):
+        self._subnetpool = subnetpool
+
+    def _get_allocated_cidrs(self, session):
+        query = session.query(
+            models_v2.Subnet).with_lockmode('update')
+        subnets = query.filter_by(subnetpool_id=self._subnetpool['id'])
+        return (x.cidr for x in subnets)
+
+    def _get_available_prefix_list(self, session):
+        prefixes = (x.cidr for x in self._subnetpool['prefixes'])
+        allocations = self._get_allocated_cidrs(session)
+        prefix_set = netaddr.IPSet(iterable=prefixes)
+        allocation_set = netaddr.IPSet(iterable=allocations)
+        available_set = prefix_set.difference(allocation_set)
+        available_set.compact()
+        return sorted(available_set.iter_cidrs(),
+                      key=operator.attrgetter('prefixlen'),
+                      reverse=True)
+
+    def _allocate_any_subnet(self, session, request):
+        with session.begin(subtransactions=True):
+            prefix_pool = self._get_available_prefix_list(session)
+            for prefix in prefix_pool:
+                if request.prefixlen >= prefix.prefixlen:
+                    subnet = prefix.subnet(request.prefixlen).next()
+                    gateway_ip = request.gateway_ip
+                    if not gateway_ip:
+                        gateway_ip = subnet.network + 1
+
+                    return IpamSubnet(request.tenant_id,
+                                      request.subnet_id,
+                                      subnet.cidr,
+                                      gateway_ip=gateway_ip,
+                                      allocation_pools=None)
+            msg = _("Insufficient prefix space to allocate subnet size /%s")
+            raise n_exc.SubnetAllocationError(reason=msg %
+                                              str(request.prefixlen))
+
+    def _allocate_specific_subnet(self, session, request):
+        with session.begin(subtransactions=True):
+            subnet = request.subnet
+            available = self._get_available_prefix_list(session)
+            matched = netaddr.all_matching_cidrs(subnet, available)
+            if len(matched) is 1 and matched[0].prefixlen <= subnet.prefixlen:
+                return IpamSubnet(request.tenant_id,
+                                  request.subnet_id,
+                                  subnet.cidr,
+                                  gateway_ip=request.gateway_ip,
+                                  allocation_pools=request.allocation_pools)
+            msg = _("Cannot allocate requested subnet from the available "
+                    "set of prefixes")
+            raise n_exc.SubnetAllocationError(reason=msg)
+
+    def allocate_subnet(self, session, request):
+        max_prefixlen = int(self._subnetpool['max_prefixlen'])
+        min_prefixlen = int(self._subnetpool['min_prefixlen'])
+        if request.prefixlen > max_prefixlen:
+            raise n_exc.MaxPrefixSubnetAllocationError(
+                              prefixlen=request.prefixlen,
+                              max_prefixlen=max_prefixlen)
+        if request.prefixlen < min_prefixlen:
+            raise n_exc.MinPrefixSubnetAllocationError(
+                              prefixlen=request.prefixlen,
+                              min_prefixlen=min_prefixlen)
+
+        if isinstance(request, ipam.AnySubnetRequest):
+            return self._allocate_any_subnet(session, request)
+        elif isinstance(request, ipam.SpecificSubnetRequest):
+            return self._allocate_specific_subnet(session, request)
+        else:
+            msg = _("Unsupported request type")
+            raise n_exc.SubnetAllocationError(reason=msg)
+
+    def get_subnet(self, subnet, subnet_id):
+        raise NotImplementedError()
+
+    def update_subnet(self, request):
+        raise NotImplementedError()
+
+    def remove_subnet(self, subnet, subnet_id):
+        raise NotImplementedError()
+
+
+class IpamSubnet(driver.Subnet):
+
+    def __init__(self,
+                 tenant_id,
+                 subnet_id,
+                 cidr,
+                 gateway_ip=None,
+                 allocation_pools=None):
+        self._req = ipam.SpecificSubnetRequest(tenant_id,
+                                               subnet_id,
+                                               cidr,
+                                               gateway_ip=gateway_ip,
+                                               allocation_pools=None)
+
+    def allocate(self, address_request):
+        raise NotImplementedError()
+
+    def deallocate(self, address):
+        raise NotImplementedError()
+
+    def get_details(self):
+        return self._req
+
+
 class SubnetPoolReader(object):
     '''Class to assist with reading a subnetpool, loading defaults, and
        inferring IP version from prefix list. Provides a common way of
index 0f42741f83fbc874082b0460a220756563fb0ab8..01089ca8f9ad481187528e4adf1cbdfb1a3a2afd 100644 (file)
@@ -112,7 +112,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
                                     "quotas", "security-group", "agent",
                                     "dhcp_agent_scheduler",
                                     "multi-provider", "allowed-address-pairs",
-                                    "extra_dhcp_opt"]
+                                    "extra_dhcp_opt", "subnet_allocation"]
 
     @property
     def supported_extension_aliases(self):
diff --git a/neutron/tests/unit/ipam/test_subnet_alloc.py b/neutron/tests/unit/ipam/test_subnet_alloc.py
new file mode 100644 (file)
index 0000000..256d3b9
--- /dev/null
@@ -0,0 +1,144 @@
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
+# All rights reserved.
+#
+#    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.
+
+import netaddr
+from oslo_config import cfg
+
+from neutron.api.v2 import attributes
+from neutron.common import constants
+from neutron.common import exceptions as n_exc
+from neutron import context
+import neutron.ipam as ipam
+from neutron.ipam import subnet_alloc
+from neutron import manager
+from neutron.openstack.common import uuidutils
+from neutron.tests.unit import test_db_plugin
+from neutron.tests.unit import testlib_api
+
+
+class TestSubnetAllocation(testlib_api.SqlTestCase):
+
+    def setUp(self):
+        super(TestSubnetAllocation, self).setUp()
+        self._tenant_id = 'test-tenant'
+        self.setup_coreplugin(test_db_plugin.DB_PLUGIN_KLASS)
+        self.plugin = manager.NeutronManager.get_plugin()
+        self.ctx = context.get_admin_context()
+        cfg.CONF.set_override('allow_overlapping_ips', True)
+
+    def _create_subnet_pool(self, plugin, ctx, name, prefix_list,
+                            min_prefixlen, ip_version,
+                            max_prefixlen=attributes.ATTR_NOT_SPECIFIED,
+                            default_prefixlen=attributes.ATTR_NOT_SPECIFIED,
+                            shared=False):
+        subnetpool = {'subnetpool': {'name': name,
+                                     'tenant_id': self._tenant_id,
+                                     'prefixes': prefix_list,
+                                     'min_prefixlen': min_prefixlen,
+                                     'max_prefixlen': max_prefixlen,
+                                     'default_prefixlen': default_prefixlen,
+                                     'shared': shared}}
+        return plugin.create_subnetpool(ctx, subnetpool)
+
+    def _get_subnetpool(self, ctx, plugin, id):
+        return plugin.get_subnetpool(ctx, id)
+
+    def test_allocate_any_subnet(self):
+        prefix_list = ['10.1.0.0/16', '192.168.1.0/24']
+        sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp',
+                                      prefix_list, 21, 4)
+        sp = self.plugin._get_subnetpool(self.ctx, sp['id'])
+        with self.ctx.session.begin(subtransactions=True):
+            sa = subnet_alloc.SubnetAllocator(sp)
+            req = ipam.AnySubnetRequest(self._tenant_id,
+                                        uuidutils.generate_uuid(),
+                                        constants.IPv4, 21)
+            res = sa.allocate_subnet(self.ctx.session, req)
+            detail = res.get_details()
+            prefix_set = netaddr.IPSet(iterable=prefix_list)
+            allocated_set = netaddr.IPSet(iterable=[detail.subnet.cidr])
+            self.assertTrue(allocated_set.issubset(prefix_set))
+            self.assertEqual(detail.prefixlen, 21)
+
+    def test_allocate_specific_subnet(self):
+        sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp',
+                                      ['10.1.0.0/16', '192.168.1.0/24'],
+                                      21, 4)
+        with self.ctx.session.begin(subtransactions=True):
+            sp = self.plugin._get_subnetpool(self.ctx, sp['id'])
+            sa = subnet_alloc.SubnetAllocator(sp)
+            req = ipam.SpecificSubnetRequest(self._tenant_id,
+                                             uuidutils.generate_uuid(),
+                                             '10.1.2.0/24')
+            res = sa.allocate_subnet(self.ctx.session, req)
+            detail = res.get_details()
+            sp = self._get_subnetpool(self.ctx, self.plugin, sp['id'])
+            self.assertEqual(str(detail.subnet.cidr), '10.1.2.0/24')
+            self.assertEqual(detail.prefixlen, 24)
+
+    def test_insufficient_prefix_space_for_any_allocation(self):
+        sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp',
+                                      ['10.1.1.0/24', '192.168.1.0/24'],
+                                      21, 4)
+        sp = self.plugin._get_subnetpool(self.ctx, sp['id'])
+        sa = subnet_alloc.SubnetAllocator(sp)
+        req = ipam.AnySubnetRequest(self._tenant_id,
+                                    uuidutils.generate_uuid(),
+                                    constants.IPv4,
+                                    21)
+        self.assertRaises(n_exc.SubnetAllocationError,
+                          sa.allocate_subnet, self.ctx.session, req)
+
+    def test_insufficient_prefix_space_for_specific_allocation(self):
+        sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp',
+                                      ['10.1.0.0/24'],
+                                      21, 4)
+        sp = self.plugin._get_subnetpool(self.ctx, sp['id'])
+        sa = subnet_alloc.SubnetAllocator(sp)
+        req = ipam.SpecificSubnetRequest(self._tenant_id,
+                                         uuidutils.generate_uuid(),
+                                         '10.1.0.0/21')
+        self.assertRaises(n_exc.SubnetAllocationError,
+                          sa.allocate_subnet, self.ctx.session, req)
+
+    def test_allocate_any_subnet_gateway(self):
+        sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp',
+                                      ['10.1.0.0/16', '192.168.1.0/24'],
+                                      21, 4)
+        sp = self.plugin._get_subnetpool(self.ctx, sp['id'])
+        with self.ctx.session.begin(subtransactions=True):
+            sa = subnet_alloc.SubnetAllocator(sp)
+            req = ipam.AnySubnetRequest(self._tenant_id,
+                                        uuidutils.generate_uuid(),
+                                        constants.IPv4, 21)
+            res = sa.allocate_subnet(self.ctx.session, req)
+            detail = res.get_details()
+            self.assertEqual(detail.gateway_ip, detail.subnet.network + 1)
+
+    def test_allocate_specific_subnet_specific_gateway(self):
+        sp = self._create_subnet_pool(self.plugin, self.ctx, 'test-sp',
+                                      ['10.1.0.0/16', '192.168.1.0/24'],
+                                      21, 4)
+        sp = self.plugin._get_subnetpool(self.ctx, sp['id'])
+        with self.ctx.session.begin(subtransactions=True):
+            sa = subnet_alloc.SubnetAllocator(sp)
+            req = ipam.SpecificSubnetRequest(self._tenant_id,
+                                             uuidutils.generate_uuid(),
+                                             '10.1.2.0/24',
+                                             gateway_ip='10.1.2.254')
+            res = sa.allocate_subnet(self.ctx.session, req)
+            detail = res.get_details()
+            self.assertEqual(detail.gateway_ip,
+                             netaddr.IPAddress('10.1.2.254'))
index dd16a2d972888566819062f5aa90c5da7361381b..7a370f13bbec7f492cc8b13317a89bb8f614b61f 100644 (file)
@@ -617,3 +617,18 @@ class TestCidrIsHost(base.BaseTestCase):
         self.assertRaises(ValueError,
                           utils.is_cidr_host,
                           ip_address)
+
+
+class TestIpVersionFromInt(base.BaseTestCase):
+    def test_ip_version_from_int_ipv4(self):
+        self.assertEqual(utils.ip_version_from_int(4),
+                         constants.IPv4)
+
+    def test_ip_version_from_int_ipv6(self):
+        self.assertEqual(utils.ip_version_from_int(6),
+                         constants.IPv6)
+
+    def test_ip_version_from_int_illegal_int(self):
+        self.assertRaises(ValueError,
+                          utils.ip_version_from_int,
+                          8)
index 1bdcc622a19168d2334d39dffcbfc3f03e20ada6..077f4e90fb3dc25f741fe2453a28b62db83127af 100644 (file)
@@ -86,6 +86,7 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
 
         super(NeutronDbPluginV2TestCase, self).setUp()
         cfg.CONF.set_override('notify_nova_on_port_status_changes', False)
+        cfg.CONF.set_override('allow_overlapping_ips', True)
         # Make sure at each test according extensions for the plugin is loaded
         extensions.PluginAwareExtensionManager._instance = None
         # Save the attributes map in case the plugin will alter it
@@ -4814,6 +4815,258 @@ class TestSubnetPoolsV2(NeutronDbPluginV2TestCase):
         res = req.get_response(self.api)
         self.assertEqual(res.status_int, 400)
 
+    def test_allocate_any_subnet_with_prefixlen(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request a subnet allocation (no CIDR)
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'prefixlen': 24,
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = self.deserialize(self.fmt, req.get_response(self.api))
+
+            subnet = netaddr.IPNetwork(res['subnet']['cidr'])
+            self.assertEqual(subnet.prefixlen, 24)
+            # Assert the allocated subnet CIDR is a subnet of our pool prefix
+            supernet = netaddr.smallest_matching_cidr(
+                                                 subnet,
+                                                 sp['subnetpool']['prefixes'])
+            self.assertEqual(supernet, netaddr.IPNetwork('10.10.0.0/16'))
+
+    def test_allocate_any_subnet_with_default_prefixlen(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request any subnet allocation using default prefix
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = self.deserialize(self.fmt, req.get_response(self.api))
+
+            subnet = netaddr.IPNetwork(res['subnet']['cidr'])
+            self.assertEqual(subnet.prefixlen,
+                             int(sp['subnetpool']['default_prefixlen']))
+
+    def test_allocate_specific_subnet_with_mismatch_prefixlen(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.1.0/24',
+                               'prefixlen': 26,
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 400)
+
+    def test_allocate_specific_subnet_with_matching_prefixlen(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.1.0/24',
+                               'prefixlen': 24,
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 400)
+
+    def test_allocate_specific_subnet(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request a specific subnet allocation
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.1.0/24',
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = self.deserialize(self.fmt, req.get_response(self.api))
+
+            # Assert the allocated subnet CIDR is what we expect
+            subnet = netaddr.IPNetwork(res['subnet']['cidr'])
+            self.assertEqual(subnet, netaddr.IPNetwork('10.10.1.0/24'))
+
+    def test_allocate_specific_subnet_non_existent_prefix(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request a specific subnet allocation
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '192.168.1.0/24',
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 500)
+
+    def test_allocate_specific_subnet_already_allocated(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.10.0/24'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request a specific subnet allocation
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.10.0/24',
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            # Allocate the subnet
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 201)
+            # Attempt to allocate it again
+            res = req.get_response(self.api)
+            # Assert error
+            self.assertEqual(res.status_int, 500)
+
+    def test_allocate_specific_subnet_prefix_too_small(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request a specific subnet allocation
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.0.0/20',
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 400)
+
+    def test_allocate_specific_subnet_prefix_specific_gw(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request a specific subnet allocation
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.1.0/24',
+                               'gateway_ip': '10.10.1.254',
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = self.deserialize(self.fmt, req.get_response(self.api))
+            self.assertEqual(res['subnet']['gateway_ip'], '10.10.1.254')
+
+    def test_allocate_specific_subnet_prefix_allocation_pools(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request a specific subnet allocation
+            pools = [{'start': '10.10.1.2',
+                     'end': '10.10.1.253'}]
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.1.0/24',
+                               'gateway_ip': '10.10.1.1',
+                               'ip_version': 4,
+                               'allocation_pools': pools,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = self.deserialize(self.fmt, req.get_response(self.api))
+            self.assertEqual(res['subnet']['allocation_pools'][0]['start'],
+                             pools[0]['start'])
+            self.assertEqual(res['subnet']['allocation_pools'][0]['end'],
+                             pools[0]['end'])
+
+    def test_allocate_any_subnet_prefix_allocation_pools(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.10.0/24'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            # Request an any subnet allocation
+            pools = [{'start': '10.10.10.1',
+                     'end': '10.10.10.254'}]
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'prefixlen': '24',
+                               'ip_version': 4,
+                               'allocation_pools': pools,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 400)
+
+    def test_allocate_specific_subnet_prefix_too_large(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21',
+                                              max_prefixlen='21')
+
+            # Request a specific subnet allocation
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.0.0/24',
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 400)
+
+    def test_delete_subnetpool_existing_allocations(self):
+        with self.network() as network:
+            sp = self._test_create_subnetpool(['10.10.0.0/16'],
+                                              tenant_id=self._tenant_id,
+                                              name=self._POOL_NAME,
+                                              min_prefixlen='21')
+
+            data = {'subnet': {'network_id': network['network']['id'],
+                               'subnetpool_id': sp['subnetpool']['id'],
+                               'cidr': '10.10.0.0/24',
+                               'ip_version': 4,
+                               'tenant_id': network['network']['tenant_id']}}
+            req = self.new_create_request('subnets', data)
+            req.get_response(self.api)
+            req = self.new_delete_request('subnetpools',
+                                          sp['subnetpool']['id'])
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_int, 400)
+
 
 class DbModelTestCase(base.BaseTestCase):
     """DB model tests."""