From 57bc6d167b44b4c898626aad2ad840ed85b7df17 Mon Sep 17 00:00:00 2001 From: Jens Harbott Date: Tue, 3 Dec 2019 15:27:53 +0000 Subject: [PATCH] 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 --- devstack/lib/dns | 2 +- doc/source/admin/config-dns-int-ext-serv.rst | 285 ++++++++++++------ doc/source/admin/config-dns-int.rst | 11 +- .../alembic_migrations/versions/EXPAND_HEAD | 2 +- ...655_add_dns_publish_fixed_ip_to_subnets.py | 46 +++ neutron/db/models/dns.py | 23 ++ .../extensions/subnet_dns_publish_fixed_ip.py | 20 ++ neutron/objects/subnet.py | 47 ++- .../plugins/ml2/extensions/dns_integration.py | 67 ++-- .../extensions/subnet_dns_publish_fixed_ip.py | 83 +++++ .../tests/contrib/hooks/api_all_extensions | 1 + .../test_subnet_dns_publish_fixed_ip.py | 105 +++++++ neutron/tests/unit/objects/test_objects.py | 3 +- ...h-fixed-ip-extension-6a5bb42a048a6671.yaml | 10 + setup.cfg | 1 + 15 files changed, 590 insertions(+), 116 deletions(-) create mode 100644 neutron/db/migration/alembic_migrations/versions/ussuri/expand/263d454a9655_add_dns_publish_fixed_ip_to_subnets.py create mode 100644 neutron/extensions/subnet_dns_publish_fixed_ip.py create mode 100644 neutron/plugins/ml2/extensions/subnet_dns_publish_fixed_ip.py create mode 100644 neutron/tests/unit/extensions/test_subnet_dns_publish_fixed_ip.py create mode 100644 releasenotes/notes/subnet-dns-publish-fixed-ip-extension-6a5bb42a048a6671.yaml diff --git a/devstack/lib/dns b/devstack/lib/dns index 5d4312d7046..efa00e39cd1 100644 --- a/devstack/lib/dns +++ b/devstack/lib/dns @@ -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 diff --git a/doc/source/admin/config-dns-int-ext-serv.rst b/doc/source/admin/config-dns-int-ext-serv.rst index 78f2a93cfb2..53c30b860cf 100644 --- a/doc/source/admin/config-dns-int-ext-serv.rst +++ b/doc/source/admin/config-dns-int-ext-serv.rst @@ -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. diff --git a/doc/source/admin/config-dns-int.rst b/doc/source/admin/config-dns-int.rst index 282e4c83726..99881bdc27f 100644 --- a/doc/source/admin/config-dns-int.rst +++ b/doc/source/admin/config-dns-int.rst @@ -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 diff --git a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD index af6774e291b..8a2025caffd 100644 --- a/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD +++ b/neutron/db/migration/alembic_migrations/versions/EXPAND_HEAD @@ -1 +1 @@ -a010322604bc +263d454a9655 diff --git a/neutron/db/migration/alembic_migrations/versions/ussuri/expand/263d454a9655_add_dns_publish_fixed_ip_to_subnets.py b/neutron/db/migration/alembic_migrations/versions/ussuri/expand/263d454a9655_add_dns_publish_fixed_ip_to_subnets.py new file mode 100644 index 00000000000..ea3f783d8b1 --- /dev/null +++ b/neutron/db/migration/alembic_migrations/versions/ussuri/expand/263d454a9655_add_dns_publish_fixed_ip_to_subnets.py @@ -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')) diff --git a/neutron/db/models/dns.py b/neutron/db/models/dns.py index b018c8aecff..51bd7a748e2 100644 --- a/neutron/db/models/dns.py +++ b/neutron/db/models/dns.py @@ -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', ) diff --git a/neutron/extensions/subnet_dns_publish_fixed_ip.py b/neutron/extensions/subnet_dns_publish_fixed_ip.py new file mode 100644 index 00000000000..7d3291b4ea0 --- /dev/null +++ b/neutron/extensions/subnet_dns_publish_fixed_ip.py @@ -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 diff --git a/neutron/objects/subnet.py b/neutron/objects/subnet.py index e43d8442b1e..cd697e9bfd2 100644 --- a/neutron/objects/subnet.py +++ b/neutron/objects/subnet.py @@ -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() + } diff --git a/neutron/plugins/ml2/extensions/dns_integration.py b/neutron/plugins/ml2/extensions/dns_integration.py index d588cb04544..60d5e06bf12 100644 --- a/neutron/plugins/ml2/extensions/dns_integration.py +++ b/neutron/plugins/ml2/extensions/dns_integration.py @@ -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) diff --git a/neutron/plugins/ml2/extensions/subnet_dns_publish_fixed_ip.py b/neutron/plugins/ml2/extensions/subnet_dns_publish_fixed_ip.py new file mode 100644 index 00000000000..762e1c008c8 --- /dev/null +++ b/neutron/plugins/ml2/extensions/subnet_dns_publish_fixed_ip.py @@ -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 diff --git a/neutron/tests/contrib/hooks/api_all_extensions b/neutron/tests/contrib/hooks/api_all_extensions index 630bc54f019..2db594226b2 100644 --- a/neutron/tests/contrib/hooks/api_all_extensions +++ b/neutron/tests/contrib/hooks/api_all_extensions @@ -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" diff --git a/neutron/tests/unit/extensions/test_subnet_dns_publish_fixed_ip.py b/neutron/tests/unit/extensions/test_subnet_dns_publish_fixed_ip.py new file mode 100644 index 00000000000..c1b13193504 --- /dev/null +++ b/neutron/tests/unit/extensions/test_subnet_dns_publish_fixed_ip.py @@ -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']) diff --git a/neutron/tests/unit/objects/test_objects.py b/neutron/tests/unit/objects/test_objects.py index ddd0b815f85..6b44078f2ec 100644 --- a/neutron/tests/unit/objects/test_objects.py +++ b/neutron/tests/unit/objects/test_objects.py @@ -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', diff --git a/releasenotes/notes/subnet-dns-publish-fixed-ip-extension-6a5bb42a048a6671.yaml b/releasenotes/notes/subnet-dns-publish-fixed-ip-extension-6a5bb42a048a6671.yaml new file mode 100644 index 00000000000..5d82e2f0565 --- /dev/null +++ b/releasenotes/notes/subnet-dns-publish-fixed-ip-extension-6a5bb42a048a6671.yaml @@ -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 + `_ + documentation. diff --git a/setup.cfg b/setup.cfg index e81093a0238..6a5c5987454 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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