# Copyright (c) 2016 IBM # All Rights Reserved. # # 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. import netaddr from designateclient import exceptions as d_exc from designateclient.v2 import client as d_client from keystoneclient.auth.identity.generic import password from keystoneclient.auth import token_endpoint from keystoneclient import session from neutron_lib import constants from oslo_config import cfg from oslo_log import log from neutron._i18n import _ from neutron.extensions import dns from neutron.services.externaldns import driver IPV4_PTR_ZONE_PREFIX_MIN_SIZE = 8 IPV4_PTR_ZONE_PREFIX_MAX_SIZE = 24 IPV6_PTR_ZONE_PREFIX_MIN_SIZE = 4 IPV6_PTR_ZONE_PREFIX_MAX_SIZE = 124 LOG = log.getLogger(__name__) _SESSION = None designate_opts = [ cfg.StrOpt('url', help=_('URL for connecting to designate')), cfg.StrOpt('admin_username', help=_('Username for connecting to designate in admin ' 'context')), cfg.StrOpt('admin_password', help=_('Password for connecting to designate in admin ' 'context'), secret=True), cfg.StrOpt('admin_tenant_id', help=_('Tenant id for connecting to designate in admin ' 'context')), cfg.StrOpt('admin_tenant_name', help=_('Tenant name for connecting to designate in admin ' 'context')), cfg.StrOpt('admin_auth_url', help=_('Authorization URL for connecting to designate in admin ' 'context')), cfg.BoolOpt('insecure', default=False, help=_('Skip cert validation for SSL based admin_auth_url')), cfg.StrOpt('ca_cert', help=_('CA certificate file to use to verify ' 'connecting clients')), cfg.BoolOpt('allow_reverse_dns_lookup', default=True, help=_('Allow the creation of PTR records')), cfg.IntOpt('ipv4_ptr_zone_prefix_size', default=24, help=_('Number of bits in an ipv4 PTR zone that will be considered ' 'network prefix. It has to align to byte boundary. Minimum ' 'value is 8. Maximum value is 24. As a consequence, range ' 'of values is 8, 16 and 24')), cfg.IntOpt('ipv6_ptr_zone_prefix_size', default=120, help=_('Number of bits in an ipv6 PTR zone that will be considered ' 'network prefix. It has to align to nyble boundary. Minimum ' 'value is 4. Maximum value is 124. As a consequence, range ' 'of values is 4, 8, 12, 16,..., 124')), cfg.StrOpt('ptr_zone_email', default='', help=_('The email address to be used when creating PTR zones. ' 'If not specified, the email address will be ' 'admin@')), ] DESIGNATE_GROUP = 'designate' CONF = cfg.CONF CONF.register_opts(designate_opts, DESIGNATE_GROUP) def get_clients(context): global _SESSION if not _SESSION: if CONF.designate.insecure: verify = False else: verify = CONF.designate.ca_cert or True _SESSION = session.Session(verify=verify) auth = token_endpoint.Token(CONF.designate.url, context.auth_token) client = d_client.Client(session=_SESSION, auth=auth) admin_auth = password.Password( auth_url=CONF.designate.admin_auth_url, username=CONF.designate.admin_username, password=CONF.designate.admin_password, tenant_name=CONF.designate.admin_tenant_name, tenant_id=CONF.designate.admin_tenant_id) admin_client = d_client.Client(session=_SESSION, auth=admin_auth) return client, admin_client class Designate(driver.ExternalDNSService): """Driver for Designate.""" def __init__(self): ipv4_ptr_zone_size = CONF.designate.ipv4_ptr_zone_prefix_size ipv6_ptr_zone_size = CONF.designate.ipv6_ptr_zone_prefix_size if (ipv4_ptr_zone_size < IPV4_PTR_ZONE_PREFIX_MIN_SIZE or ipv4_ptr_zone_size > IPV4_PTR_ZONE_PREFIX_MAX_SIZE or (ipv4_ptr_zone_size % 8) != 0): raise dns.InvalidPTRZoneConfiguration( parameter='ipv4_ptr_zone_size', number='8', maximum=str(IPV4_PTR_ZONE_PREFIX_MAX_SIZE), minimum=str(IPV4_PTR_ZONE_PREFIX_MIN_SIZE)) if (ipv6_ptr_zone_size < IPV6_PTR_ZONE_PREFIX_MIN_SIZE or ipv6_ptr_zone_size > IPV6_PTR_ZONE_PREFIX_MAX_SIZE or (ipv6_ptr_zone_size % 4) != 0): raise dns.InvalidPTRZoneConfiguration( parameter='ipv6_ptr_zone_size', number='4', maximum=str(IPV6_PTR_ZONE_PREFIX_MAX_SIZE), minimum=str(IPV6_PTR_ZONE_PREFIX_MIN_SIZE)) def create_record_set(self, context, dns_domain, dns_name, records): designate, designate_admin = get_clients(context) v4, v6 = self._classify_records(records) try: if v4: designate.recordsets.create(dns_domain, dns_name, 'A', v4) if v6: designate.recordsets.create(dns_domain, dns_name, 'AAAA', v6) except d_exc.NotFound: raise dns.DNSDomainNotFound(dns_domain=dns_domain) except d_exc.Conflict: raise dns.DuplicateRecordSet(dns_name=dns_name) if not CONF.designate.allow_reverse_dns_lookup: return # Set up the PTR records recordset_name = '%s.%s' % (dns_name, dns_domain) ptr_zone_email = 'admin@%s' % dns_domain[:-1] if CONF.designate.ptr_zone_email: ptr_zone_email = CONF.designate.ptr_zone_email for record in records: in_addr_name = netaddr.IPAddress(record).reverse_dns in_addr_zone_name = self._get_in_addr_zone_name(in_addr_name) in_addr_zone_description = ( 'An %s zone for reverse lookups set up by Neutron.' % '.'.join(in_addr_name.split('.')[-3:])) try: # Since we don't delete in-addr zones, assume it already # exists. If it doesn't, create it designate_admin.recordsets.create(in_addr_zone_name, in_addr_name, 'PTR', [recordset_name]) except d_exc.NotFound: designate_admin.zones.create( in_addr_zone_name, email=ptr_zone_email, description=in_addr_zone_description) designate_admin.recordsets.create(in_addr_zone_name, in_addr_name, 'PTR', [recordset_name]) def _classify_records(self, records): v4 = [] v6 = [] for record in records: if netaddr.IPAddress(record).version == 4: v4.append(record) else: v6.append(record) return v4, v6 def _get_in_addr_zone_name(self, in_addr_name): units = self._get_bytes_or_nybles_to_skip(in_addr_name) return '.'.join(in_addr_name.split('.')[units:]) def _get_bytes_or_nybles_to_skip(self, in_addr_name): if 'in-addr.arpa' in in_addr_name: return int((constants.IPv4_BITS - CONF.designate.ipv4_ptr_zone_prefix_size) / 8) return int((constants.IPv6_BITS - CONF.designate.ipv6_ptr_zone_prefix_size) / 4) def delete_record_set(self, context, dns_domain, dns_name, records): designate, designate_admin = get_clients(context) ids_to_delete = self._get_ids_ips_to_delete( dns_domain, '%s.%s' % (dns_name, dns_domain), records, designate) for _id in ids_to_delete: designate.recordsets.delete(dns_domain, _id) if not CONF.designate.allow_reverse_dns_lookup: return for record in records: in_addr_name = netaddr.IPAddress(record).reverse_dns in_addr_zone_name = self._get_in_addr_zone_name(in_addr_name) designate_admin.recordsets.delete(in_addr_zone_name, in_addr_name) def _get_ids_ips_to_delete(self, dns_domain, name, records, designate_client): try: recordsets = designate_client.recordsets.list( dns_domain, criterion={"name": "%s" % name}) except d_exc.NotFound: raise dns.DNSDomainNotFound(dns_domain=dns_domain) ids = [rec['id'] for rec in recordsets] ips = [ip for rec in recordsets for ip in rec['records']] if set(ips) != set(records): raise dns.DuplicateRecordSet(dns_name=name) return ids