]> review.fuel-infra Code Review - openstack-build/neutron-build.git/commitdiff
DB, IPAM & RPC changes for IPv6 Prefix Delegation
authorJohn Davidge <jodavidg@cisco.com>
Thu, 16 Jul 2015 17:26:24 +0000 (18:26 +0100)
committerJohn Davidge <jodavidg@cisco.com>
Wed, 5 Aug 2015 11:22:22 +0000 (12:22 +0100)
This patch includes the DB, IPAM & RPC changes needed for the IPv6 Prefix
Delegation feature.

To enable this feature, the subnetpool_id attribute of subnets has been
modified to allow for a special subnetpool identifier - "prefix_delegation".

WORKFLOW:

1. Admin sets default_ipv6_subnet_pool in neutron.conf to "prefix_delegation"
2. User creates a new IPv6 subnet without a CIDR or subnetpool ID
3. User creates an interface between this subnet and a router with an existing
external interface

The agent-side changes will follow in separate patches.

A documentation patch is up for review here:

https://review.openstack.org/#/c/178739

Video guides for configuring and using this feature are available on
YouTube:

https://www.youtube.com/watch?v=wI830s881HQ
https://www.youtube.com/watch?v=zfsFyS01Fn0

Change-Id: Ic0c6ed4dba74da94a75838178a1837f93d2d0885
Co-Authored-By: Baodong (Robert) Li <baoli@cisco.com>
Partially-Implements: blueprint ipv6-prefix-delegation

neutron/api/rpc/handlers/l3_rpc.py
neutron/api/v2/attributes.py
neutron/common/constants.py
neutron/common/ipv6_utils.py
neutron/common/utils.py
neutron/db/db_base_plugin_v2.py
neutron/db/ipam_backend_mixin.py
neutron/db/ipam_non_pluggable_backend.py
neutron/db/l3_db.py
neutron/tests/unit/db/test_db_base_plugin_v2.py
neutron/tests/unit/db/test_ipam_backend_mixin.py

index 3cc50a5dbff1dd564fce229c261d0668df6400a0..3b9dbd2c3a10564774784c1146fc595e63480606 100644 (file)
@@ -43,7 +43,8 @@ class L3RpcCallback(object):
     # 1.4 Added L3 HA update_router_state. This method was later removed,
     #     since it was unused. The RPC version was not changed
     # 1.5 Added update_ha_routers_states
-    target = oslo_messaging.Target(version='1.5')
+    # 1.6 Added process_prefix_update to support IPv6 Prefix Delegation
+    target = oslo_messaging.Target(version='1.6')
 
     @property
     def plugin(self):
@@ -224,3 +225,10 @@ class L3RpcCallback(object):
 
         LOG.debug('Updating HA routers states on host %s: %s', host, states)
         self.l3plugin.update_routers_states(context, states, host)
+
+    def process_prefix_update(self, context, **kwargs):
+        subnets = kwargs.get('subnets')
+
+        for subnet_id, prefix in subnets.items():
+            self.plugin.update_subnet(context, subnet_id,
+                                      {'subnet': {'cidr': prefix}})
index ce51c3035d0aa0861ee71e997e96b58fa90c39f7..ff0165be431c9ead91b5a5d89d9d2c1dcb7cc1d9 100644 (file)
@@ -367,6 +367,16 @@ def _validate_regex_or_none(data, valid_values=None):
         return _validate_regex(data, valid_values)
 
 
+def _validate_subnetpool_id(data, valid_values=None):
+    if data != constants.IPV6_PD_POOL_ID:
+        return _validate_uuid_or_none(data, valid_values)
+
+
+def _validate_subnetpool_id_or_none(data, valid_values=None):
+    if data is not None:
+        return _validate_subnetpool_id(data, valid_values)
+
+
 def _validate_uuid(data, valid_values=None):
     if not uuidutils.is_uuid_like(data):
         msg = _("'%s' is not a valid UUID") % data
@@ -613,6 +623,8 @@ validators = {'type:dict': _validate_dict,
               'type:subnet': _validate_subnet,
               'type:subnet_list': _validate_subnet_list,
               'type:subnet_or_none': _validate_subnet_or_none,
+              'type:subnetpool_id': _validate_subnetpool_id,
+              'type:subnetpool_id_or_none': _validate_subnetpool_id_or_none,
               'type:uuid': _validate_uuid,
               'type:uuid_or_none': _validate_uuid_or_none,
               'type:uuid_list': _validate_uuid_list,
@@ -743,7 +755,7 @@ RESOURCE_ATTRIBUTE_MAP = {
                           'allow_put': False,
                           'default': ATTR_NOT_SPECIFIED,
                           'required_by_policy': False,
-                          'validate': {'type:uuid_or_none': None},
+                          'validate': {'type:subnetpool_id_or_none': None},
                           'is_visible': True},
         'prefixlen': {'allow_post': True,
                       'allow_put': False,
index d52f4312ae0e6065f6b0ae7972109491304b2350..a575f03a7b92132c7e49e569ca48cc146a924d05 100644 (file)
@@ -141,6 +141,9 @@ IPV6_LLA_PREFIX = 'fe80::/64'
 # indicate that IPv6 Prefix Delegation should be used to allocate subnet CIDRs
 IPV6_PD_POOL_ID = 'prefix_delegation'
 
+# Special provisional prefix for IPv6 Prefix Delegation
+PROVISIONAL_IPV6_PD_PREFIX = '::/64'
+
 # Linux interface max length
 DEVICE_NAME_MAX_LEN = 15
 
index 96d0153f161e8572e4c4038c56d69add9acc0465..9ad5b1ee3e142eec47d546e7894141773b512de9 100644 (file)
@@ -77,3 +77,10 @@ def is_eui64_address(ip_address):
     # '0xfffe' addition is used to build EUI-64 from MAC (RFC4291)
     # Look for it in the middle of the EUI-64 part of address
     return ip.version == 6 and not ((ip & 0xffff000000) ^ 0xfffe000000)
+
+
+def is_ipv6_pd_enabled(subnet):
+    """Returns True if the subnetpool_id of the given subnet is equal to
+       constants.IPV6_PD_POOL_ID
+    """
+    return subnet.get('subnetpool_id') == constants.IPV6_PD_POOL_ID
index c3e56d75c157f6a1e21f7a98e81fa50fb050a4b1..94607e644d07ed84fa48c5b34d0bcb83aef50850 100644 (file)
@@ -233,6 +233,10 @@ def get_hostname():
     return socket.gethostname()
 
 
+def get_first_host_ip(net, ip_version):
+    return str(netaddr.IPAddress(net.first + 1, ip_version))
+
+
 def compare_elements(a, b):
     """Compare elements if a and b have same elements.
 
index 01fb2fc2d2958384dc1815a808f35f2db88f7ae8..7cf34b34691f369e30dc1a9bdb87a613521b0e62 100644 (file)
@@ -24,6 +24,7 @@ from oslo_utils import uuidutils
 from sqlalchemy import and_
 from sqlalchemy import event
 
+from neutron.api.rpc.agentnotifiers import l3_rpc_agent_api
 from neutron.api.v2 import attributes
 from neutron.callbacks import events
 from neutron.callbacks import exceptions
@@ -32,6 +33,7 @@ from neutron.callbacks import resources
 from neutron.common import constants
 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 db_base_plugin_common
@@ -394,7 +396,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
             # NOTE(salv-orlando): There is slight chance of a race, when
             # a subnet-update and a router-interface-add operation are
             # executed concurrently
-            if cur_subnet:
+            if cur_subnet and not ipv6_utils.is_ipv6_pd_enabled(s):
                 alloc_qry = context.session.query(models_v2.IPAllocation)
                 allocated = alloc_qry.filter_by(
                     ip_address=cur_subnet['gateway_ip'],
@@ -439,6 +441,29 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
         if ip_ver == 6:
             self._validate_ipv6_attributes(s, cur_subnet)
 
+    def _validate_subnet_for_pd(self, subnet):
+        """Validates that subnet parameters are correct for IPv6 PD"""
+        if (subnet.get('ip_version') != constants.IP_VERSION_6):
+            reason = _("Prefix Delegation can only be used with IPv6 "
+                       "subnets.")
+            raise n_exc.BadRequest(resource='subnets', msg=reason)
+
+        mode_list = [constants.IPV6_SLAAC,
+                     constants.DHCPV6_STATELESS,
+                     attributes.ATTR_NOT_SPECIFIED]
+
+        ra_mode = subnet.get('ipv6_ra_mode')
+        if ra_mode not in mode_list:
+            reason = _("IPv6 RA Mode must be SLAAC or Stateless for "
+                       "Prefix Delegation.")
+            raise n_exc.BadRequest(resource='subnets', msg=reason)
+
+        address_mode = subnet.get('ipv6_address_mode')
+        if address_mode not in mode_list:
+            reason = _("IPv6 Address Mode must be SLAAC or Stateless for "
+                       "Prefix Delegation.")
+            raise n_exc.BadRequest(resource='subnets', msg=reason)
+
     def _update_router_gw_ports(self, context, network, subnet):
         l3plugin = manager.NeutronManager.get_service_plugins().get(
                 service_constants.L3_ROUTER_NAT)
@@ -543,6 +568,17 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
         subnetpool_id = self._get_subnetpool_id(s)
         if subnetpool_id:
             self.ipam.validate_pools_with_subnetpool(s)
+            if subnetpool_id == constants.IPV6_PD_POOL_ID:
+                if has_cidr:
+                    # We do not currently support requesting a specific
+                    # cidr with IPv6 prefix delegation. Set the subnetpool_id
+                    # to None and allow the request to continue as normal.
+                    subnetpool_id = None
+                    self._validate_subnet(context, s)
+                else:
+                    prefix = constants.PROVISIONAL_IPV6_PD_PREFIX
+                    subnet['subnet']['cidr'] = prefix
+                    self._validate_subnet_for_pd(s)
         else:
             if not has_cidr:
                 msg = _('A cidr must be specified in the absence of a '
@@ -552,6 +588,16 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
 
         return self._create_subnet(context, subnet, subnetpool_id)
 
+    def _update_allocation_pools(self, subnet):
+        """Gets new allocation pools and formats them correctly"""
+        allocation_pools = self.ipam.generate_allocation_pools(
+                                    subnet['cidr'],
+                                    subnet['gateway_ip'])
+        return [{'start': str(netaddr.IPAddress(p.first,
+                                                subnet['ip_version'])),
+                 'end': str(netaddr.IPAddress(p.last, subnet['ip_version']))}
+                for p in allocation_pools]
+
     def update_subnet(self, context, id, subnet):
         """Update the subnet with new info.
 
@@ -559,6 +605,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
         dns lease or we support gratuitous DHCP offers
         """
         s = subnet['subnet']
+        new_cidr = s.get('cidr')
         db_subnet = self._get_subnet(context, id)
         # Fill 'ip_version' and 'allocation_pools' fields with the current
         # value since _validate_subnet() expects subnet spec has 'ip_version'
@@ -567,6 +614,7 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
         s['cidr'] = db_subnet.cidr
         s['id'] = db_subnet.id
         s['tenant_id'] = db_subnet.tenant_id
+        s['subnetpool_id'] = db_subnet.subnetpool_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]
@@ -577,6 +625,17 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
             range_pools = self.ipam.pools_to_ip_range(s['allocation_pools'])
             s['allocation_pools'] = range_pools
 
+        update_ports_needed = False
+        if new_cidr and ipv6_utils.is_ipv6_pd_enabled(s):
+            # This is an ipv6 prefix delegation-enabled subnet being given an
+            # updated cidr by the process_prefix_update RPC
+            s['cidr'] = new_cidr
+            update_ports_needed = True
+            net = netaddr.IPNetwork(s['cidr'], s['ip_version'])
+            # Update gateway_ip and allocation pools based on new cidr
+            s['gateway_ip'] = utils.get_first_host_ip(net, s['ip_version'])
+            s['allocation_pools'] = self._update_allocation_pools(s)
+
         # If either gateway_ip or allocation_pools were specified
         gateway_ip = s.get('gateway_ip')
         if gateway_ip is not None or s.get('allocation_pools') is not None:
@@ -591,6 +650,31 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
         result = self._make_subnet_dict(subnet, context=context)
         # Keep up with fields that changed
         result.update(changes)
+
+        if update_ports_needed:
+            # Find ports that have not yet been updated
+            # with an IP address by Prefix Delegation, and update them
+            ports = self.get_ports(context)
+            routers = []
+            for port in ports:
+                fixed_ips = []
+                new_port = {'port': port}
+                for ip in port['fixed_ips']:
+                    if ip['subnet_id'] == s['id']:
+                        fixed_ip = {'subnet_id': s['id']}
+                        if "router_interface" in port['device_owner']:
+                            routers.append(port['device_id'])
+                            fixed_ip['ip_address'] = s['gateway_ip']
+                        fixed_ips.append(fixed_ip)
+                if fixed_ips:
+                    new_port['port']['fixed_ips'] = fixed_ips
+                    self.update_port(context, port['id'], new_port)
+
+            # Send router_update to l3_agent
+            if routers:
+                l3_rpc_notifier = l3_rpc_agent_api.L3AgentNotifyAPI()
+                l3_rpc_notifier.routers_updated(context, routers)
+
         return result
 
     def _subnet_check_ip_allocations(self, context, subnet_id):
index e52650de3d88e8063eb018d349bffb03d19a72d7..398efa15ee39119811b5cd0180efdce517f03b6f 100644 (file)
@@ -168,7 +168,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
         context.session.add_all(new_pools)
         # Call static method with self to redefine in child
         # (non-pluggable backend)
-        self._rebuild_availability_ranges(context, [s])
+        if not ipv6_utils.is_ipv6_pd_enabled(s):
+            self._rebuild_availability_ranges(context, [s])
         # Gather new pools for result
         result_pools = [{'start': p[0], 'end': p[1]} for p in pools]
         del s['allocation_pools']
@@ -199,7 +200,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
 
         Verifies the specified CIDR does not overlap with the ones defined
         for the other subnets specified for this network, or with any other
-        CIDR if overlapping IPs are disabled.
+        CIDR if overlapping IPs are disabled. Does not apply to subnets with
+        temporary IPv6 Prefix Delegation CIDRs (::/64).
         """
         new_subnet_ipset = netaddr.IPSet([new_subnet_cidr])
         # Disallow subnets with prefix length 0 as they will lead to
@@ -217,7 +219,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
         else:
             subnet_list = self._get_all_subnets(context)
         for subnet in subnet_list:
-            if (netaddr.IPSet([subnet.cidr]) & new_subnet_ipset):
+            if ((netaddr.IPSet([subnet.cidr]) & new_subnet_ipset) and
+                subnet.cidr != constants.PROVISIONAL_IPV6_PD_PREFIX):
                 # don't give out details of the overlapping subnet
                 err_msg = (_("Requested subnet with cidr: %(cidr)s for "
                              "network: %(network_id)s overlaps with another "
@@ -330,10 +333,13 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
                 return subnet
         raise n_exc.InvalidIpForNetwork(ip_address=fixed['ip_address'])
 
+    def generate_pools(self, cidr, gateway_ip):
+        return ipam_utils.generate_pools(cidr, gateway_ip)
+
     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)
+            return self.generate_pools(cidr, gateway_ip)
 
         ip_range_pools = self.pools_to_ip_range(allocation_pools)
         self._validate_allocation_pools(ip_range_pools, cidr)
@@ -355,7 +361,8 @@ class IpamBackendMixin(db_base_plugin_common.DbBasePluginCommon):
             return True
 
         subnet = self._get_subnet(context, subnet_id)
-        return not ipv6_utils.is_auto_address_subnet(subnet)
+        return not (ipv6_utils.is_auto_address_subnet(subnet) and
+                    not ipv6_utils.is_ipv6_pd_enabled(subnet))
 
     def _get_changed_ips_for_port(self, context, original_ips,
                                   new_ips, device_owner):
index 5f5daa7ac7ddbd1f25e1acb211f92af4f1f7c23b..87bf0d188a5b8ffa3a04238ce53e2765ee52557f 100644 (file)
@@ -243,7 +243,8 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
             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 ('ip_address' in fixed and
+                subnet['cidr'] != constants.PROVISIONAL_IPV6_PD_PREFIX):
                 # Ensure that the IP's are unique
                 if not IpamNonPluggableBackend._check_unique_ip(
                         context, network_id,
@@ -268,6 +269,7 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
                 # listed explicitly here by subnet ID) are associated
                 # with the port.
                 if (device_owner in constants.ROUTER_INTERFACE_OWNERS_SNAT or
+                    ipv6_utils.is_ipv6_pd_enabled(subnet) or
                     not is_auto_addr_subnet):
                     fixed_ip_set.append({'subnet_id': subnet['id']})
 
@@ -433,7 +435,7 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
 
     def allocate_subnet(self, context, network, subnet, subnetpool_id):
         subnetpool = None
-        if subnetpool_id:
+        if subnetpool_id and not subnetpool_id == constants.IPV6_PD_POOL_ID:
             subnetpool = self._get_subnetpool(context, subnetpool_id)
             self._validate_ip_version_with_subnetpool(subnet, subnetpool)
 
@@ -452,7 +454,7 @@ class IpamNonPluggableBackend(ipam_backend_mixin.IpamBackendMixin):
                                                                    subnet,
                                                                    subnetpool)
 
-        if subnetpool_id:
+        if subnetpool_id and not subnetpool_id == constants.IPV6_PD_POOL_ID:
             driver = subnet_alloc.SubnetAllocator(subnetpool, context)
             ipam_subnet = driver.allocate_subnet(subnet_request)
             subnet_request = ipam_subnet.get_details()
index c09c5273d33a0d9a8217a6e26e4693747269fb93..180824571f3b07b6552877d662a336b8f8734b78 100644 (file)
@@ -30,6 +30,7 @@ from neutron.callbacks import registry
 from neutron.callbacks import resources
 from neutron.common import constants as l3_constants
 from neutron.common import exceptions as n_exc
+from neutron.common import ipv6_utils
 from neutron.common import rpc as n_rpc
 from neutron.common import utils
 from neutron.db import model_base
@@ -470,6 +471,9 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
                         msg = (_("Router already has a port on subnet %s")
                                % subnet_id)
                         raise n_exc.BadRequest(resource='router', msg=msg)
+                    # Ignore temporary Prefix Delegation CIDRs
+                    if subnet_cidr == l3_constants.PROVISIONAL_IPV6_PD_PREFIX:
+                        continue
                     sub_id = ip['subnet_id']
                     cidr = self._core_plugin._get_subnet(context.elevated(),
                                                          sub_id)['cidr']
@@ -579,7 +583,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
         fixed_ip = {'ip_address': subnet['gateway_ip'],
                     'subnet_id': subnet['id']}
 
-        if subnet['ip_version'] == 6:
+        if (subnet['ip_version'] == 6 and not
+            ipv6_utils.is_ipv6_pd_enabled(subnet)):
             # Add new prefix to an existing ipv6 port with the same network id
             # if one exists
             port = self._find_ipv6_router_port_by_network(router,
@@ -1197,7 +1202,7 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
                           for p in each_port_having_fixed_ips())
         filters = {'network_id': [id for id in network_ids]}
         fields = ['id', 'cidr', 'gateway_ip',
-                  'network_id', 'ipv6_ra_mode']
+                  'network_id', 'ipv6_ra_mode', 'subnetpool_id']
 
         subnets_by_network = dict((id, []) for id in network_ids)
         for subnet in self._core_plugin.get_subnets(context, filters, fields):
@@ -1215,7 +1220,8 @@ class L3_NAT_dbonly_mixin(l3.RouterPluginBase):
                 subnet_info = {'id': subnet['id'],
                                'cidr': subnet['cidr'],
                                'gateway_ip': subnet['gateway_ip'],
-                               'ipv6_ra_mode': subnet['ipv6_ra_mode']}
+                               'ipv6_ra_mode': subnet['ipv6_ra_mode'],
+                               'subnetpool_id': subnet['subnetpool_id']}
                 for fixed_ip in port['fixed_ips']:
                     if fixed_ip['subnet_id'] == subnet['id']:
                         port['subnets'].append(subnet_info)
index fd29ede387ddf0c95dbf4845067bf79a51a08c3a..7e2538d352ae4750cc9126f758765484ef703358 100644 (file)
@@ -3402,6 +3402,38 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
                                  ipv6_ra_mode=constants.IPV6_SLAAC,
                                  ipv6_address_mode=constants.IPV6_SLAAC)
 
+    def test_create_subnet_ipv6_pd_gw_values(self):
+        cidr = constants.PROVISIONAL_IPV6_PD_PREFIX
+        # Gateway is last IP in IPv6 DHCPv6 Stateless subnet
+        gateway = '::ffff:ffff:ffff:ffff'
+        allocation_pools = [{'start': '::1',
+                             'end': '::ffff:ffff:ffff:fffe'}]
+        expected = {'gateway_ip': gateway,
+                    'cidr': cidr,
+                    'allocation_pools': allocation_pools}
+        self._test_create_subnet(expected=expected, gateway_ip=gateway,
+                                 cidr=cidr, ip_version=6,
+                                 ipv6_ra_mode=constants.DHCPV6_STATELESS,
+                                 ipv6_address_mode=constants.DHCPV6_STATELESS)
+        # Gateway is first IP in IPv6 DHCPv6 Stateless subnet
+        gateway = '::1'
+        allocation_pools = [{'start': '::2',
+                             'end': '::ffff:ffff:ffff:ffff'}]
+        expected = {'gateway_ip': gateway,
+                    'cidr': cidr,
+                    'allocation_pools': allocation_pools}
+        self._test_create_subnet(expected=expected, gateway_ip=gateway,
+                                 cidr=cidr, ip_version=6,
+                                 ipv6_ra_mode=constants.DHCPV6_STATELESS,
+                                 ipv6_address_mode=constants.DHCPV6_STATELESS)
+        # If gateway_ip is not specified, allocate first IP from the subnet
+        expected = {'gateway_ip': gateway,
+                    'cidr': cidr}
+        self._test_create_subnet(expected=expected,
+                                 cidr=cidr, ip_version=6,
+                                 ipv6_ra_mode=constants.IPV6_SLAAC,
+                                 ipv6_address_mode=constants.IPV6_SLAAC)
+
     def test_create_subnet_gw_outside_cidr_returns_400(self):
         with self.network() as network:
             self._create_subnet(self.fmt,
@@ -3496,6 +3528,15 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
                                  cidr=cidr, ip_version=6,
                                  allocation_pools=allocation_pools)
 
+    def test_create_subnet_with_v6_pd_allocation_pool(self):
+        gateway_ip = '::1'
+        cidr = constants.PROVISIONAL_IPV6_PD_PREFIX
+        allocation_pools = [{'start': '::2',
+                             'end': '::ffff:ffff:ffff:fffe'}]
+        self._test_create_subnet(gateway_ip=gateway_ip,
+                                 cidr=cidr, ip_version=6,
+                                 allocation_pools=allocation_pools)
+
     def test_create_subnet_with_large_allocation_pool(self):
         gateway_ip = '10.0.0.1'
         cidr = '10.0.0.0/8'
@@ -3693,17 +3734,38 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
             self.assertRaises(n_exc.InvalidInput, plugin._validate_subnet,
                               ctx, new_subnet, cur_subnet)
 
+    def _test_validate_subnet_ipv6_pd_modes(self, cur_subnet=None,
+                                         expect_success=True, **modes):
+        plugin = manager.NeutronManager.get_plugin()
+        ctx = context.get_admin_context(load_admin_roles=False)
+        new_subnet = {'ip_version': 6,
+                      'cidr': constants.PROVISIONAL_IPV6_PD_PREFIX,
+                      'enable_dhcp': True,
+                      'ipv6_address_mode': None,
+                      'ipv6_ra_mode': None}
+        for mode, value in modes.items():
+            new_subnet[mode] = value
+        if expect_success:
+            plugin._validate_subnet(ctx, new_subnet, cur_subnet)
+        else:
+            self.assertRaises(n_exc.InvalidInput, plugin._validate_subnet,
+                              ctx, new_subnet, cur_subnet)
+
     def test_create_subnet_ipv6_ra_modes(self):
         # Test all RA modes with no address mode specified
         for ra_mode in constants.IPV6_MODES:
             self._test_validate_subnet_ipv6_modes(
                 ipv6_ra_mode=ra_mode)
+            self._test_validate_subnet_ipv6_pd_modes(
+                ipv6_ra_mode=ra_mode)
 
     def test_create_subnet_ipv6_addr_modes(self):
         # Test all address modes with no RA mode specified
         for addr_mode in constants.IPV6_MODES:
             self._test_validate_subnet_ipv6_modes(
                 ipv6_address_mode=addr_mode)
+            self._test_validate_subnet_ipv6_pd_modes(
+                ipv6_address_mode=addr_mode)
 
     def test_create_subnet_ipv6_same_ra_and_addr_modes(self):
         # Test all ipv6 modes with ra_mode==addr_mode
@@ -3711,6 +3773,9 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
             self._test_validate_subnet_ipv6_modes(
                 ipv6_ra_mode=ipv6_mode,
                 ipv6_address_mode=ipv6_mode)
+            self._test_validate_subnet_ipv6_pd_modes(
+                ipv6_ra_mode=ipv6_mode,
+                ipv6_address_mode=ipv6_mode)
 
     def test_create_subnet_ipv6_different_ra_and_addr_modes(self):
         # Test all ipv6 modes with ra_mode!=addr_mode
@@ -3720,6 +3785,10 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
                 expect_success=not (ra_mode and addr_mode),
                 ipv6_ra_mode=ra_mode,
                 ipv6_address_mode=addr_mode)
+            self._test_validate_subnet_ipv6_pd_modes(
+                expect_success=not (ra_mode and addr_mode),
+                ipv6_ra_mode=ra_mode,
+                ipv6_address_mode=addr_mode)
 
     def test_create_subnet_ipv6_out_of_cidr_global(self):
         gateway_ip = '2000::1'
@@ -5531,6 +5600,39 @@ class TestNeutronDbPluginV2(base.BaseTestCase):
 
         self._test__allocate_ips_for_port(subnets, port, expected)
 
+    def test__allocate_ips_for_port_2_slaac_pd_subnets(self):
+        subnets = [
+            {
+                'cidr': constants.PROVISIONAL_IPV6_PD_PREFIX,
+                'enable_dhcp': True,
+                'gateway_ip': '::1',
+                'id': 'd1a28edd-bd83-480a-bd40-93d036c89f13',
+                'network_id': 'fbb9b578-95eb-4b79-a116-78e5c4927176',
+                'ip_version': 6,
+                'ipv6_address_mode': None,
+                'ipv6_ra_mode': 'slaac'},
+            {
+                'cidr': constants.PROVISIONAL_IPV6_PD_PREFIX,
+                'enable_dhcp': True,
+                'gateway_ip': '::1',
+                'id': 'dc813d3d-ed66-4184-8570-7325c8195e28',
+                'network_id': 'fbb9b578-95eb-4b79-a116-78e5c4927176',
+                'ip_version': 6,
+                'ipv6_address_mode': None,
+                'ipv6_ra_mode': 'slaac'}]
+        port = {'port': {
+            'network_id': 'fbb9b578-95eb-4b79-a116-78e5c4927176',
+            'fixed_ips': attributes.ATTR_NOT_SPECIFIED,
+            'mac_address': '12:34:56:78:44:ab',
+            'device_owner': 'compute'}}
+        expected = []
+        for subnet in subnets:
+            addr = str(ipv6_utils.get_ipv6_addr_by_EUI64(
+                            subnet['cidr'], port['port']['mac_address']))
+            expected.append({'ip_address': addr, 'subnet_id': subnet['id']})
+
+        self._test__allocate_ips_for_port(subnets, port, expected)
+
 
 class NeutronDbPluginV2AsMixinTestCase(NeutronDbPluginV2TestCase,
                                        testlib_api.SqlTestCase):
index 1a5183ddc9260dbb95ccc8c3854cce9f1c2afb7d..c25045a63a0e1f757cd1c5cc0b804ac502f28a45 100644 (file)
@@ -80,6 +80,29 @@ class TestIpamBackendMixin(base.BaseTestCase):
         self._test_get_changed_ips_for_port(expected_change, original_ips,
                                             new_ips, self.owner_non_router)
 
+    def test__get_changed_ips_for_port_autoaddress_ipv6_pd_enabled(self):
+        owner_not_router = constants.DEVICE_OWNER_DHCP
+        new_ips = self._prepare_ips(self.default_new_ips)
+
+        original = (('id-1', '192.168.1.1'),
+                    ('id-5', '2000:1234:5678::12FF:FE34:5678'))
+        original_ips = self._prepare_ips(original)
+
+        # mock to test auto address part
+        pd_subnet = {'subnetpool_id': constants.IPV6_PD_POOL_ID,
+                     'ipv6_address_mode': constants.IPV6_SLAAC,
+                     'ipv6_ra_mode': constants.IPV6_SLAAC}
+        self.mixin._get_subnet = mock.Mock(return_value=pd_subnet)
+
+        # make a copy of original_ips
+        # since it is changed by _get_changed_ips_for_port
+        expected_change = self.mixin.Changes(add=[new_ips[1]],
+                                             original=[original_ips[0]],
+                                             remove=[original_ips[1]])
+
+        self._test_get_changed_ips_for_port(expected_change, original_ips,
+                                            new_ips, owner_not_router)
+
     def _test_get_changed_ips_for_port_no_ip_address(self):
         # IP address should be added if only subnet_id is provided,
         # independently from auto_address status for subnet