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