Source code for designate.central.service

# Copyright 2012 Managed I.T.
# Copyright 2013 - 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 concurrent.futures import ThreadPoolExecutor
from concurrent.futures import TimeoutError
import copy
import random
from random import SystemRandom
import re
import string

from dns import exception as dnsexception
from dns import zone as dnszone
from oslo_log import log as logging
import oslo_messaging as messaging
from oslo_utils import timeutils

from designate.common import constants
from designate.common.decorators import lock
from designate.common.decorators import notification
from designate.common.decorators import rpc
import designate.conf
from designate import coordination
from designate import dnsutils
from designate import exceptions
from designate import network_api
from designate import objects
from designate import policy
from designate import quota
from designate import scheduler
from designate import service
from designate import storage
from designate.storage import transaction
from designate.storage import transaction_shallow_copy
from designate import utils
from designate.worker import rpcapi as worker_rpcapi


CONF = designate.conf.CONF
LOG = logging.getLogger(__name__)


[docs] class Service(service.RPCService): RPC_API_VERSION = '6.10' target = messaging.Target(version=RPC_API_VERSION) def __init__(self): self.zone_lock_local = lock.ZoneLockLocal() self.notification_thread_local = notification.NotificationThreadLocal() self._scheduler = None self._storage = None self._quota = None self._blacklist_executor = ThreadPoolExecutor(max_workers=1) super().__init__( self.service_name, CONF['service:central'].topic, threads=CONF['service:central'].threads, ) self.coordination = coordination.Coordination( self.service_name, self.tg, grouping_enabled=False ) self.network_api = network_api.get_network_api(CONF.network_api) @property def scheduler(self): if not self._scheduler: # Get a scheduler instance self._scheduler = scheduler.get_scheduler(storage=self.storage) return self._scheduler @property def quota(self): if not self._quota: # Get a quota manager instance self._quota = quota.get_quota() return self._quota @property def storage(self): if not self._storage: self._storage = storage.get_storage() return self._storage @property def service_name(self): return 'central'
[docs] def start(self): if (CONF['service:central'].managed_resource_tenant_id == "00000000-0000-0000-0000-000000000000"): LOG.warning("Managed Resource Tenant ID is not properly " "configured") super().start() self.coordination.start()
[docs] def stop(self, graceful=True): self.coordination.stop() super().stop(graceful)
@property def worker_api(self): return worker_rpcapi.WorkerAPI.get_instance() def _is_valid_zone_name(self, context, zone_name): # Validate zone name length if zone_name is None: raise exceptions.InvalidObject if len(zone_name) > CONF['service:central'].max_zone_name_len: raise exceptions.InvalidZoneName('Name too long') # Break the zone name up into its component labels zone_labels = zone_name.strip('.').split('.') # We need more than 1 label. if len(zone_labels) <= 1: raise exceptions.InvalidZoneName('More than one label is ' 'required') tlds = self.storage.find_tlds(context) if tlds: LOG.debug("Checking if %s has a valid TLD", zone_name) allowed = False for i in range(-len(zone_labels), 0): last_i_labels = zone_labels[i:] LOG.debug("Checking %s against the TLD list", last_i_labels) if ".".join(last_i_labels) in tlds: allowed = True break if not allowed: raise exceptions.InvalidZoneName('Invalid TLD') # Now check that the zone name is not the same as a TLD try: stripped_zone_name = zone_name.rstrip('.').lower() self.storage.find_tld( context, {'name': stripped_zone_name}) except exceptions.TldNotFound: LOG.debug("%s has a valid TLD", zone_name) else: raise exceptions.InvalidZoneName( 'Zone name cannot be the same as a TLD') # Check zone name blacklist if self._is_blacklisted_zone_name(context, zone_name): # Some users are allowed bypass the blacklist.. Is this one? if not policy.check('use_blacklisted_zone', context, do_raise=False): raise exceptions.InvalidZoneName('Blacklisted zone name') return True def _is_valid_recordset_name(self, context, zone, recordset_name): if recordset_name is None: raise exceptions.InvalidObject if not recordset_name.endswith('.'): raise ValueError('Please supply a FQDN') # Validate record name length max_len = CONF['service:central'].max_recordset_name_len if len(recordset_name) > max_len: raise exceptions.InvalidRecordSetName('Name too long') # RecordSets must be contained in the parent zone if (recordset_name != zone['name'] and not recordset_name.endswith("." + zone['name'])): raise exceptions.InvalidRecordSetLocation( 'RecordSet is not contained within it\'s parent zone') def _is_valid_recordset_placement(self, context, zone, recordset_name, recordset_type, recordset_id=None): # CNAME's must not be created at the zone apex. if recordset_type == 'CNAME' and recordset_name == zone.name: raise exceptions.InvalidRecordSetLocation( 'CNAME recordsets may not be created at the zone apex') # CNAME's must not share a name with other recordsets criterion = { 'zone_id': zone.id, 'name': recordset_name, } if recordset_type != 'CNAME': criterion['type'] = 'CNAME' recordsets = self.storage.find_recordsets(context, criterion) if ((len(recordsets) == 1 and recordsets[0].id != recordset_id) or len(recordsets) > 1): raise exceptions.InvalidRecordSetLocation( 'CNAME recordsets may not share a name with any other records') return True def _is_valid_recordset_placement_subzone(self, context, zone, recordset_name, criterion=None): """ Check that the placement of the requested rrset belongs to any of the zones subzones.. """ LOG.debug("Checking if %s belongs in any of %s subzones", recordset_name, zone.name) criterion = criterion or {} context = context.elevated(all_tenants=True) if zone.name == recordset_name: return child_zones = self.storage.find_zones( context, {"parent_zone_id": zone.id}) for child_zone in child_zones: try: self._is_valid_recordset_name( context, child_zone, recordset_name) except Exception: continue else: msg = ( 'RecordSet belongs in a child zone: {}' .format(child_zone['name']) ) raise exceptions.InvalidRecordSetLocation(msg) def _is_valid_recordset_records(self, recordset): """ Check to make sure that the records in the recordset follow the rules, and won't blow up on the nameserver. """ try: recordset.records except (AttributeError, exceptions.RelationNotLoaded): pass else: if len(recordset.records) > 1 and recordset.type == 'CNAME': raise exceptions.BadRequest( 'CNAME recordsets may not have more than 1 record' ) def _is_blacklisted_zone_name(self, context, zone_name): """ Ensures the provided zone_name is not blacklisted. """ blacklists = self.storage.find_blacklists(context) def _check_pattern(pattern, name): return bool(re.search(pattern, name)) for blacklist in blacklists: future = self._blacklist_executor.submit(_check_pattern, blacklist.pattern, zone_name) try: if future.result(timeout=0.02): return True except TimeoutError: LOG.critical( 'Blacklist regex (%(pattern)s) took too long to ' 'evaluate against zone name (%(zone_name)s)', { 'pattern': blacklist.pattern, 'zone_name': zone_name }) return True return False def _is_subzone(self, context, zone_name, pool_id): """ Ensures the provided zone_name is the subzone of an existing zone (checks across all tenants) """ context = context.elevated(all_tenants=True) # Break the name up into it's component labels labels = zone_name.split(".") criterion = {"pool_id": pool_id} i = 1 # Starting with label #2, search for matching zone's in the database while (i < len(labels)): name = '.'.join(labels[i:]) criterion["name"] = name try: zone = self.storage.find_zone(context, criterion) except exceptions.ZoneNotFound: i += 1 else: return zone return False def _is_superzone(self, context, zone_name, pool_id): """ Ensures the provided zone_name is the parent zone of an existing subzone (checks across all tenants) """ context = context.elevated(all_tenants=True) # Create wildcard term to catch all subzones search_term = "%%.%(name)s" % {"name": zone_name} criterion = {'name': search_term, "pool_id": pool_id} subzones = self.storage.find_zones(context, criterion) return subzones def _is_valid_ttl(self, context, ttl): if ttl is None: return min_ttl = CONF['service:central'].min_ttl if min_ttl is not None and ttl < int(min_ttl): try: policy.check('use_low_ttl', context) except exceptions.Forbidden: raise exceptions.InvalidTTL('TTL is below the minimum: %s' % min_ttl) def _is_valid_project_id(self, project_id): if project_id is None: raise exceptions.MissingProjectID( "A project ID must be specified when not using a project " "scoped token.") # SOA Recordset Methods @staticmethod def _build_soa_record(zone, ns_records): address = zone['email'] atsign = address.index('@') soa_address = (address[0:atsign].replace('.', '\\.') + '.' + address[atsign + 1:]) return '%s %s. %d %d %d %d %d' % ( ns_records[0]['hostname'], soa_address, zone['serial'], zone['refresh'], zone['retry'], zone['expire'], zone['minimum'] ) def _create_soa(self, context, zone): pool_ns_records = self._get_pool_ns_records(context, zone.pool_id) records = objects.RecordList(objects=[ objects.Record( data=self._build_soa_record(zone, pool_ns_records), managed=True ) ]) return self._create_recordset_in_storage( context, zone, objects.RecordSet( name=zone['name'], type='SOA', records=records ), increment_serial=False )[0] def _update_soa(self, context, zone): # NOTE: We should not be updating SOA records when a zone is SECONDARY. if zone.type == constants.ZONE_SECONDARY: return # Get the pool for it's list of ns_records pool_ns_records = self._get_pool_ns_records(context, zone.pool_id) soa = self.find_recordset( context, criterion={ 'zone_id': zone['id'], 'type': 'SOA' } ) soa.records[0].data = self._build_soa_record(zone, pool_ns_records) self._update_recordset_in_storage( context, zone, soa, increment_serial=False ) # NS Recordset Methods def _create_ns(self, context, zone, ns_records): # NOTE: We should not be creating NS records when a zone is SECONDARY. if zone.type != 'PRIMARY': return # Create an NS record for each server recordlist = objects.RecordList(objects=[ objects.Record(data=r, managed=True) for r in ns_records]) values = { 'name': zone['name'], 'type': 'NS', 'records': recordlist } ns, zone = self._create_recordset_in_storage( context, zone, objects.RecordSet(**values), increment_serial=False ) return ns def _add_ns(self, context, zone, ns_record): # Get NS recordset # If the zone doesn't have an NS recordset yet, create one try: recordset = self.find_recordset( context, criterion={ 'zone_id': zone['id'], 'name': zone['name'], 'type': 'NS' } ) except exceptions.RecordSetNotFound: self._create_ns(context, zone, [ns_record]) return # Add new record to recordset based on the new nameserver recordset.records.append( objects.Record(data=ns_record, managed=True) ) self._update_recordset_in_storage(context, zone, recordset, set_delayed_notify=True) def _delete_ns(self, context, zone, ns_record): recordset = self.find_recordset( context, criterion={ 'zone_id': zone['id'], 'name': zone['name'], 'type': 'NS' } ) for record in list(recordset.records): if record.data == ns_record: recordset.records.remove(record) self._update_recordset_in_storage(context, zone, recordset, set_delayed_notify=True) # Quota Enforcement Methods def _enforce_zone_quota(self, context, tenant_id): criterion = {'tenant_id': tenant_id} count = self.storage.count_zones(context, criterion) # Check if adding one more zone would exceed the quota self.quota.limit_check(context, tenant_id, zones=count + 1) def _enforce_recordset_quota(self, context, zone): # Ensure the recordsets per zone quota is OK criterion = {'zone_id': zone.id} count = self.storage.count_recordsets(context, criterion) # Check if adding one more recordset would exceed the quota self.quota.limit_check( context, zone.tenant_id, zone_recordsets=count + 1) def _enforce_record_quota(self, context, zone, recordset): # Quotas don't apply to managed records. if recordset.managed: return # Ensure the records per zone quota is OK zone_criterion = {