diff --git a/neutron/common/constants.py b/neutron/common/constants.py
index ced7d93161c..fec9713ce39 100644
--- a/neutron/common/constants.py
+++ b/neutron/common/constants.py
@@ -45,6 +45,9 @@ DEVICE_OWNER_LOADBALANCERV2 = "neutron:LOADBALANCERV2"
 # DEVICE_OWNER_ROUTER_HA_INTF is a special case and so is not included.
 ROUTER_INTERFACE_OWNERS = (DEVICE_OWNER_ROUTER_INTF,
                            DEVICE_OWNER_DVR_INTERFACE)
+ROUTER_INTERFACE_OWNERS_SNAT = (DEVICE_OWNER_ROUTER_INTF,
+                                DEVICE_OWNER_DVR_INTERFACE,
+                                DEVICE_OWNER_ROUTER_SNAT)
 L3_AGENT_MODE_DVR = 'dvr'
 L3_AGENT_MODE_DVR_SNAT = 'dvr_snat'
 L3_AGENT_MODE_LEGACY = 'legacy'
diff --git a/neutron/db/db_base_plugin_v2.py b/neutron/db/db_base_plugin_v2.py
index 11996a33897..b0d23d26199 100644
--- a/neutron/db/db_base_plugin_v2.py
+++ b/neutron/db/db_base_plugin_v2.py
@@ -36,6 +36,7 @@ from neutron import context as ctx
 from neutron.db import api as db_api
 from neutron.db import db_base_plugin_common
 from neutron.db import ipam_non_pluggable_backend
+from neutron.db import ipam_pluggable_backend
 from neutron.db import models_v2
 from neutron.db import rbac_db_models as rbac_db
 from neutron.db import sqlalchemyutils
@@ -101,7 +102,10 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
                          self.nova_notifier.record_port_status_changed)
 
     def set_ipam_backend(self):
-        self.ipam = ipam_non_pluggable_backend.IpamNonPluggableBackend()
+        if cfg.CONF.ipam_driver:
+            self.ipam = ipam_pluggable_backend.IpamPluggableBackend()
+        else:
+            self.ipam = ipam_non_pluggable_backend.IpamNonPluggableBackend()
 
     def _validate_host_route(self, route, ip_version):
         try:
@@ -470,10 +474,10 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
 
         with context.session.begin(subtransactions=True):
             network = self._get_network(context, s["network_id"])
-            subnet = self.ipam.allocate_subnet(context,
-                                               network,
-                                               s,
-                                               subnetpool_id)
+            subnet, ipam_subnet = self.ipam.allocate_subnet(context,
+                                                            network,
+                                                            s,
+                                                            subnetpool_id)
         if hasattr(network, 'external') and network.external:
             self._update_router_gw_ports(context,
                                          network,
@@ -481,7 +485,8 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
         # If this subnet supports auto-addressing, then update any
         # internal ports on the network with addresses for this subnet.
         if ipv6_utils.is_auto_address_subnet(subnet):
-            self.ipam.add_auto_addrs_on_network_ports(context, subnet)
+            self.ipam.add_auto_addrs_on_network_ports(context, subnet,
+                                                      ipam_subnet)
         return self._make_subnet_dict(subnet, context=context)
 
     def _get_subnetpool_id(self, subnet):
@@ -561,21 +566,24 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
         s['ip_version'] = db_subnet.ip_version
         s['cidr'] = db_subnet.cidr
         s['id'] = db_subnet.id
+        s['tenant_id'] = db_subnet.tenant_id
         self._validate_subnet(context, s, cur_subnet=db_subnet)
+        db_pools = [netaddr.IPRange(p['first_ip'], p['last_ip'])
+                    for p in db_subnet.allocation_pools]
+
+        range_pools = None
+        if s.get('allocation_pools') is not None:
+            # Convert allocation pools to IPRange to simplify future checks
+            range_pools = self.ipam.pools_to_ip_range(s['allocation_pools'])
+            s['allocation_pools'] = range_pools
 
         if s.get('gateway_ip') is not None:
-            if s.get('allocation_pools') is not None:
-                allocation_pools = [{'start': p['start'], 'end': p['end']}
-                                    for p in s['allocation_pools']]
-            else:
-                allocation_pools = [{'start': p['first_ip'],
-                                     'end': p['last_ip']}
-                                    for p in db_subnet.allocation_pools]
-            self.ipam.validate_gw_out_of_pools(s["gateway_ip"],
-                                               allocation_pools)
+            pools = range_pools if range_pools is not None else db_pools
+            self.ipam.validate_gw_out_of_pools(s["gateway_ip"], pools)
 
         with context.session.begin(subtransactions=True):
-            subnet, changes = self.ipam.update_db_subnet(context, id, s)
+            subnet, changes = self.ipam.update_db_subnet(context, id, s,
+                                                         db_pools)
         result = self._make_subnet_dict(subnet, context=context)
         # Keep up with fields that changed
         result.update(changes)
@@ -654,6 +662,9 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
                     raise n_exc.SubnetInUse(subnet_id=id)
 
             context.session.delete(subnet)
+            # Delete related ipam subnet manually,
+            # since there is no FK relationship
+            self.ipam.delete_subnet(context, id)
 
     def get_subnet(self, context, id, fields=None):
         subnet = self._get_subnet(context, id)
diff --git a/neutron/db/ipam_backend_mixin.py b/neutron/db/ipam_backend_mixin.py
index d4de20937af..ca69f460b78 100644
--- a/neutron/db/ipam_backend_mixin.py
+++ b/neutron/db/ipam_backend_mixin.py
@@ -52,6 +52,24 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
             return str(netaddr.IPNetwork(cidr_net).network + 1)
         return subnet.get('gateway_ip')
 
+    @staticmethod
+    def pools_to_ip_range(ip_pools):
+        ip_range_pools = []
+        for ip_pool in ip_pools:
+            try:
+                ip_range_pools.append(netaddr.IPRange(ip_pool['start'],
+                                                      ip_pool['end']))
+            except netaddr.AddrFormatError:
+                LOG.info(_LI("Found invalid IP address in pool: "
+                             "%(start)s - %(end)s:"),
+                         {'start': ip_pool['start'],
+                          'end': ip_pool['end']})
+                raise n_exc.InvalidAllocationPool(pool=ip_pool)
+        return ip_range_pools
+
+    def delete_subnet(self, context, subnet_id):
+        pass
+
     def validate_pools_with_subnetpool(self, subnet):
         """Verifies that allocation pools are set correctly
 
@@ -140,22 +158,23 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
     def _update_subnet_allocation_pools(self, context, subnet_id, s):
         context.session.query(models_v2.IPAllocationPool).filter_by(
             subnet_id=subnet_id).delete()
-        new_pools = [models_v2.IPAllocationPool(first_ip=p['start'],
-                                                last_ip=p['end'],
+        pools = ((netaddr.IPAddress(p.first, p.version).format(),
+                  netaddr.IPAddress(p.last, p.version).format())
+                 for p in s['allocation_pools'])
+        new_pools = [models_v2.IPAllocationPool(first_ip=p[0],
+                                                last_ip=p[1],
                                                 subnet_id=subnet_id)
-                     for p in s['allocation_pools']]
+                     for p in pools]
         context.session.add_all(new_pools)
         # Call static method with self to redefine in child
         # (non-pluggable backend)
         self._rebuild_availability_ranges(context, [s])
-        # Gather new pools for result:
-        result_pools = [{'start': pool['start'],
-                         'end': pool['end']}
-                        for pool in s['allocation_pools']]
+        # Gather new pools for result
+        result_pools = [{'start': p[0], 'end': p[1]} for p in pools]
         del s['allocation_pools']
         return result_pools
 
-    def update_db_subnet(self, context, subnet_id, s):
+    def update_db_subnet(self, context, subnet_id, s, oldpools):
         changes = {}
         if "dns_nameservers" in s:
             changes['dns_nameservers'] = (
@@ -239,38 +258,23 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
         LOG.debug("Performing IP validity checks on allocation pools")
         ip_sets = []
         for ip_pool in ip_pools:
-            try:
-                start_ip = netaddr.IPAddress(ip_pool['start'])
-                end_ip = netaddr.IPAddress(ip_pool['end'])
-            except netaddr.AddrFormatError:
-                LOG.info(_LI("Found invalid IP address in pool: "
-                             "%(start)s - %(end)s:"),
-                         {'start': ip_pool['start'],
-                          'end': ip_pool['end']})
-                raise n_exc.InvalidAllocationPool(pool=ip_pool)
+            start_ip = netaddr.IPAddress(ip_pool.first, ip_pool.version)
+            end_ip = netaddr.IPAddress(ip_pool.last, ip_pool.version)
             if (start_ip.version != subnet.version or
                     end_ip.version != subnet.version):
                 LOG.info(_LI("Specified IP addresses do not match "
                              "the subnet IP version"))
                 raise n_exc.InvalidAllocationPool(pool=ip_pool)
-            if end_ip < start_ip:
-                LOG.info(_LI("Start IP (%(start)s) is greater than end IP "
-                             "(%(end)s)"),
-                         {'start': ip_pool['start'], 'end': ip_pool['end']})
-                raise n_exc.InvalidAllocationPool(pool=ip_pool)
             if start_ip < subnet_first_ip or end_ip > subnet_last_ip:
                 LOG.info(_LI("Found pool larger than subnet "
                              "CIDR:%(start)s - %(end)s"),
-                         {'start': ip_pool['start'],
-                          'end': ip_pool['end']})
+                         {'start': start_ip, 'end': end_ip})
                 raise n_exc.OutOfBoundsAllocationPool(
                     pool=ip_pool,
                     subnet_cidr=subnet_cidr)
             # Valid allocation pool
             # Create an IPSet for it for easily verifying overlaps
-            ip_sets.append(netaddr.IPSet(netaddr.IPRange(
-                ip_pool['start'],
-                ip_pool['end']).cidrs()))
+            ip_sets.append(netaddr.IPSet(ip_pool.cidrs()))
 
         LOG.debug("Checking for overlaps among allocation pools "
                   "and gateway ip")
@@ -291,22 +295,54 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
                         pool_2=r_range,
                         subnet_cidr=subnet_cidr)
 
+    def _validate_max_ips_per_port(self, fixed_ip_list):
+        if len(fixed_ip_list) > cfg.CONF.max_fixed_ips_per_port:
+            msg = _('Exceeded maximim amount of fixed ips per port')
+            raise n_exc.InvalidInput(error_message=msg)
+
+    def _get_subnet_for_fixed_ip(self, context, fixed, network_id):
+        if 'subnet_id' in fixed:
+            subnet = self._get_subnet(context, fixed['subnet_id'])
+            if subnet['network_id'] != network_id:
+                msg = (_("Failed to create port on network %(network_id)s"
+                         ", because fixed_ips included invalid subnet "
+                         "%(subnet_id)s") %
+                       {'network_id': network_id,
+                        'subnet_id': fixed['subnet_id']})
+                raise n_exc.InvalidInput(error_message=msg)
+            # Ensure that the IP is valid on the subnet
+            if ('ip_address' in fixed and
+                not ipam_utils.check_subnet_ip(subnet['cidr'],
+                                               fixed['ip_address'])):
+                raise n_exc.InvalidIpForSubnet(ip_address=fixed['ip_address'])
+            return subnet
+
+        if 'ip_address' not in fixed:
+            msg = _('IP allocation requires subnet_id or ip_address')
+            raise n_exc.InvalidInput(error_message=msg)
+
+        filter = {'network_id': [network_id]}
+        subnets = self._get_subnets(context, filters=filter)
+
+        for subnet in subnets:
+            if ipam_utils.check_subnet_ip(subnet['cidr'],
+                                          fixed['ip_address']):
+                return subnet
+        raise n_exc.InvalidIpForNetwork(ip_address=fixed['ip_address'])
+
     def _prepare_allocation_pools(self, allocation_pools, cidr, gateway_ip):
         """Returns allocation pools represented as list of IPRanges"""
         if not attributes.is_attr_set(allocation_pools):
             return ipam_utils.generate_pools(cidr, gateway_ip)
 
-        self._validate_allocation_pools(allocation_pools, cidr)
+        ip_range_pools = self.pools_to_ip_range(allocation_pools)
+        self._validate_allocation_pools(ip_range_pools, cidr)
         if gateway_ip:
-            self.validate_gw_out_of_pools(gateway_ip, allocation_pools)
-        return [netaddr.IPRange(p['start'], p['end'])
-                for p in allocation_pools]
+            self.validate_gw_out_of_pools(gateway_ip, ip_range_pools)
+        return ip_range_pools
 
     def validate_gw_out_of_pools(self, gateway_ip, pools):
-        for allocation_pool in pools:
-            pool_range = netaddr.IPRange(
-                allocation_pool['start'],
-                allocation_pool['end'])
+        for pool_range in pools:
             if netaddr.IPAddress(gateway_ip) in pool_range:
                 raise n_exc.GatewayConflictWithAllocationPools(
                     pool=pool_range,
diff --git a/neutron/db/ipam_non_pluggable_backend.py b/neutron/db/ipam_non_pluggable_backend.py
index 543f0cac1df..5f5daa7ac7d 100644
--- a/neutron/db/ipam_non_pluggable_backend.py
+++ b/neutron/db/ipam_non_pluggable_backend.py
@@ -14,7 +14,6 @@
 #    under the License.
 
 import netaddr
-from oslo_config import cfg
 from oslo_db import exception as db_exc
 from oslo_log import log as logging
 from sqlalchemy import and_
@@ -29,7 +28,6 @@ from neutron.db import ipam_backend_mixin
 from neutron.db import models_v2
 from neutron.ipam import requests as ipam_req
 from neutron.ipam import subnet_alloc
-from neutron.ipam import utils as ipam_utils
 
 LOG = logging.getLogger(__name__)
 
@@ -242,49 +240,17 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
         """
         fixed_ip_set = []
         for fixed in fixed_ips:
-            found = False
-            if 'subnet_id' not in fixed:
-                if 'ip_address' not in fixed:
-                    msg = _('IP allocation requires subnet_id or ip_address')
-                    raise n_exc.InvalidInput(error_message=msg)
-
-                filter = {'network_id': [network_id]}
-                subnets = self._get_subnets(context, filters=filter)
-                for subnet in subnets:
-                    if ipam_utils.check_subnet_ip(subnet['cidr'],
-                                                  fixed['ip_address']):
-                        found = True
-                        subnet_id = subnet['id']
-                        break
-                if not found:
-                    raise n_exc.InvalidIpForNetwork(
-                        ip_address=fixed['ip_address'])
-            else:
-                subnet = self._get_subnet(context, fixed['subnet_id'])
-                if subnet['network_id'] != network_id:
-                    msg = (_("Failed to create port on network %(network_id)s"
-                             ", because fixed_ips included invalid subnet "
-                             "%(subnet_id)s") %
-                           {'network_id': network_id,
-                            'subnet_id': fixed['subnet_id']})
-                    raise n_exc.InvalidInput(error_message=msg)
-                subnet_id = subnet['id']
+            subnet = self._get_subnet_for_fixed_ip(context, fixed, network_id)
 
             is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
             if 'ip_address' in fixed:
                 # Ensure that the IP's are unique
                 if not IpamNonPluggableBackend._check_unique_ip(
                         context, network_id,
-                        subnet_id, fixed['ip_address']):
+                        subnet['id'], fixed['ip_address']):
                     raise n_exc.IpAddressInUse(net_id=network_id,
                                                ip_address=fixed['ip_address'])
 
-                # Ensure that the IP is valid on the subnet
-                if (not found and
-                    not ipam_utils.check_subnet_ip(subnet['cidr'],
-                                                   fixed['ip_address'])):
-                    raise n_exc.InvalidIpForSubnet(
-                        ip_address=fixed['ip_address'])
                 if (is_auto_addr_subnet and
                     device_owner not in
                         constants.ROUTER_INTERFACE_OWNERS):
@@ -292,23 +258,20 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
                             "assigned to a port on subnet %(id)s since the "
                             "subnet is configured for automatic addresses") %
                            {'address': fixed['ip_address'],
-                            'id': subnet_id})
+                            'id': subnet['id']})
                     raise n_exc.InvalidInput(error_message=msg)
-                fixed_ip_set.append({'subnet_id': subnet_id,
+                fixed_ip_set.append({'subnet_id': subnet['id'],
                                      'ip_address': fixed['ip_address']})
             else:
                 # A scan for auto-address subnets on the network is done
                 # separately so that all such subnets (not just those
                 # listed explicitly here by subnet ID) are associated
                 # with the port.
-                if (device_owner in constants.ROUTER_INTERFACE_OWNERS or
-                    device_owner == constants.DEVICE_OWNER_ROUTER_SNAT or
+                if (device_owner in constants.ROUTER_INTERFACE_OWNERS_SNAT or
                     not is_auto_addr_subnet):
-                    fixed_ip_set.append({'subnet_id': subnet_id})
+                    fixed_ip_set.append({'subnet_id': subnet['id']})
 
-        if len(fixed_ip_set) > cfg.CONF.max_fixed_ips_per_port:
-            msg = _('Exceeded maximim amount of fixed ips per port')
-            raise n_exc.InvalidInput(error_message=msg)
+        self._validate_max_ips_per_port(fixed_ip_set)
         return fixed_ip_set
 
     def _allocate_fixed_ips(self, context, fixed_ips, mac_address):
@@ -382,8 +345,7 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
         net_id_filter = {'network_id': [p['network_id']]}
         subnets = self._get_subnets(context, filters=net_id_filter)
         is_router_port = (
-            p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS or
-            p['device_owner'] == constants.DEVICE_OWNER_ROUTER_SNAT)
+            p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
 
         fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED
         if fixed_configured:
@@ -431,17 +393,16 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
 
         return ips
 
-    def add_auto_addrs_on_network_ports(self, context, subnet):
+    def add_auto_addrs_on_network_ports(self, context, subnet, ipam_subnet):
         """For an auto-address subnet, add addrs for ports on the net."""
         with context.session.begin(subtransactions=True):
             network_id = subnet['network_id']
             port_qry = context.session.query(models_v2.Port)
-            for port in port_qry.filter(
+            ports = port_qry.filter(
                 and_(models_v2.Port.network_id == network_id,
-                     models_v2.Port.device_owner !=
-                     constants.DEVICE_OWNER_ROUTER_SNAT,
                      ~models_v2.Port.device_owner.in_(
-                         constants.ROUTER_INTERFACE_OWNERS))):
+                         constants.ROUTER_INTERFACE_OWNERS_SNAT)))
+            for port in ports:
                 ip_address = self._calculate_ipv6_eui64_addr(
                     context, subnet, port['mac_address'])
                 allocated = models_v2.IPAllocation(network_id=network_id,
@@ -505,4 +466,6 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
                                    subnet['dns_nameservers'],
                                    subnet['host_routes'],
                                    subnet_request)
-        return subnet
+        # ipam_subnet is not expected to be allocated for non pluggable ipam,
+        # so just return None for it (second element in returned tuple)
+        return subnet, None
diff --git a/neutron/db/ipam_pluggable_backend.py b/neutron/db/ipam_pluggable_backend.py
index a93f66b4747..dd35ac0b271 100644
--- a/neutron/db/ipam_pluggable_backend.py
+++ b/neutron/db/ipam_pluggable_backend.py
@@ -13,13 +13,22 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import netaddr
+from oslo_db import exception as db_exc
 from oslo_log import log as logging
 from oslo_utils import excutils
+from sqlalchemy import and_
 
+from neutron.api.v2 import attributes
+from neutron.common import constants
 from neutron.common import exceptions as n_exc
+from neutron.common import ipv6_utils
 from neutron.db import ipam_backend_mixin
+from neutron.db import models_v2
 from neutron.i18n import _LE
+from neutron.ipam import driver
 from neutron.ipam import exceptions as ipam_exc
+from neutron.ipam import requests as ipam_req
 
 
 LOG = logging.getLogger(__name__)
@@ -110,7 +119,6 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
                 ip_address, ip_subnet = self._ipam_allocate_single_ip(
                     context, ipam_driver, port, ip_list)
                 allocated.append({'ip_address': ip_address,
-                                  'subnet_cidr': ip_subnet['subnet_cidr'],
                                   'subnet_id': ip_subnet['subnet_id']})
         except Exception:
             with excutils.save_and_reraise_exception():
@@ -127,3 +135,317 @@ class IpamPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
                                   "external system for %s"), addresses)
 
         return allocated
+
+    def _ipam_update_allocation_pools(self, context, ipam_driver, subnet):
+        self._validate_allocation_pools(subnet['allocation_pools'],
+                                        subnet['cidr'])
+
+        factory = ipam_driver.get_subnet_request_factory()
+        subnet_request = factory.get_request(context, subnet, None)
+
+        ipam_driver.update_subnet(subnet_request)
+
+    def delete_subnet(self, context, subnet_id):
+        ipam_driver = driver.Pool.get_instance(None, context)
+        ipam_driver.remove_subnet(subnet_id)
+
+    def allocate_ips_for_port_and_store(self, context, port, port_id):
+        network_id = port['port']['network_id']
+        ips = []
+        try:
+            ips = self._allocate_ips_for_port(context, port)
+            for ip in ips:
+                ip_address = ip['ip_address']
+                subnet_id = ip['subnet_id']
+                IpamPluggableBackend._store_ip_allocation(
+                    context, ip_address, network_id,
+                    subnet_id, port_id)
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                if ips:
+                    LOG.debug("An exception occurred during port creation."
+                              "Reverting IP allocation")
+                    ipam_driver = driver.Pool.get_instance(None, context)
+                    self._ipam_deallocate_ips(context, ipam_driver,
+                                              port['port'], ips,
+                                              revert_on_fail=False)
+
+    def _allocate_ips_for_port(self, context, port):
+        """Allocate IP addresses for the port. IPAM version.
+
+        If port['fixed_ips'] is set to 'ATTR_NOT_SPECIFIED', allocate IP
+        addresses for the port. If port['fixed_ips'] contains an IP address or
+        a subnet_id then allocate an IP address accordingly.
+        """
+        p = port['port']
+        ips = []
+        v6_stateless = []
+        net_id_filter = {'network_id': [p['network_id']]}
+        subnets = self._get_subnets(context, filters=net_id_filter)
+        is_router_port = (
+            p['device_owner'] in constants.ROUTER_INTERFACE_OWNERS_SNAT)
+
+        fixed_configured = p['fixed_ips'] is not attributes.ATTR_NOT_SPECIFIED
+        if fixed_configured:
+            ips = self._test_fixed_ips_for_port(context,
+                                                p["network_id"],
+                                                p['fixed_ips'],
+                                                p['device_owner'])
+            # For ports that are not router ports, implicitly include all
+            # auto-address subnets for address association.
+            if not is_router_port:
+                v6_stateless += [subnet for subnet in subnets
+                                 if ipv6_utils.is_auto_address_subnet(subnet)]
+        else:
+            # Split into v4, v6 stateless and v6 stateful subnets
+            v4 = []
+            v6_stateful = []
+            for subnet in subnets:
+                if subnet['ip_version'] == 4:
+                    v4.append(subnet)
+                else:
+                    if ipv6_utils.is_auto_address_subnet(subnet):
+                        if not is_router_port:
+                            v6_stateless.append(subnet)
+                    else:
+                        v6_stateful.append(subnet)
+
+            version_subnets = [v4, v6_stateful]
+            for subnets in version_subnets:
+                if subnets:
+                    ips.append([{'subnet_id': s['id']}
+                                for s in subnets])
+
+        for subnet in v6_stateless:
+            # IP addresses for IPv6 SLAAC and DHCPv6-stateless subnets
+            # are implicitly included.
+            ips.append({'subnet_id': subnet['id'],
+                        'subnet_cidr': subnet['cidr'],
+                        'eui64_address': True,
+                        'mac': p['mac_address']})
+        ipam_driver = driver.Pool.get_instance(None, context)
+        return self._ipam_allocate_ips(context, ipam_driver, p, ips)
+
+    def _test_fixed_ips_for_port(self, context, network_id, fixed_ips,
+                                 device_owner):
+        """Test fixed IPs for port.
+
+        Check that configured subnets are valid prior to allocating any
+        IPs. Include the subnet_id in the result if only an IP address is
+        configured.
+
+        :raises: InvalidInput, IpAddressInUse, InvalidIpForNetwork,
+                 InvalidIpForSubnet
+        """
+        fixed_ip_list = []
+        for fixed in fixed_ips:
+            subnet = self._get_subnet_for_fixed_ip(context, fixed, network_id)
+
+            is_auto_addr_subnet = ipv6_utils.is_auto_address_subnet(subnet)
+            if 'ip_address' in fixed:
+                if (is_auto_addr_subnet and device_owner not in
+                        constants.ROUTER_INTERFACE_OWNERS):
+                    msg = (_("IPv6 address %(address)s can not be directly "
+                            "assigned to a port on subnet %(id)s since the "
+                            "subnet is configured for automatic addresses") %
+                           {'address': fixed['ip_address'],
+                            'id': subnet['id']})
+                    raise n_exc.InvalidInput(error_message=msg)
+                fixed_ip_list.append({'subnet_id': subnet['id'],
+                                      'ip_address': fixed['ip_address']})
+            else:
+                # A scan for auto-address subnets on the network is done
+                # separately so that all such subnets (not just those
+                # listed explicitly here by subnet ID) are associated
+                # with the port.
+                if (device_owner in constants.ROUTER_INTERFACE_OWNERS_SNAT or
+                        not is_auto_addr_subnet):
+                    fixed_ip_list.append({'subnet_id': subnet['id']})
+
+        self._validate_max_ips_per_port(fixed_ip_list)
+        return fixed_ip_list
+
+    def _update_ips_for_port(self, context, port,
+                             original_ips, new_ips, mac):
+        """Add or remove IPs from the port. IPAM version"""
+        added = []
+        removed = []
+        changes = self._get_changed_ips_for_port(
+            context, original_ips, new_ips, port['device_owner'])
+        # Check if the IP's to add are OK
+        to_add = self._test_fixed_ips_for_port(
+            context, port['network_id'], changes.add,
+            port['device_owner'])
+
+        ipam_driver = driver.Pool.get_instance(None, context)
+        if changes.remove:
+            removed = self._ipam_deallocate_ips(context, ipam_driver, port,
+                                                changes.remove)
+        if to_add:
+            added = self._ipam_allocate_ips(context, ipam_driver,
+                                            changes, to_add)
+        return self.Changes(add=added,
+                            original=changes.original,
+                            remove=removed)
+
+    def save_allocation_pools(self, context, subnet, allocation_pools):
+        for pool in allocation_pools:
+            first_ip = str(netaddr.IPAddress(pool.first, pool.version))
+            last_ip = str(netaddr.IPAddress(pool.last, pool.version))
+            ip_pool = models_v2.IPAllocationPool(subnet=subnet,
+                                                 first_ip=first_ip,
+                                                 last_ip=last_ip)
+            context.session.add(ip_pool)
+
+    def update_port_with_ips(self, context, db_port, new_port, new_mac):
+        changes = self.Changes(add=[], original=[], remove=[])
+
+        if 'fixed_ips' in new_port:
+            original = self._make_port_dict(db_port,
+                                            process_extensions=False)
+            changes = self._update_ips_for_port(context,
+                                                db_port,
+                                                original["fixed_ips"],
+                                                new_port['fixed_ips'],
+                                                new_mac)
+        try:
+            # Check if the IPs need to be updated
+            network_id = db_port['network_id']
+            for ip in changes.add:
+                self._store_ip_allocation(
+                    context, ip['ip_address'], network_id,
+                    ip['subnet_id'], db_port.id)
+            for ip in changes.remove:
+                self._delete_ip_allocation(context, network_id,
+                                           ip['subnet_id'], ip['ip_address'])
+            self._update_db_port(context, db_port, new_port, network_id,
+                                 new_mac)
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                if 'fixed_ips' in new_port:
+                    LOG.debug("An exception occurred during port update.")
+                    ipam_driver = driver.Pool.get_instance(None, context)
+                    if changes.add:
+                        LOG.debug("Reverting IP allocation.")
+                        self._ipam_deallocate_ips(context, ipam_driver,
+                                                  db_port, changes.add,
+                                                  revert_on_fail=False)
+                    if changes.remove:
+                        LOG.debug("Reverting IP deallocation.")
+                        self._ipam_allocate_ips(context, ipam_driver,
+                                                db_port, changes.remove,
+                                                revert_on_fail=False)
+        return changes
+
+    def delete_port(self, context, id):
+        # Get fixed_ips list before port deletion
+        port = self._get_port(context, id)
+        ipam_driver = driver.Pool.get_instance(None, context)
+
+        super(IpamPluggableBackend, self).delete_port(context, id)
+        # Deallocating ips via IPAM after port is deleted locally.
+        # So no need to do rollback actions on remote server
+        # in case of fail to delete port locally
+        self._ipam_deallocate_ips(context, ipam_driver, port,
+                                  port['fixed_ips'])
+
+    def update_db_subnet(self, context, id, s, old_pools):
+        ipam_driver = driver.Pool.get_instance(None, context)
+        if "allocation_pools" in s:
+            self._ipam_update_allocation_pools(context, ipam_driver, s)
+
+        try:
+            subnet, changes = super(IpamPluggableBackend,
+                                    self).update_db_subnet(context, id,
+                                                           s, old_pools)
+        except Exception:
+            with excutils.save_and_reraise_exception():
+                if "allocation_pools" in s and old_pools:
+                    LOG.error(
+                        _LE("An exception occurred during subnet update."
+                            "Reverting allocation pool changes"))
+                    s['allocation_pools'] = old_pools
+                    self._ipam_update_allocation_pools(context, ipam_driver, s)
+        return subnet, changes
+
+    def add_auto_addrs_on_network_ports(self, context, subnet, ipam_subnet):
+        """For an auto-address subnet, add addrs for ports on the net."""
+        with context.session.begin(subtransactions=True):
+            network_id = subnet['network_id']
+            port_qry = context.session.query(models_v2.Port)
+            ports = port_qry.filter(
+                and_(models_v2.Port.network_id == network_id,
+                     ~models_v2.Port.device_owner.in_(
+                         constants.ROUTER_INTERFACE_OWNERS_SNAT)))
+            for port in ports:
+                ip_request = ipam_req.AutomaticAddressRequest(
+                    prefix=subnet['cidr'],
+                    mac=port['mac_address'])
+                ip_address = ipam_subnet.allocate(ip_request)
+                allocated = models_v2.IPAllocation(network_id=network_id,
+                                                   port_id=port['id'],
+                                                   ip_address=ip_address,
+                                                   subnet_id=subnet['id'])
+                try:
+                    # Do the insertion of each IP allocation entry within
+                    # the context of a nested transaction, so that the entry
+                    # is rolled back independently of other entries whenever
+                    # the corresponding port has been deleted.
+                    with context.session.begin_nested():
+                        context.session.add(allocated)
+                except db_exc.DBReferenceError:
+                    LOG.debug("Port %s was deleted while updating it with an "
+                              "IPv6 auto-address. Ignoring.", port['id'])
+                    LOG.debug("Reverting IP allocation for %s", ip_address)
+                    # Do not fail if reverting allocation was unsuccessful
+                    try:
+                        ipam_subnet.deallocate(ip_address)
+                    except Exception:
+                        LOG.debug("Reverting IP allocation failed for %s",
+                                  ip_address)
+
+    def allocate_subnet(self, context, network, subnet, subnetpool_id):
+        subnetpool = None
+
+        if subnetpool_id:
+            subnetpool = self._get_subnetpool(context, subnetpool_id)
+            self._validate_ip_version_with_subnetpool(subnet, subnetpool)
+
+        # gateway_ip and allocation pools should be validated or generated
+        # only for specific request
+        if subnet['cidr'] is not attributes.ATTR_NOT_SPECIFIED:
+            subnet['gateway_ip'] = self._gateway_ip_str(subnet,
+                                                        subnet['cidr'])
+            subnet['allocation_pools'] = self._prepare_allocation_pools(
+                subnet['allocation_pools'],
+                subnet['cidr'],
+                subnet['gateway_ip'])
+
+        ipam_driver = driver.Pool.get_instance(subnetpool, context)
+        subnet_factory = ipam_driver.get_subnet_request_factory()
+        subnet_request = subnet_factory.get_request(context, subnet,
+                                                    subnetpool)
+        ipam_subnet = ipam_driver.allocate_subnet(subnet_request)
+        # get updated details with actually allocated subnet
+        subnet_request = ipam_subnet.get_details()
+
+        try:
+            subnet = self._save_subnet(context,
+                                       network,
+                                       self._make_subnet_args(
+                                           subnet_request,
+                                           subnet,
+                                           subnetpool_id),
+                                       subnet['dns_nameservers'],
+                                       subnet['host_routes'],
+                                       subnet_request)
+        except Exception:
+            # Note(pbondar): Third-party ipam servers can't rely
+            # on transaction rollback, so explicit rollback call needed.
+            # IPAM part rolled back in exception handling
+            # and subnet part is rolled back by transaction rollback.
+            with excutils.save_and_reraise_exception():
+                LOG.debug("An exception occurred during subnet creation."
+                          "Reverting subnet allocation.")
+                self.delete_subnet(context, subnet_request.subnet_id)
+        return subnet, ipam_subnet
diff --git a/neutron/ipam/driver.py b/neutron/ipam/driver.py
index cd44fd09d66..3460517f6cd 100644
--- a/neutron/ipam/driver.py
+++ b/neutron/ipam/driver.py
@@ -148,14 +148,3 @@ class Subnet(object):
 
         :returns: An instance of SpecificSubnetRequest with the subnet detail.
         """
-
-    @abc.abstractmethod
-    def associate_neutron_subnet(self, subnet_id):
-        """Associate the IPAM subnet with a neutron subnet.
-
-        This operation should be performed to attach a neutron subnet to the
-        current subnet instance. In some cases IPAM subnets may be created
-        independently of neutron subnets and associated at a later stage.
-
-        :param subnet_id: neutron subnet identifier.
-        """
diff --git a/neutron/ipam/drivers/neutrondb_ipam/db_api.py b/neutron/ipam/drivers/neutrondb_ipam/db_api.py
index 188d55990b0..223fb1c3484 100644
--- a/neutron/ipam/drivers/neutrondb_ipam/db_api.py
+++ b/neutron/ipam/drivers/neutrondb_ipam/db_api.py
@@ -54,11 +54,18 @@ class IpamSubnetManager(object):
         session.add(ipam_subnet)
         return self._ipam_subnet_id
 
-    def associate_neutron_id(self, session, neutron_subnet_id):
-        session.query(db_models.IpamSubnet).filter_by(
-            id=self._ipam_subnet_id).update(
-            {'neutron_subnet_id': neutron_subnet_id})
-        self._neutron_subnet_id = neutron_subnet_id
+    @classmethod
+    def delete(cls, session, neutron_subnet_id):
+        """Delete IPAM subnet.
+
+        IPAM subnet no longer has foreign key to neutron subnet,
+        so need to perform delete manually
+
+        :param session: database sesssion
+        :param neutron_subnet_id: neutron subnet id associated with ipam subnet
+        """
+        return session.query(db_models.IpamSubnet).filter_by(
+            neutron_subnet_id=neutron_subnet_id).delete()
 
     def create_pool(self, session, pool_start, pool_end):
         """Create an allocation pool and availability ranges for the subnet.
diff --git a/neutron/ipam/drivers/neutrondb_ipam/driver.py b/neutron/ipam/drivers/neutrondb_ipam/driver.py
index 28a3eb91d38..1ddab84340f 100644
--- a/neutron/ipam/drivers/neutrondb_ipam/driver.py
+++ b/neutron/ipam/drivers/neutrondb_ipam/driver.py
@@ -56,7 +56,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
         ipam_subnet_id = uuidutils.generate_uuid()
         subnet_manager = ipam_db_api.IpamSubnetManager(
             ipam_subnet_id,
-            None)
+            subnet_request.subnet_id)
         # Create subnet resource
         session = ctx.session
         subnet_manager.create(session)
@@ -76,8 +76,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
                    allocation_pools=pools,
                    gateway_ip=subnet_request.gateway_ip,
                    tenant_id=subnet_request.tenant_id,
-                   subnet_id=subnet_request.subnet_id,
-                   subnet_id_not_set=True)
+                   subnet_id=subnet_request.subnet_id)
 
     @classmethod
     def load(cls, neutron_subnet_id, ctx):
@@ -88,7 +87,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
         ipam_subnet = ipam_db_api.IpamSubnetManager.load_by_neutron_subnet_id(
             ctx.session, neutron_subnet_id)
         if not ipam_subnet:
-            LOG.error(_LE("Unable to retrieve IPAM subnet as the referenced "
+            LOG.error(_LE("IPAM subnet referenced to "
                           "Neutron subnet %s does not exist"),
                       neutron_subnet_id)
             raise n_exc.SubnetNotFound(subnet_id=neutron_subnet_id)
@@ -113,7 +112,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
 
     def __init__(self, internal_id, ctx, cidr=None,
                  allocation_pools=None, gateway_ip=None, tenant_id=None,
-                 subnet_id=None, subnet_id_not_set=False):
+                 subnet_id=None):
         # NOTE: In theory it could have been possible to grant the IPAM
         # driver direct access to the database. While this is possible,
         # it would have led to duplicate code and/or non-trivial
@@ -124,7 +123,7 @@ class NeutronDbSubnet(ipam_base.Subnet):
         self._pools = allocation_pools
         self._gateway_ip = gateway_ip
         self._tenant_id = tenant_id
-        self._subnet_id = None if subnet_id_not_set else subnet_id
+        self._subnet_id = subnet_id
         self.subnet_manager = ipam_db_api.IpamSubnetManager(internal_id,
                                                             self._subnet_id)
         self._context = ctx
@@ -363,17 +362,6 @@ class NeutronDbSubnet(ipam_base.Subnet):
             self._tenant_id, self.subnet_manager.neutron_id,
             self._cidr, self._gateway_ip, self._pools)
 
-    def associate_neutron_subnet(self, subnet_id):
-        """Set neutron identifier for this subnet"""
-        session = self._context.session
-        if self._subnet_id:
-            raise
-        # IPAMSubnet does not have foreign key to Subnet,
-        # so need verify subnet existence.
-        NeutronDbSubnet._fetch_subnet(self._context, subnet_id)
-        self.subnet_manager.associate_neutron_id(session, subnet_id)
-        self._subnet_id = subnet_id
-
 
 class NeutronDbPool(subnet_alloc.SubnetAllocator):
     """Subnet pools backed by Neutron Database.
@@ -429,10 +417,16 @@ class NeutronDbPool(subnet_alloc.SubnetAllocator):
         subnet.update_allocation_pools(subnet_request.allocation_pools)
         return subnet
 
-    def remove_subnet(self, subnet):
+    def remove_subnet(self, subnet_id):
         """Remove data structures for a given subnet.
 
-        All the IPAM-related data are cleared when a subnet is deleted thanks
-        to cascaded foreign key relationships.
+        IPAM-related data has no foreign key relationships to neutron subnet,
+        so removing ipam subnet manually
         """
-        pass
+        count = ipam_db_api.IpamSubnetManager.delete(self._context.session,
+                                                     subnet_id)
+        if count < 1:
+            LOG.error(_LE("IPAM subnet referenced to "
+                          "Neutron subnet %s does not exist"),
+                      subnet_id)
+            raise n_exc.SubnetNotFound(subnet_id=subnet_id)
diff --git a/neutron/ipam/subnet_alloc.py b/neutron/ipam/subnet_alloc.py
index cd17d338be9..1bc213ec4ba 100644
--- a/neutron/ipam/subnet_alloc.py
+++ b/neutron/ipam/subnet_alloc.py
@@ -193,9 +193,6 @@ class IpamSubnet(driver.Subnet):
     def get_details(self):
         return self._req
 
-    def associate_neutron_subnet(self, subnet_id):
-        pass
-
 
 class SubnetPoolReader(object):
     '''Class to assist with reading a subnetpool, loading defaults, and
diff --git a/neutron/tests/functional/db/test_ipam.py b/neutron/tests/functional/db/test_ipam.py
index a10e9e288a1..a1dd8468f05 100644
--- a/neutron/tests/functional/db/test_ipam.py
+++ b/neutron/tests/functional/db/test_ipam.py
@@ -24,6 +24,7 @@ from neutron import context
 from neutron.db import db_base_plugin_v2 as base_plugin
 from neutron.db import model_base
 from neutron.db import models_v2
+from neutron.ipam.drivers.neutrondb_ipam import db_models as ipam_models
 from neutron.tests import base
 from neutron.tests.common import base as common_base
 
@@ -47,9 +48,13 @@ class IpamTestCase(object):
     Base class for tests that aim to test ip allocation.
     """
 
-    def configure_test(self):
+    def configure_test(self, use_pluggable_ipam=False):
         model_base.BASEV2.metadata.create_all(self.engine)
         cfg.CONF.set_override('notify_nova_on_port_status_changes', False)
+        if use_pluggable_ipam:
+            self._turn_on_pluggable_ipam()
+        else:
+            self._turn_off_pluggable_ipam()
         self.plugin = base_plugin.NeutronDbPluginV2()
         self.cxt = get_admin_test_context(self.engine.url)
         self.addCleanup(self.cxt._session.close)
@@ -60,6 +65,16 @@ class IpamTestCase(object):
         self._create_network()
         self._create_subnet()
 
+    def _turn_off_pluggable_ipam(self):
+        cfg.CONF.set_override('ipam_driver', None)
+        self.ip_availability_range = models_v2.IPAvailabilityRange
+
+    def _turn_on_pluggable_ipam(self):
+        cfg.CONF.set_override('ipam_driver', 'internal')
+        DB_PLUGIN_KLASS = 'neutron.db.db_base_plugin_v2.NeutronDbPluginV2'
+        self.setup_coreplugin(DB_PLUGIN_KLASS)
+        self.ip_availability_range = ipam_models.IpamAvailabilityRange
+
     def result_set_to_dicts(self, resultset, keys):
         dicts = []
         for item in resultset:
@@ -75,7 +90,7 @@ class IpamTestCase(object):
 
     def assert_ip_avail_range_matches(self, expected):
         result_set = self.cxt.session.query(
-            models_v2.IPAvailabilityRange).all()
+            self.ip_availability_range).all()
         keys = ['first_ip', 'last_ip']
         actual = self.result_set_to_dicts(result_set, keys)
         self.assertEqual(expected, actual)
@@ -218,3 +233,19 @@ class TestIpamPsql(common_base.PostgreSQLTestCase,
     def setUp(self):
         super(TestIpamPsql, self).setUp()
         self.configure_test()
+
+
+class TestPluggableIpamMySql(common_base.MySQLTestCase,
+                             base.BaseTestCase, IpamTestCase):
+
+    def setUp(self):
+        super(TestPluggableIpamMySql, self).setUp()
+        self.configure_test(use_pluggable_ipam=True)
+
+
+class TestPluggableIpamPsql(common_base.PostgreSQLTestCase,
+                            base.BaseTestCase, IpamTestCase):
+
+    def setUp(self):
+        super(TestPluggableIpamPsql, self).setUp()
+        self.configure_test(use_pluggable_ipam=True)
diff --git a/neutron/tests/unit/db/test_db_base_plugin_v2.py b/neutron/tests/unit/db/test_db_base_plugin_v2.py
index f7a86533e43..1a2a9bdcade 100644
--- a/neutron/tests/unit/db/test_db_base_plugin_v2.py
+++ b/neutron/tests/unit/db/test_db_base_plugin_v2.py
@@ -3278,6 +3278,9 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
                                 [{'start': '10.0.0.2', 'end': '10.0.0.254'},
                                  {'end': '10.0.0.254'}],
                                 None,
+                                [{'start': '10.0.0.200', 'end': '10.0.3.20'}],
+                                [{'start': '10.0.2.250', 'end': '10.0.3.5'}],
+                                [{'start': '10.0.2.10', 'end': '10.0.2.5'}],
                                 [{'start': '10.0.0.2', 'end': '10.0.0.3'},
                                  {'start': '10.0.0.2', 'end': '10.0.0.3'}]]
             tenant_id = network['network']['tenant_id']
@@ -3816,7 +3819,7 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
                     return orig(s, instance)
                 mock.patch.object(orm.Session, 'add',
                                   new=db_ref_err_for_ipalloc).start()
-                mock.patch.object(non_ipam.IpamNonPluggableBackend,
+                mock.patch.object(db_base_plugin_common.DbBasePluginCommon,
                                   '_get_subnet',
                                   return_value=mock.Mock()).start()
             # Add an IPv6 auto-address subnet to the network
diff --git a/neutron/tests/unit/db/test_ipam_pluggable_backend.py b/neutron/tests/unit/db/test_ipam_pluggable_backend.py
index fd4e457d940..31cedd73b31 100644
--- a/neutron/tests/unit/db/test_ipam_pluggable_backend.py
+++ b/neutron/tests/unit/db/test_ipam_pluggable_backend.py
@@ -15,18 +15,49 @@
 
 import mock
 import netaddr
+import webob.exc
 
+from oslo_config import cfg
 from oslo_utils import uuidutils
 
 from neutron.common import exceptions as n_exc
 from neutron.common import ipv6_utils
+from neutron.db import ipam_backend_mixin
 from neutron.db import ipam_pluggable_backend
 from neutron.ipam import requests as ipam_req
 from neutron.tests.unit.db import test_db_base_plugin_v2 as test_db_base
 
 
+class UseIpamMixin(object):
+
+    def setUp(self):
+        cfg.CONF.set_override("ipam_driver", 'internal')
+        super(UseIpamMixin, self).setUp()
+
+
+class TestIpamHTTPResponse(UseIpamMixin, test_db_base.TestV2HTTPResponse):
+    pass
+
+
+class TestIpamPorts(UseIpamMixin, test_db_base.TestPortsV2):
+    pass
+
+
+class TestIpamNetworks(UseIpamMixin, test_db_base.TestNetworksV2):
+    pass
+
+
+class TestIpamSubnets(UseIpamMixin, test_db_base.TestSubnetsV2):
+    pass
+
+
+class TestIpamSubnetPool(UseIpamMixin, test_db_base.TestSubnetPoolsV2):
+    pass
+
+
 class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
     def setUp(self):
+        cfg.CONF.set_override("ipam_driver", 'internal')
         super(TestDbBasePluginIpam, self).setUp()
         self.tenant_id = uuidutils.generate_uuid()
         self.subnet_id = uuidutils.generate_uuid()
@@ -56,6 +87,11 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
         mocks['ipam'] = ipam_pluggable_backend.IpamPluggableBackend()
         return mocks
 
+    def _prepare_mocks_with_pool_mock(self, pool_mock):
+        mocks = self._prepare_mocks()
+        pool_mock.get_instance.return_value = mocks['driver']
+        return mocks
+
     def _get_allocate_mock(self, auto_ip='10.0.0.2',
                            fail_ip='127.0.0.1',
                            error_message='SomeError'):
@@ -233,3 +269,225 @@ class TestDbBasePluginIpam(test_db_base.NeutronDbPluginV2TestCase):
         self._validate_allocate_calls(ips[:-1], mocks)
         # Deallocate should be called for the first ip only
         mocks['subnet'].deallocate.assert_called_once_with(auto_ip)
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_create_subnet_over_ipam(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        cidr = '192.168.0.0/24'
+        allocation_pools = [{'start': '192.168.0.2', 'end': '192.168.0.254'}]
+        with self.subnet(allocation_pools=allocation_pools,
+                         cidr=cidr):
+            pool_mock.get_instance.assert_called_once_with(None, mock.ANY)
+            assert mocks['driver'].allocate_subnet.called
+            request = mocks['driver'].allocate_subnet.call_args[0][0]
+            self.assertEqual(ipam_req.SpecificSubnetRequest, type(request))
+            self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_create_subnet_over_ipam_with_rollback(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        mocks['driver'].allocate_subnet.side_effect = ValueError
+        cidr = '10.0.2.0/24'
+        with self.network() as network:
+            self._create_subnet(self.fmt, network['network']['id'],
+                                cidr, expected_res_status=500)
+
+            pool_mock.get_instance.assert_called_once_with(None, mock.ANY)
+            assert mocks['driver'].allocate_subnet.called
+            request = mocks['driver'].allocate_subnet.call_args[0][0]
+            self.assertEqual(ipam_req.SpecificSubnetRequest, type(request))
+            self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
+            # Verify no subnet was created for network
+            req = self.new_show_request('networks', network['network']['id'])
+            res = req.get_response(self.api)
+            net = self.deserialize(self.fmt, res)
+            self.assertEqual(0, len(net['network']['subnets']))
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_ipam_subnet_deallocated_if_create_fails(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        cidr = '10.0.2.0/24'
+        with mock.patch.object(
+                ipam_backend_mixin.IpamBackendMixin, '_save_subnet',
+                side_effect=ValueError), self.network() as network:
+            self._create_subnet(self.fmt, network['network']['id'],
+                                cidr, expected_res_status=500)
+            pool_mock.get_instance.assert_any_call(None, mock.ANY)
+            self.assertEqual(2, pool_mock.get_instance.call_count)
+            assert mocks['driver'].allocate_subnet.called
+            request = mocks['driver'].allocate_subnet.call_args[0][0]
+            self.assertEqual(ipam_req.SpecificSubnetRequest, type(request))
+            self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
+            # Verify remove ipam subnet was called
+            mocks['driver'].remove_subnet.assert_called_once_with(
+                self.subnet_id)
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_update_subnet_over_ipam(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        cidr = '10.0.0.0/24'
+        allocation_pools = [{'start': '10.0.0.2', 'end': '10.0.0.254'}]
+        with self.subnet(allocation_pools=allocation_pools,
+                         cidr=cidr) as subnet:
+            data = {'subnet': {'allocation_pools': [
+                    {'start': '10.0.0.10', 'end': '10.0.0.20'},
+                    {'start': '10.0.0.30', 'end': '10.0.0.40'}]}}
+            req = self.new_update_request('subnets', data,
+                                          subnet['subnet']['id'])
+            res = req.get_response(self.api)
+            self.assertEqual(res.status_code, 200)
+
+            pool_mock.get_instance.assert_any_call(None, mock.ANY)
+            self.assertEqual(2, pool_mock.get_instance.call_count)
+            assert mocks['driver'].update_subnet.called
+            request = mocks['driver'].update_subnet.call_args[0][0]
+            self.assertEqual(ipam_req.SpecificSubnetRequest, type(request))
+            self.assertEqual(netaddr.IPNetwork(cidr), request.subnet_cidr)
+
+            ip_ranges = [netaddr.IPRange(p['start'],
+                p['end']) for p in data['subnet']['allocation_pools']]
+            self.assertEqual(ip_ranges, request.allocation_pools)
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_delete_subnet_over_ipam(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        gateway_ip = '10.0.0.1'
+        cidr = '10.0.0.0/24'
+        res = self._create_network(fmt=self.fmt, name='net',
+                                   admin_state_up=True)
+        network = self.deserialize(self.fmt, res)
+        subnet = self._make_subnet(self.fmt, network, gateway_ip,
+                                   cidr, ip_version=4)
+        req = self.new_delete_request('subnets', subnet['subnet']['id'])
+        res = req.get_response(self.api)
+        self.assertEqual(res.status_int, webob.exc.HTTPNoContent.code)
+
+        pool_mock.get_instance.assert_any_call(None, mock.ANY)
+        self.assertEqual(2, pool_mock.get_instance.call_count)
+        mocks['driver'].remove_subnet.assert_called_once_with(
+            subnet['subnet']['id'])
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_delete_subnet_over_ipam_with_rollback(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        mocks['driver'].remove_subnet.side_effect = ValueError
+        gateway_ip = '10.0.0.1'
+        cidr = '10.0.0.0/24'
+        res = self._create_network(fmt=self.fmt, name='net',
+                                   admin_state_up=True)
+        network = self.deserialize(self.fmt, res)
+        subnet = self._make_subnet(self.fmt, network, gateway_ip,
+                                   cidr, ip_version=4)
+        req = self.new_delete_request('subnets', subnet['subnet']['id'])
+        res = req.get_response(self.api)
+        self.assertEqual(res.status_int, webob.exc.HTTPServerError.code)
+
+        pool_mock.get_instance.assert_any_call(None, mock.ANY)
+        self.assertEqual(2, pool_mock.get_instance.call_count)
+        mocks['driver'].remove_subnet.assert_called_once_with(
+            subnet['subnet']['id'])
+        # Verify subnet was recreated after failed ipam call
+        subnet_req = self.new_show_request('subnets',
+                                           subnet['subnet']['id'])
+        raw_res = subnet_req.get_response(self.api)
+        sub_res = self.deserialize(self.fmt, raw_res)
+        self.assertIn(sub_res['subnet']['cidr'], cidr)
+        self.assertIn(sub_res['subnet']['gateway_ip'],
+                      gateway_ip)
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_create_port_ipam(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        auto_ip = '10.0.0.2'
+        expected_calls = [{'ip_address': ''}]
+        mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
+            auto_ip=auto_ip)
+        with self.subnet() as subnet:
+            with self.port(subnet=subnet) as port:
+                ips = port['port']['fixed_ips']
+                self.assertEqual(1, len(ips))
+                self.assertEqual(ips[0]['ip_address'], auto_ip)
+                self.assertEqual(ips[0]['subnet_id'], subnet['subnet']['id'])
+                self._validate_allocate_calls(expected_calls, mocks)
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_create_port_ipam_with_rollback(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        mocks['subnet'].allocate.side_effect = ValueError
+        with self.network() as network:
+            with self.subnet(network=network):
+                net_id = network['network']['id']
+                data = {
+                    'port': {'network_id': net_id,
+                             'tenant_id': network['network']['tenant_id']}}
+                port_req = self.new_create_request('ports', data)
+                res = port_req.get_response(self.api)
+                self.assertEqual(res.status_int,
+                                 webob.exc.HTTPServerError.code)
+
+                # verify no port left after failure
+                req = self.new_list_request('ports', self.fmt,
+                                            "network_id=%s" % net_id)
+                res = self.deserialize(self.fmt, req.get_response(self.api))
+                self.assertEqual(0, len(res['ports']))
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_update_port_ipam(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        auto_ip = '10.0.0.2'
+        new_ip = '10.0.0.15'
+        expected_calls = [{'ip_address': ip} for ip in ['', new_ip]]
+        mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
+            auto_ip=auto_ip)
+        with self.subnet() as subnet:
+            with self.port(subnet=subnet) as port:
+                ips = port['port']['fixed_ips']
+                self.assertEqual(1, len(ips))
+                self.assertEqual(ips[0]['ip_address'], auto_ip)
+                # Update port with another new ip
+                data = {"port": {"fixed_ips": [{
+                        'subnet_id': subnet['subnet']['id'],
+                        'ip_address': new_ip}]}}
+                req = self.new_update_request('ports', data,
+                                              port['port']['id'])
+                res = self.deserialize(self.fmt, req.get_response(self.api))
+                ips = res['port']['fixed_ips']
+                self.assertEqual(1, len(ips))
+                self.assertEqual(new_ip, ips[0]['ip_address'])
+
+                # Allocate should be called for the first two networks
+                self._validate_allocate_calls(expected_calls, mocks)
+                # Deallocate should be called for the first ip only
+                mocks['subnet'].deallocate.assert_called_once_with(auto_ip)
+
+    @mock.patch('neutron.ipam.driver.Pool')
+    def test_delete_port_ipam(self, pool_mock):
+        mocks = self._prepare_mocks_with_pool_mock(pool_mock)
+        auto_ip = '10.0.0.2'
+        mocks['subnet'].allocate.side_effect = self._get_allocate_mock(
+            auto_ip=auto_ip)
+        with self.subnet() as subnet:
+            with self.port(subnet=subnet) as port:
+                ips = port['port']['fixed_ips']
+                self.assertEqual(1, len(ips))
+                self.assertEqual(ips[0]['ip_address'], auto_ip)
+                req = self.new_delete_request('ports', port['port']['id'])
+                res = req.get_response(self.api)
+
+                self.assertEqual(res.status_int, webob.exc.HTTPNoContent.code)
+                mocks['subnet'].deallocate.assert_called_once_with(auto_ip)
+
+    def test_recreate_port_ipam(self):
+        ip = '10.0.0.2'
+        with self.subnet() as subnet:
+            with self.port(subnet=subnet) as port:
+                ips = port['port']['fixed_ips']
+                self.assertEqual(1, len(ips))
+                self.assertEqual(ips[0]['ip_address'], ip)
+                req = self.new_delete_request('ports', port['port']['id'])
+                res = req.get_response(self.api)
+                self.assertEqual(res.status_int, webob.exc.HTTPNoContent.code)
+                with self.port(subnet=subnet, fixed_ips=ips) as port:
+                    ips = port['port']['fixed_ips']
+                    self.assertEqual(1, len(ips))
+                    self.assertEqual(ips[0]['ip_address'], ip)
diff --git a/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_db_api.py b/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_db_api.py
index 5680018159f..645a09564ad 100644
--- a/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_db_api.py
+++ b/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_db_api.py
@@ -43,12 +43,18 @@ class TestIpamSubnetManager(testlib_api.SqlTestCase):
             id=self.ipam_subnet_id).all()
         self.assertEqual(1, len(subnets))
 
-    def test_associate_neutron_id(self):
-        self.subnet_manager.associate_neutron_id(self.ctx.session,
-                                                 'test-id')
-        subnet = self.ctx.session.query(db_models.IpamSubnet).filter_by(
-            id=self.ipam_subnet_id).first()
-        self.assertEqual('test-id', subnet['neutron_subnet_id'])
+    def test_remove(self):
+        count = db_api.IpamSubnetManager.delete(self.ctx.session,
+                                                self.neutron_subnet_id)
+        self.assertEqual(1, count)
+        subnets = self.ctx.session.query(db_models.IpamSubnet).filter_by(
+            id=self.ipam_subnet_id).all()
+        self.assertEqual(0, len(subnets))
+
+    def test_remove_non_existent_subnet(self):
+        count = db_api.IpamSubnetManager.delete(self.ctx.session,
+                                                'non-existent')
+        self.assertEqual(0, count)
 
     def _create_pools(self, pools):
         db_pools = []
diff --git a/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py b/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py
index 53c511e19d3..5a3f6d6e9cb 100644
--- a/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py
+++ b/neutron/tests/unit/ipam/drivers/neutrondb_ipam/test_driver.py
@@ -144,8 +144,7 @@ class TestNeutronDbIpamPool(testlib_api.SqlTestCase,
     def test_update_subnet_pools(self):
         cidr = '10.0.0.0/24'
         subnet, subnet_req = self._prepare_specific_subnet_request(cidr)
-        ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
-        ipam_subnet.associate_neutron_subnet(subnet['id'])
+        self.ipam_pool.allocate_subnet(subnet_req)
         allocation_pools = [netaddr.IPRange('10.0.0.100', '10.0.0.150'),
                             netaddr.IPRange('10.0.0.200', '10.0.0.250')]
         update_subnet_req = ipam_req.SpecificSubnetRequest(
@@ -162,8 +161,7 @@ class TestNeutronDbIpamPool(testlib_api.SqlTestCase,
     def test_get_subnet(self):
         cidr = '10.0.0.0/24'
         subnet, subnet_req = self._prepare_specific_subnet_request(cidr)
-        ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
-        ipam_subnet.associate_neutron_subnet(subnet['id'])
+        self.ipam_pool.allocate_subnet(subnet_req)
         # Retrieve the subnet
         ipam_subnet = self.ipam_pool.get_subnet(subnet['id'])
         self._verify_ipam_subnet_details(
@@ -176,6 +174,30 @@ class TestNeutronDbIpamPool(testlib_api.SqlTestCase,
                           self.ipam_pool.get_subnet,
                           'boo')
 
+    def test_remove_ipam_subnet(self):
+        cidr = '10.0.0.0/24'
+        subnet, subnet_req = self._prepare_specific_subnet_request(cidr)
+        self.ipam_pool.allocate_subnet(subnet_req)
+        # Remove ipam subnet by neutron subnet id
+        self.ipam_pool.remove_subnet(subnet['id'])
+
+    def test_remove_non_existent_subnet_fails(self):
+        self.assertRaises(n_exc.SubnetNotFound,
+                          self.ipam_pool.remove_subnet,
+                          'non-existent-id')
+
+    def test_get_details_for_invalid_subnet_id_fails(self):
+        cidr = '10.0.0.0/24'
+        subnet_req = ipam_req.SpecificSubnetRequest(
+            self._tenant_id,
+            'non-existent-id',
+            cidr)
+        self.ipam_pool.allocate_subnet(subnet_req)
+        # Neutron subnet does not exist, so get_subnet should fail
+        self.assertRaises(n_exc.SubnetNotFound,
+                          self.ipam_pool.get_subnet,
+                          'non-existent-id')
+
 
 class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
                               TestNeutronDbIpamMixin):
@@ -214,7 +236,6 @@ class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
             gateway_ip=subnet['gateway_ip'],
             allocation_pools=allocation_pool_ranges)
         ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
-        ipam_subnet.associate_neutron_subnet(subnet['id'])
         return ipam_subnet, subnet
 
     def setUp(self):
@@ -314,7 +335,7 @@ class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
         subnet = self._create_subnet(
             self.plugin, self.ctx, self.net_id, cidr)
         subnet_req = ipam_req.SpecificSubnetRequest(
-            'tenant_id', subnet, cidr, gateway_ip=subnet['gateway_ip'])
+            'tenant_id', subnet['id'], cidr, gateway_ip=subnet['gateway_ip'])
         ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
         with self.ctx.session.begin():
             ranges = ipam_subnet._allocate_specific_ip(
@@ -416,28 +437,10 @@ class TestNeutronDbIpamSubnet(testlib_api.SqlTestCase,
         # This test instead might be made to pass, but for the wrong reasons!
         pass
 
-    def _test_allocate_subnet(self, subnet_id):
-        subnet_req = ipam_req.SpecificSubnetRequest(
-            'tenant_id', subnet_id, '192.168.0.0/24')
-        return self.ipam_pool.allocate_subnet(subnet_req)
-
     def test_allocate_subnet_for_non_existent_subnet_pass(self):
-        # This test should pass because neutron subnet is not checked
-        # until associate neutron subnet step
+        # This test should pass because ipam subnet is no longer
+        # have foreign key relationship with neutron subnet.
+        # Creating ipam subnet before neutron subnet is a valid case.
         subnet_req = ipam_req.SpecificSubnetRequest(
             'tenant_id', 'meh', '192.168.0.0/24')
         self.ipam_pool.allocate_subnet(subnet_req)
-
-    def test_associate_neutron_subnet(self):
-        ipam_subnet, subnet = self._create_and_allocate_ipam_subnet(
-            '192.168.0.0/24', ip_version=4)
-        details = ipam_subnet.get_details()
-        self.assertEqual(subnet['id'], details.subnet_id)
-
-    def test_associate_non_existing_neutron_subnet_fails(self):
-        subnet_req = ipam_req.SpecificSubnetRequest(
-            'tenant_id', 'meh', '192.168.0.0/24')
-        ipam_subnet = self.ipam_pool.allocate_subnet(subnet_req)
-        self.assertRaises(n_exc.SubnetNotFound,
-                          ipam_subnet.associate_neutron_subnet,
-                          'meh')
diff --git a/neutron/tests/unit/plugins/opencontrail/test_contrail_plugin.py b/neutron/tests/unit/plugins/opencontrail/test_contrail_plugin.py
index 17df9fcab35..b5ca8d18e1d 100644
--- a/neutron/tests/unit/plugins/opencontrail/test_contrail_plugin.py
+++ b/neutron/tests/unit/plugins/opencontrail/test_contrail_plugin.py
@@ -193,6 +193,8 @@ class KeyStoneInfo(object):
 
 class ContrailPluginTestCase(test_plugin.NeutronDbPluginV2TestCase):
     _plugin_name = ('%s.NeutronPluginContrailCoreV2' % CONTRAIL_PKG_PATH)
+    _fetch = ('neutron.ipam.drivers.neutrondb_ipam.driver.NeutronDbSubnet'
+              '._fetch_subnet')
 
     def setUp(self, plugin=None, ext_mgr=None):
         if 'v6' in self._testMethodName:
@@ -201,6 +203,7 @@ class ContrailPluginTestCase(test_plugin.NeutronDbPluginV2TestCase):
             self.skipTest("OpenContrail Plugin does not support subnet pools.")
         cfg.CONF.keystone_authtoken = KeyStoneInfo()
         mock.patch('requests.post').start().side_effect = FAKE_SERVER.request
+        mock.patch(self._fetch).start().side_effect = FAKE_SERVER._get_subnet
         super(ContrailPluginTestCase, self).setUp(self._plugin_name)
 
 
diff --git a/tox.ini b/tox.ini
index 37ddf17ccd0..9d0a4eaed84 100644
--- a/tox.ini
+++ b/tox.ini
@@ -158,7 +158,6 @@ commands = python -m testtools.run \
     neutron.tests.unit.scheduler.test_dhcp_agent_scheduler \
     neutron.tests.unit.db.test_ipam_backend_mixin \
     neutron.tests.unit.db.test_l3_dvr_db \
-    neutron.tests.unit.db.test_ipam_pluggable_backend \
     neutron.tests.unit.db.test_migration \
     neutron.tests.unit.db.test_agents_db \
     neutron.tests.unit.db.test_dvr_mac_db \