Allow to select subnets to publish DNS records

As described in [0] a new attribute ``dns_publish_fixed_ip`` is added
to subnets, allowing to specify directly whether DNS records should be
published for this subnet. This overrides the previous behaviour that
makes this decision based on various properties of the network that
the subnet is contained in, see [1].

[0] https://launchpad.net/bugs/1784879
[1] https://docs.openstack.org/neutron/latest/admin/config-dns-int-ext-serv.html

Change-Id: I14605ead2694d9e9422b3d7b519aed2e3c340e2a
Partial-Bug: 1784879
This commit is contained in:
Jens Harbott 2019-12-03 15:27:53 +00:00 committed by Jens Harbott (frickler)
parent 36727e3463
commit 57bc6d167b
15 changed files with 590 additions and 116 deletions

View File

@ -1,5 +1,5 @@
function configure_dns_extension {
neutron_ml2_extension_driver_add "dns_domain_ports"
neutron_ml2_extension_driver_add "subnet_dns_publish_fixed_ip"
}
function configure_dns_integration {
iniset $NEUTRON_CONF DEFAULT external_dns_driver designate

View File

@ -94,7 +94,7 @@ In each of the use cases described below:
* The examples assume the OpenStack DNS service as the external DNS.
* A, AAAA and PTR records will be created in the DNS service.
* Before executing any of the use cases, the user must create in the DNS
service under his project a DNS zone where the A and AAAA records will be
service under their project a DNS zone where the A and AAAA records will be
created. For the description of the use cases below, it is assumed the zone
``example.org.`` was created previously.
* The PTR records will be created in zones owned by the project specified
@ -495,7 +495,8 @@ Note that in this use case:
Following are the PTR records created for this example. Note that for
IPv4, the value of ``ipv4_ptr_zone_prefix_size`` is 24. Also, since the zone
for the PTR records is created in the ``service`` project, you need to use
for the PTR records is created in the project specified in the ``[designate]``
section in the config above, usually the ``service`` project, you need to use
admin credentials in order to be able to view it.
@ -516,7 +517,195 @@ Use case 3: Ports are published directly in the external DNS service
--------------------------------------------------------------------
In this case, the user is creating ports or booting instances on a network
that is accessible externally. If the user wants to publish a port in the
that is accessible externally. There are multiple possible scenarios here
depending on which of the DNS extensions is enabled in the Neutron
configuration. These extensions are described in the following in
descending order of priority.
Use case 3a: The ``subnet_dns_publish_fixed_ips`` extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
When the ``subnet_dns_publish_fixed_ips`` extension is enabled, it is possible
to make a selection per subnet whether DNS records should be published for
fixed IPs that are assigned to ports from that subnet. This happens via the
``dns_publish_fixed_ips`` attribute that this extension adds to the
definition of the subnet resource.
It is a boolean flag with a default value of ``False`` but it can be set
to ``True`` when creating or updating subnets. When the flag is ``True``,
all fixed IPs from this subnet are published in the external DNS service,
while at the same time IPs from other subnets having the flag set to
``False`` are not published, even if they otherwise would meet the
criteria from the other use cases below.
A typical scenario for this use case is a dual stack deployment, where a
tenant network would be configured with both an IPv4 and an IPv6 subnet.
The IPv4 subnet will usually be using some RFC1918 address space and being
NATted towards the outside on the attached router, therefore the fixed IPs
from this subnet will not be globally routed and they also should not be
published in the DNS service. (One can still bind floating IPs to these
fixed IPs and DNS records for those floating IPs can still be published
as described above in use cases 1 and 2).
But for the IPv6 subnet, no NAT will happen, instead the subnet will be
configured with some globally routable prefix and thus the user will want
to publish DNS records for fixed IPs from this subnet. This can be
achieved by setting the ``dns_publish_fixed_ips`` attribute for the
IPv6 subnet to ``True`` while leaving the flag set to ``False`` for
the IPv4 subnet. Example:
.. code-block:: console
$ openstack network create dualstack
... output omitted ...
$ openstack subnet create --network dualstack dualstackv4 --subnet-range 192.0.2.0/24
... output omitted ...
$ openstack subnet create --network dualstack dualstackv6 --protocol ipv6 --subnet-range 2001:db8:42:42::/64 --dns-publish-fixed-ip
... output omitted ...
$ openstack zone create example.org. --email mail@example.org
... output omitted ...
$ openstack recordset list example.org.
+--------------------------------------+--------------+------+--------------------------------------------------------------------+--------+--------+
| id | name | type | records | status | action |
+--------------------------------------+--------------+------+--------------------------------------------------------------------+--------+--------+
| 404e9846-1482-433b-8bbc-67677e587d28 | example.org. | NS | ns1.devstack.org. | ACTIVE | NONE |
| de73576a-f9c7-4892-934c-259b77ff02c0 | example.org. | SOA | ns1.devstack.org. mail.example.org. 1575897792 3559 600 86400 3600 | ACTIVE | NONE |
+--------------------------------------+--------------+------+--------------------------------------------------------------------+--------+--------+
$ openstack port create port1 --dns-domain example.org. --dns-name port1 --network dualstack
+-------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Field | Value |
+-------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| admin_state_up | UP |
| allowed_address_pairs | |
| binding_host_id | None |
| binding_profile | None |
| binding_vif_details | None |
| binding_vif_type | None |
| binding_vnic_type | normal |
| created_at | 2019-12-09T13:23:52Z |
| data_plane_status | None |
| description | |
| device_id | |
| device_owner | |
| dns_assignment | fqdn='port1.openstackgate.local.', hostname='port1', ip_address='192.0.2.100' |
| | fqdn='port1.openstackgate.local.', hostname='port1', ip_address='2001:db8:42:42::2a2' |
| dns_domain | example.org. |
| dns_name | port1 |
| extra_dhcp_opts | |
| fixed_ips | ip_address='192.0.2.100', subnet_id='47cc9a39-c88b-4082-a52c-1237c2a1d479' |
| | ip_address='2001:db8:42:42::2a2', subnet_id='f9c04195-1000-4575-a203-3c174772617f' |
| id | f8bc991b-1f84-435a-a5f8-814bd8b9ae9f |
| location | cloud='devstack', project.domain_id='default', project.domain_name=, project.id='86de4dab952d48f79e625b106f7a75f7', project.name='demo', region_name='RegionOne', zone= |
| mac_address | fa:16:3e:13:7a:56 |
| name | port1 |
| network_id | fa8118ed-b7c2-41b8-89bc-97e46f0491ac |
| port_security_enabled | True |
| project_id | 86de4dab952d48f79e625b106f7a75f7 |
| propagate_uplink_status | None |
| qos_policy_id | None |
| resource_request | None |
| revision_number | 1 |
| security_group_ids | f0b02df0-a0b9-4ce8-b067-8b61a8679e9d |
| status | DOWN |
| tags | |
| trunk_details | None |
| updated_at | 2019-12-09T13:23:53Z |
+-------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
$ openstack recordset list example.org.
+--------------------------------------+--------------------+------+--------------------------------------------------------------------+--------+--------+
| id | name | type | records | status | action |
+--------------------------------------+--------------------+------+--------------------------------------------------------------------+--------+--------+
| 404e9846-1482-433b-8bbc-67677e587d28 | example.org. | NS | ns1.devstack.org. | ACTIVE | NONE |
| de73576a-f9c7-4892-934c-259b77ff02c0 | example.org. | SOA | ns1.devstack.org. mail.example.org. 1575897833 3559 600 86400 3600 | ACTIVE | NONE |
| 85ce74a5-7dd6-42d3-932c-c9a029dea05e | port1.example.org. | AAAA | 2001:db8:42:42::2a2 | ACTIVE | NONE |
+--------------------------------------+--------------------+------+--------------------------------------------------------------------+--------+--------+
Use case 3b: The ``dns_domain_ports`` extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the ``dns_domain for ports`` extension has been configured,
the user can create a port specifying a non-blank value in its
``dns_domain`` attribute. If the port is created in an externally
accessible network, DNS records will be published for this port:
.. code-block:: console
$ openstack port create --network 37aaff3a-6047-45ac-bf4f-a825e56fd2b3 --dns-name my-vm --dns-domain port-domain.org. test
+-------------------------+-------------------------------------------------------------------------------+
| Field | Value |
+-------------------------+-------------------------------------------------------------------------------+
| admin_state_up | UP |
| allowed_address_pairs | |
| binding_host_id | None |
| binding_profile | None |
| binding_vif_details | None |
| binding_vif_type | None |
| binding_vnic_type | normal |
| created_at | 2019-06-12T15:43:29Z |
| data_plane_status | None |
| description | |
| device_id | |
| device_owner | |
| dns_assignment | fqdn='my-vm.example.org.', hostname='my-vm', ip_address='203.0.113.9' |
| | fqdn='my-vm.example.org.', hostname='my-vm', ip_address='2001:db8:10::9' |
| dns_domain | port-domain.org. |
| dns_name | my-vm |
| extra_dhcp_opts | |
| fixed_ips | ip_address='203.0.113.9', subnet_id='277eca5d-9869-474b-960e-6da5951d09f7' |
| | ip_address='2001:db8:10::9', subnet_id='eab47748-3f0a-4775-a09f-b0c24bb64bc4' |
| id | 57541c27-f8a9-41f1-8dde-eb10155496e6 |
| mac_address | fa:16:3e:55:d6:c7 |
| name | test |
| network_id | 37aaff3a-6047-45ac-bf4f-a825e56fd2b3 |
| port_security_enabled | True |
| project_id | 07b21ad4-edb6-420b-bd76-9bb4aab0d135 |
| propagate_uplink_status | None |
| qos_policy_id | None |
| resource_request | None |
| revision_number | 1 |
| security_group_ids | 82227b10-d135-4bca-b41f-63c1f2286b3e |
| status | DOWN |
| tags | |
| trunk_details | None |
| updated_at | 2019-06-12T15:43:29Z |
+-------------------------+-------------------------------------------------------------------------------+
In this case, the port's ``dns_name`` (``my-vm``) will be published in the
``port-domain.org.`` zone, as shown here:
.. code-block:: console
$ openstack recordset list port-domain.org.
+--------------------------------------+-------------------------+------+-----------------------------------------------------------------------+--------+--------+
| id | name | type | records | status | action |
+--------------------------------------+-------------------------+------+-----------------------------------------------------------------------+--------+--------+
| 03e5a35b-d984-4d10-942a-2de8ccb9b941 | port-domain.org. | SOA | ns1.devstack.org. malavall.us.ibm.com. 1503272259 3549 600 86400 3600 | ACTIVE | NONE |
| d2dd1dfe-531d-4fea-8c0e-f5b559942ac5 | port-domain.org. | NS | ns1.devstack.org. | ACTIVE | NONE |
| 67a8e83d-7e3c-4fb1-9261-0481318bb7b5 | my-vm.port-domain.org. | A | 203.0.113.9 | ACTIVE | NONE |
| 5a4f671c-9969-47aa-82e1-e05754021852 | my-vm.port-domain.org. | AAAA | 2001:db8:10::9 | ACTIVE | NONE |
+--------------------------------------+-------------------------+------+-----------------------------------------------------------------------+--------+--------+
.. note::
If both the port and its network have a valid non-blank string assigned to
their ``dns_domain`` attributes, the port's ``dns_domain`` takes precedence
over the network's.
.. note::
The name assigned to the port's ``dns_domain`` attribute must end with a
period (``.``).
.. note::
In the above example, the ``port-domain.org.`` zone must be created before
Neutron can publish any port data to it.
.. note::
See :ref:`config-dns-int-ext-serv-net` for detailed instructions on how
to create the externally accessible network.
Use case 3c: The ``dns`` extension
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If the user wants to publish a port in the
external DNS service in a zone specified by the ``dns_domain`` attribute of the
network, these are the steps to be taken:
@ -684,8 +873,8 @@ an instance. Notice that:
the potential performance impact associated with this use case.
Following are the PTR records created for this example. Note that for
IPv4, the value of ipv4_ptr_zone_prefix_size is 24. In the case of IPv6, the
value of ipv6_ptr_zone_prefix_size is 116.
IPv4, the value of ``ipv4_ptr_zone_prefix_size`` is 24. In the case of IPv6, the
value of ``ipv6_ptr_zone_prefix_size`` is 116.
.. code-block:: console
@ -710,80 +899,6 @@ value of ipv6_ptr_zone_prefix_size is 116.
See :ref:`config-dns-int-ext-serv-net` for detailed instructions on how
to create the externally accessible network.
Alternatively, if the ``dns_domain for ports`` extension has been configured,
the user can create a port specifying a non-blank value in its
``dns_domain`` attribute, as shown here:
.. code-block:: console
$ openstack port create --network 37aaff3a-6047-45ac-bf4f-a825e56fd2b3 --dns-name my-vm --dns-domain port-domain.org. test
+-------------------------+-------------------------------------------------------------------------------+
| Field | Value |
+-------------------------+-------------------------------------------------------------------------------+
| admin_state_up | UP |
| allowed_address_pairs | |
| binding_host_id | None |
| binding_profile | None |
| binding_vif_details | None |
| binding_vif_type | None |
| binding_vnic_type | normal |
| created_at | 2019-06-12T15:43:29Z |
| data_plane_status | None |
| description | |
| device_id | |
| device_owner | |
| dns_assignment | fqdn='my-vm.example.org.', hostname='my-vm', ip_address='203.0.113.9' |
| | fqdn='my-vm.example.org.', hostname='my-vm', ip_address='2001:db8:10::9' |
| dns_domain | port-domain.org. |
| dns_name | my-vm |
| extra_dhcp_opts | |
| fixed_ips | ip_address='203.0.113.9', subnet_id='277eca5d-9869-474b-960e-6da5951d09f7' |
| | ip_address='2001:db8:10::9', subnet_id='eab47748-3f0a-4775-a09f-b0c24bb64bc4' |
| id | 57541c27-f8a9-41f1-8dde-eb10155496e6 |
| mac_address | fa:16:3e:55:d6:c7 |
| name | test |
| network_id | 37aaff3a-6047-45ac-bf4f-a825e56fd2b3 |
| port_security_enabled | True |
| project_id | 07b21ad4-edb6-420b-bd76-9bb4aab0d135 |
| propagate_uplink_status | None |
| qos_policy_id | None |
| resource_request | None |
| revision_number | 1 |
| security_group_ids | 82227b10-d135-4bca-b41f-63c1f2286b3e |
| status | DOWN |
| tags | |
| trunk_details | None |
| updated_at | 2019-06-12T15:43:29Z |
+-------------------------+-------------------------------------------------------------------------------+
In this case, the port's ``dns_name`` (``my-vm``) will be published in the
``port-domain.org.`` zone, as shown here:
.. code-block:: console
$ openstack recordset list port-domain.org.
+--------------------------------------+-------------------------+------+-----------------------------------------------------------------------+--------+--------+
| id | name | type | records | status | action |
+--------------------------------------+-------------------------+------+-----------------------------------------------------------------------+--------+--------+
| 03e5a35b-d984-4d10-942a-2de8ccb9b941 | port-domain.org. | SOA | ns1.devstack.org. malavall.us.ibm.com. 1503272259 3549 600 86400 3600 | ACTIVE | NONE |
| d2dd1dfe-531d-4fea-8c0e-f5b559942ac5 | port-domain.org. | NS | ns1.devstack.org. | ACTIVE | NONE |
| 67a8e83d-7e3c-4fb1-9261-0481318bb7b5 | my-vm.port-domain.org. | A | 203.0.113.9 | ACTIVE | NONE |
| 5a4f671c-9969-47aa-82e1-e05754021852 | my-vm.port-domain.org. | AAAA | 2001:db8:10::9 | ACTIVE | NONE |
+--------------------------------------+-------------------------+------+-----------------------------------------------------------------------+--------+--------+
.. note::
If both the port and its network have a valid non-blank string assigned to
their ``dns_domain`` attributes, the port's ``dns_domain`` takes precedence
over the network's.
.. note::
The name assigned to the port's ``dns_domain`` attribute must end with a
period (``.``).
.. note::
In the above example, the ``port-domain.org.`` zone must be created before
Neutron can publish any port data to it.
.. _config-dns-performance-considerations:
Performance considerations
@ -798,10 +913,10 @@ use case.
.. _config-dns-int-ext-serv-net:
Configuration of the externally accessible network for use case 3
-----------------------------------------------------------------
Configuration of the externally accessible network for use cases 3b and 3c
--------------------------------------------------------------------------
In :ref:`config-dns-use-case-3`, the externally accessible network must
For use cases 3b and 3c, the externally accessible network must
meet the following requirements:
* The network may not have attribute ``router:external`` set to ``True``.
@ -809,6 +924,6 @@ meet the following requirements:
* For network types VLAN, GRE, VXLAN or GENEVE, the segmentation ID must be
outside the ranges assigned to project networks.
This usually implies that this use case only works for networks specifically
created for this purpose by an admin, it does not work for networks
which tenants can create.
This usually implies that these use cases only work for networks specifically
created for this purpose by an admin, they do not work for networks
which tenants can create on their own.

View File

@ -5,13 +5,10 @@ DNS integration
===============
This page serves as a guide for how to use the DNS integration functionality of
the Networking service. The functionality described covers DNS from two points
of view:
the Networking service and its interaction with the Compute service.
* The internal DNS functionality offered by the Networking service and its
interaction with the Compute service.
* Integration of the Compute service and the Networking service with an
external DNSaaS (DNS-as-a-Service).
The integration of the Networking service with an external DNSaaS
(DNS-as-a-Service) is described in :ref:`config-dns-int-ext-serv`.
Users can control the behavior of the Networking service in regards to DNS
using two attributes associated with ports, networks, and floating IPs. The
@ -71,7 +68,7 @@ the internal DNS. To enable this functionality, do the following:
``[ml2]`` section of ``/etc/neutron/plugins/ml2/ml2_conf.ini``. The
following is an example:
.. code-block:: console
.. code-block:: ini
[ml2]
extension_drivers = port_security,dns_domain_ports

View File

@ -1 +1 @@
a010322604bc
263d454a9655

View File

@ -0,0 +1,46 @@
# Copyright 2019 x-ion GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
from alembic import op
from neutron_lib.db import constants as db_const
import sqlalchemy as sa
"""Add table and relations for subnet dns_publish_fixed_ip attribute
Revision ID: 263d454a9655
Revises: a010322604bc
Create Date: 2019-05-24 10:00:00.000000
"""
# revision identifiers, used by Alembic.
revision = '263d454a9655'
down_revision = 'a010322604bc'
def upgrade():
op.create_table('subnet_dns_publish_fixed_ips',
sa.Column('subnet_id',
sa.String(length=db_const.UUID_FIELD_SIZE),
nullable=False,
index=True),
sa.Column('dns_publish_fixed_ip',
sa.Boolean(),
nullable=False,
server_default=sa.sql.false()),
sa.ForeignKeyConstraint(['subnet_id'],
['subnets.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('subnet_id'))

View File

@ -14,6 +14,7 @@ from neutron_lib.db import constants
from neutron_lib.db import model_base
import sqlalchemy as sa
from sqlalchemy import orm
from sqlalchemy import sql
from neutron.db.models import l3 as l3_models
from neutron.db import models_v2
@ -97,3 +98,25 @@ class PortDNS(model_base.BASEV2):
uselist=False,
cascade='delete'))
revises_on_change = ('port', )
class SubnetDNSPublishFixedIP(model_base.BASEV2):
__tablename__ = "subnet_dns_publish_fixed_ips"
subnet_id = sa.Column(sa.String(constants.UUID_FIELD_SIZE),
sa.ForeignKey('subnets.id', ondelete="CASCADE"),
primary_key=True,
index=True)
dns_publish_fixed_ip = sa.Column(sa.Boolean(),
nullable=False,
server_default=sql.false())
# Add a relationship to the Subnet model in order to instruct
# SQLAlchemy to eagerly load this association
subnet = orm.relationship(models_v2.Subnet,
load_on_pending=True,
backref=orm.backref("dns_publish_fixed_ip",
lazy='joined',
uselist=False,
cascade='delete'))
revises_on_change = ('subnet', )

View File

@ -0,0 +1,20 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api.definitions import subnet_dns_publish_fixed_ip as apidef
from neutron_lib.api import extensions
class Subnet_dns_publish_fixed_ip(extensions.APIExtensionDescriptor):
"""Extension class supporting dns_publish_fixed_ip attribute for subnet."""
api_definition = apidef

View File

@ -17,9 +17,11 @@ from neutron_lib.db import model_query
from neutron_lib.objects import common_types
from neutron_lib.utils import net as net_utils
from oslo_utils import versionutils
from oslo_versionedobjects import fields as obj_fields
from sqlalchemy import and_, or_
from neutron.db.models import dns as dns_models
from neutron.db.models import segment as segment_model
from neutron.db.models import subnet_service_type
from neutron.db import models_v2
@ -190,7 +192,8 @@ class SubnetServiceType(base.NeutronDbObject):
@base.NeutronObjectRegistry.register
class Subnet(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
# Version 1.1: Add dns_publish_fixed_ip field
VERSION = '1.1'
db_model = models_v2.Subnet
new_facade = True
@ -213,13 +216,15 @@ class Subnet(base.NeutronDbObject):
'shared': obj_fields.BooleanField(nullable=True),
'dns_nameservers': obj_fields.ListOfObjectsField('DNSNameServer',
nullable=True),
'dns_publish_fixed_ip': obj_fields.BooleanField(nullable=True),
'host_routes': obj_fields.ListOfObjectsField('Route', nullable=True),
'ipv6_ra_mode': common_types.IPV6ModeEnumField(nullable=True),
'ipv6_address_mode': common_types.IPV6ModeEnumField(nullable=True),
'service_types': obj_fields.ListOfStringsField(nullable=True)
}
synthetic_fields = ['allocation_pools', 'dns_nameservers', 'host_routes',
synthetic_fields = ['allocation_pools', 'dns_nameservers',
'dns_publish_fixed_ip', 'host_routes',
'service_types', 'shared']
foreign_keys = {'Network': {'network_id': 'id'}}
@ -235,12 +240,29 @@ class Subnet(base.NeutronDbObject):
self.add_extra_filter_name('shared')
def obj_load_attr(self, attrname):
if attrname == 'dns_publish_fixed_ip':
return self._load_dns_publish_fixed_ip()
if attrname == 'shared':
return self._load_shared()
if attrname == 'service_types':
return self._load_service_types()
super(Subnet, self).obj_load_attr(attrname)
def _load_dns_publish_fixed_ip(self, db_obj=None):
if db_obj:
object_data = db_obj.get('dns_publish_fixed_ip', None)
else:
object_data = SubnetDNSPublishFixedIP.get_objects(
self.obj_context,
subnet_id=self.id)
dns_publish_fixed_ip = False
if object_data:
dns_publish_fixed_ip = object_data.get(
'dns_publish_fixed_ip')
setattr(self, 'dns_publish_fixed_ip', dns_publish_fixed_ip)
self.obj_reset_changes(['dns_publish_fixed_ip'])
def _load_shared(self, db_obj=None):
if db_obj:
# NOTE(korzen) db_obj is passed when Subnet object is loaded
@ -273,6 +295,7 @@ class Subnet(base.NeutronDbObject):
def from_db_object(self, db_obj):
super(Subnet, self).from_db_object(db_obj)
self._load_dns_publish_fixed_ip(db_obj)
self._load_shared(db_obj)
self._load_service_types(db_obj)
@ -412,6 +435,11 @@ class Subnet(base.NeutronDbObject):
raise ipam_exceptions.DeferIpam()
return False
def obj_make_compatible(self, primitive, target_version):
_target_version = versionutils.convert_version_to_tuple(target_version)
if _target_version < (1, 1): # version 1.1 adds "dns_publish_fixed_ip"
primitive.pop('dns_publish_fixed_ip', None)
@base.NeutronObjectRegistry.register
class NetworkSubnetLock(base.NeutronDbObject):
@ -438,3 +466,18 @@ class NetworkSubnetLock(base.NeutronDbObject):
subnet_lock = NetworkSubnetLock(context, network_id=network_id,
subnet_id=subnet_id)
subnet_lock.create()
@base.NeutronObjectRegistry.register
class SubnetDNSPublishFixedIP(base.NeutronDbObject):
# Version 1.0: Initial version
VERSION = '1.0'
db_model = dns_models.SubnetDNSPublishFixedIP
primary_keys = ['subnet_id']
fields = {
'subnet_id': common_types.UUIDField(),
'dns_publish_fixed_ip': obj_fields.BooleanField()
}

View File

@ -30,6 +30,7 @@ from oslo_log import log as logging
from neutron.db import segments_db
from neutron.objects import network as net_obj
from neutron.objects import ports as port_obj
from neutron.objects import subnet as subnet_obj
from neutron.services.externaldns import driver
LOG = logging.getLogger(__name__)
@ -88,18 +89,19 @@ class DNSExtensionDriver(api.ExtensionDriver):
request_data)
if is_dns_domain_default:
return
network = self._get_network(plugin_context, db_data['network_id'])
network, subnets = self._get_details(plugin_context,
db_data['network_id'])
self._create_port_dns_record(plugin_context, request_data, db_data,
network, dns_name)
network, subnets, dns_name)
def _create_port_dns_record(self, plugin_context, request_data, db_data,
network, dns_name):
network, subnets, dns_name):
external_dns_domain = (request_data.get(dns_apidef.DNSDOMAIN) or
network.get(dns_apidef.DNSDOMAIN))
flag = self.external_dns_not_needed(plugin_context, network, subnets)
current_dns_name, current_dns_domain = (
self._calculate_current_dns_name_and_domain(
dns_name, external_dns_domain,
self.external_dns_not_needed(plugin_context, network)))
dns_name, external_dns_domain, flag))
dns_data_obj = port_obj.PortDNS(
plugin_context,
@ -131,7 +133,8 @@ class DNSExtensionDriver(api.ExtensionDriver):
return '', ''
return dns_name, external_dns_domain
def _update_dns_db(self, plugin_context, request_data, db_data, network):
def _update_dns_db(self, plugin_context, request_data, db_data, network,
subnets):
dns_name = request_data.get(dns_apidef.DNSNAME)
dns_domain = request_data.get(dns_apidef.DNSDOMAIN)
has_fixed_ips = 'fixed_ips' in request_data
@ -162,7 +165,8 @@ class DNSExtensionDriver(api.ExtensionDriver):
return dns_data_db
if dns_name or dns_domain:
dns_data_db = self._create_port_dns_record(
plugin_context, request_data, db_data, network, dns_name or '')
plugin_context, request_data, db_data, network, subnets,
dns_name or '')
return dns_data_db
def _populate_previous_external_dns_data(self, dns_data_db):
@ -206,9 +210,10 @@ class DNSExtensionDriver(api.ExtensionDriver):
self._extend_port_dict(plugin_context.session, db_data,
db_data, None)
return
network = self._get_network(plugin_context, db_data['network_id'])
network, subnets = self._get_details(plugin_context,
db_data['network_id'])
dns_data_db = None
if self.external_dns_not_needed(plugin_context, network):
if self.external_dns_not_needed(plugin_context, network, subnets):
# No need to update external DNS service. Only process the port's
# dns_name or dns_domain attributes if necessary
if has_dns_name or has_dns_domain:
@ -216,7 +221,7 @@ class DNSExtensionDriver(api.ExtensionDriver):
plugin_context, request_data, db_data)
else:
dns_data_db = self._update_dns_db(plugin_context, request_data,
db_data, network)
db_data, network, subnets)
self._extend_port_dict(plugin_context.session, db_data, db_data,
dns_data_db)
@ -247,14 +252,15 @@ class DNSExtensionDriver(api.ExtensionDriver):
dns_data_db.create()
return dns_data_db
def external_dns_not_needed(self, context, network):
def external_dns_not_needed(self, context, network, subnets):
"""Decide if ports in network need to be sent to the DNS service.
:param context: plugin request context
:param network: network dictionary
:param subnets: list of subnets in network
:return: True or False
"""
pass
return False
def extend_network_dict(self, session, db_data, response_data):
response_data[dns_apidef.DNSDOMAIN] = ''
@ -324,9 +330,11 @@ class DNSExtensionDriver(api.ExtensionDriver):
return self._extend_port_dict(session, db_data, response_data,
dns_data_db)
def _get_network(self, context, network_id):
def _get_details(self, context, network_id):
plugin = directory.get_plugin()
return plugin.get_network(context, network_id)
network = plugin.get_network(context, network_id)
subnets = plugin.get_subnets_by_network(context, network_id)
return network, subnets
class DNSExtensionDriverML2(DNSExtensionDriver):
@ -361,10 +369,13 @@ class DNSExtensionDriverML2(DNSExtensionDriver):
if vlan_range[0] <= segmentation_id <= vlan_range[1]:
return True
def external_dns_not_needed(self, context, network):
def external_dns_not_needed(self, context, network, subnets):
dns_driver = _get_dns_driver()
if not dns_driver:
return True
for subnet in subnets:
if subnet.get('dns_publish_fixed_ip'):
return False
if network['router:external']:
return True
segments = segments_db.get_network_segments(context, network['id'])
@ -424,6 +435,24 @@ def _get_dns_driver():
driver=cfg.CONF.external_dns_driver)
def _filter_by_subnet(context, fixed_ips):
subnet_filtered = []
filter_fixed_ips = False
for ip in fixed_ips:
# TODO(slaweq): This might be a performance issue if ports have lots
# of fixed_ips attached, possibly collect subnets first and do a
# single get_objects call instead
subnet = subnet_obj.Subnet.get_object(
context, id=ip['subnet_id'])
if subnet.get('dns_publish_fixed_ip'):
filter_fixed_ips = True
subnet_filtered.append(str(ip['ip_address']))
if filter_fixed_ips:
return subnet_filtered
else:
return [str(ip['ip_address']) for ip in fixed_ips]
def _create_port_in_external_dns_service(resource, event, trigger, **kwargs):
dns_driver = _get_dns_driver()
if not dns_driver:
@ -434,7 +463,7 @@ def _create_port_in_external_dns_service(resource, event, trigger, **kwargs):
context, port_id=port['id'])
if not (dns_data_db and dns_data_db['current_dns_name']):
return
records = [ip['ip_address'] for ip in port['fixed_ips']]
records = _filter_by_subnet(context, port['fixed_ips'])
_send_data_to_external_dns_service(context, dns_driver,
dns_data_db['current_dns_domain'],
dns_data_db['current_dns_name'],
@ -478,8 +507,8 @@ def _update_port_in_external_dns_service(resource, event, trigger, **kwargs):
original_port = kwargs.get('original_port')
if not original_port:
return
original_ips = [ip['ip_address'] for ip in original_port['fixed_ips']]
updated_ips = [ip['ip_address'] for ip in updated_port['fixed_ips']]
original_ips = _filter_by_subnet(context, original_port['fixed_ips'])
updated_ips = _filter_by_subnet(context, updated_port['fixed_ips'])
is_dns_name_changed = (updated_port[dns_apidef.DNSNAME] !=
original_port[dns_apidef.DNSNAME])
is_dns_domain_changed = (dns_apidef.DNSDOMAIN in updated_port and
@ -518,7 +547,7 @@ def _delete_port_in_external_dns_service(resource, event,
if dns_data_db['current_dns_name']:
ip_allocations = port_obj.IPAllocation.get_objects(context,
port_id=port_id)
records = [str(alloc['ip_address']) for alloc in ip_allocations]
records = _filter_by_subnet(context, ip_allocations)
_remove_data_from_external_dns_service(
context, dns_driver, dns_data_db['current_dns_domain'],
dns_data_db['current_dns_name'], records)

View File

@ -0,0 +1,83 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api.definitions import dns as dns_apidef
from neutron_lib.api.definitions import dns_domain_ports as ports_apidef
from neutron_lib.api.definitions import subnet_dns_publish_fixed_ip as sn_dns
from neutron_lib.api import validators
from oslo_log import log as logging
from neutron.objects import subnet as subnet_obj
from neutron.plugins.ml2.extensions import dns_integration as dns_int
LOG = logging.getLogger(__name__)
class SubnetDNSPublishFixedIPExtensionDriver(
dns_int.DNSDomainPortsExtensionDriver):
_supported_extension_aliases = [dns_apidef.ALIAS,
ports_apidef.ALIAS,
sn_dns.ALIAS]
def initialize(self):
LOG.info("SubnetDNSPublishFixedIPExtensionDriver initialization "
"complete")
@property
def extension_aliases(self):
return self._supported_extension_aliases
def extend_subnet_dict(self, session, db_data, response_data):
# TODO(jh): This returns None instead of the proper response_data
# response_data = (
# super(SubnetDNSPublishFixedIPExtensionDriver,
# self).extend_subnet_dict(
# session, db_data, response_data))
response_data['dns_publish_fixed_ip'] = False
if db_data.dns_publish_fixed_ip:
response_data['dns_publish_fixed_ip'] = True
return response_data
def process_create_subnet(self, plugin_context, request_data, db_data):
flag = request_data.get(sn_dns.DNS_PUBLISH_FIXED_IP)
if not validators.is_attr_set(flag):
return
if flag:
subnet_obj.SubnetDNSPublishFixedIP(
plugin_context,
subnet_id=db_data['id'],
dns_publish_fixed_ip=flag).create()
db_data[sn_dns.DNS_PUBLISH_FIXED_IP] = flag
def process_update_subnet(self, plugin_context, request_data, db_data):
new_value = request_data.get(sn_dns.DNS_PUBLISH_FIXED_IP)
if not validators.is_attr_set(new_value):
return
current_value = db_data.get(sn_dns.DNS_PUBLISH_FIXED_IP)
if current_value == new_value:
return
subnet_id = db_data['id']
if new_value:
subnet_obj.SubnetDNSPublishFixedIP(
plugin_context,
subnet_id=subnet_id,
dns_publish_fixed_ip=new_value).create()
else:
sn_obj = subnet_obj.SubnetDNSPublishFixedIP.get_object(
plugin_context,
subnet_id=subnet_id)
sn_obj.delete()
db_data[sn_dns.DNS_PUBLISH_FIXED_IP] = new_value

View File

@ -62,6 +62,7 @@ NETWORK_API_EXTENSIONS+=",standard-attr-segment"
NETWORK_API_EXTENSIONS+=",standard-attr-timestamp"
NETWORK_API_EXTENSIONS+=",standard-attr-tag"
NETWORK_API_EXTENSIONS+=",subnet_allocation"
NETWORK_API_EXTENSIONS+=",subnet-dns-publish-fixed-ip"
NETWORK_API_EXTENSIONS+=",trunk"
NETWORK_API_EXTENSIONS+=",trunk-details"
NETWORK_API_EXTENSIONS+=",uplink-status-propagation"

View File

@ -0,0 +1,105 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from neutron_lib.api.definitions import dns as dns_apidef
from neutron_lib.api.definitions import l3 as l3_apidef
from neutron_lib.api.definitions import subnet_dns_publish_fixed_ip as api_def
from neutron_lib import constants
from oslo_config import cfg
from neutron.db import db_base_plugin_v2
from neutron.extensions import subnet_dns_publish_fixed_ip
from neutron.tests.unit.plugins.ml2 import test_plugin
class SubnetDNSPublishFixedIPExtensionManager(object):
def get_resources(self):
return []
def get_actions(self):
return []
def get_request_extensions(self):
return []
def get_extended_resources(self, version):
extension = subnet_dns_publish_fixed_ip.Subnet_dns_publish_fixed_ip()
return extension.get_extended_resources(version)
class SubnetDNSPublishFixedIPExtensionTestPlugin(
db_base_plugin_v2.NeutronDbPluginV2):
"""Test plugin to mixin the subnet_dns_publish_fixed_ip extension.
"""
supported_extension_aliases = [api_def.ALIAS,
dns_apidef.ALIAS,
l3_apidef.ALIAS]
class SubnetDNSPublishFixedIPExtensionTestCase(
test_plugin.Ml2PluginV2TestCase):
"""Test API extension subnet_dns_publish_fixed_ip attributes.
"""
_extension_drivers = ['subnet_dns_publish_fixed_ip']
def setUp(self):
cfg.CONF.set_override('extension_drivers',
self._extension_drivers,
group='ml2')
super(SubnetDNSPublishFixedIPExtensionTestCase,
self).setUp()
def _create_subnet(
self, network, ip_version=constants.IP_VERSION_4, cidr=None,
**kwargs):
cidr = cidr or '192.0.2.0/24'
network_id = network['network']['id']
tenant_id = network['network']['tenant_id']
data = {'subnet': {
'network_id': network_id,
'ip_version': str(ip_version),
'tenant_id': tenant_id,
'cidr': cidr}}
data['subnet'].update(kwargs)
subnet_req = self.new_create_request('subnets', data)
res = subnet_req.get_response(self.api)
return self.deserialize(self.fmt, res)['subnet']
def test_create_subnet_default(self):
with self.network() as network:
subnet = self._create_subnet(network)
self.assertIn('dns_publish_fixed_ip', subnet)
self.assertFalse(subnet['dns_publish_fixed_ip'])
data = {'subnet': {'dns_publish_fixed_ip': 'true'}}
req = self.new_update_request('subnets', data,
subnet['id'])
res = self.deserialize(self.fmt,
req.get_response(self.api))
self.assertTrue(res['subnet']['dns_publish_fixed_ip'])
data = {'subnet': {'dns_publish_fixed_ip': 'false'}}
req = self.new_update_request('subnets', data,
subnet['id'])
res = self.deserialize(self.fmt,
req.get_response(self.api))
self.assertFalse(res['subnet']['dns_publish_fixed_ip'])
def test_create_subnet_with_arg(self):
with self.network() as network:
subnet = self._create_subnet(network, dns_publish_fixed_ip=True)
self.assertIn('dns_publish_fixed_ip', subnet)
self.assertTrue(subnet['dns_publish_fixed_ip'])

View File

@ -104,7 +104,8 @@ object_data = {
'SegmentHostMapping': '1.0-521597cf82ead26217c3bd10738f00f0',
'ServiceProfile': '1.0-9beafc9e7d081b8258f3c5cb66ac5eed',
'StandardAttribute': '1.0-617d4f46524c4ce734a6fc1cc0ac6a0b',
'Subnet': '1.0-927155c1fdd5a615cbcb981dda97bce4',
'Subnet': '1.1-5b7e1789a1732259d1e28b4bd87eb1c2',
'SubnetDNSPublishFixedIP': '1.0-db22af6fa20b143986f0cbe06cbfe0ea',
'SubnetPool': '1.0-a0e03895d1a6e7b9d4ab7b0ca13c3867',
'SubnetPoolPrefix': '1.0-13c15144135eb869faa4a76dc3ee3b6c',
'SubnetServiceType': '1.0-05ae4cdb2a9026a697b143926a1add8c',

View File

@ -0,0 +1,10 @@
---
features:
- |
The ``subnet-dns-publish-fixed-ip`` extension adds a new attribute to
the definition of the subnet resource.
When set to ``true`` it will allow publishing DNS records for
fixed IPs from that subnet independent of the restrictions described
in the `DNS integration with an external service
<https://docs.openstack.org/neutron/latest/admin/config-dns-int-ext-serv.html#configuration-of-the-externally-accessible-network-for-use-case-3>`_
documentation.

View File

@ -107,6 +107,7 @@ neutron.ml2.extension_drivers =
data_plane_status = neutron.plugins.ml2.extensions.data_plane_status:DataPlaneStatusExtensionDriver
dns_domain_ports = neutron.plugins.ml2.extensions.dns_integration:DNSDomainPortsExtensionDriver
uplink_status_propagation = neutron.plugins.ml2.extensions.uplink_status_propagation:UplinkStatusPropagationExtensionDriver
subnet_dns_publish_fixed_ip = neutron.plugins.ml2.extensions.subnet_dns_publish_fixed_ip:SubnetDNSPublishFixedIPExtensionDriver
neutron.ipam_drivers =
fake = neutron.tests.unit.ipam.fake_driver:FakeDriver
internal = neutron.ipam.drivers.neutrondb_ipam.driver:NeutronDbPool