From 32f881974e12a49cadb119b2297c8b4e3d1f4f4a Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Tue, 4 Jan 2022 13:04:58 -0800 Subject: [PATCH] Extract and shim AzureProvider --- CHANGELOG.md | 1 + README.md | 2 +- octodns/provider/azuredns.py | 1511 +------------- tests/test_octodns_provider_azuredns.py | 2462 +---------------------- 4 files changed, 21 insertions(+), 3955 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c02e43..a8e4d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ https://github.com/octodns/octodns/pull/822 for more information. Providers that have been extracted in this release include: * [PowerDnsProvider](https://github.com/octodns/octodns-powerdns/) + * [AzureProvider](https://github.com/octodns/octodns-azure/) * NS1 provider has received improvements to the dynamic record implementation. As a result, if octoDNS is downgraded from this version, any dynamic records created or updated using this version will show an update. diff --git a/README.md b/README.md index b23fa98..6b417d7 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ The table below lists the providers octoDNS supports. We're currently in the pro | Provider | Module | Requirements | Record Support | Dynamic | Notes | |--|--|--|--|--|--| -| [AzureProvider](/octodns/provider/azuredns.py) | | azure-identity, azure-mgmt-dns, azure-mgmt-trafficmanager | A, AAAA, CAA, CNAME, MX, NS, PTR, SRV, TXT | Alpha (A, AAAA, CNAME) | | +| [AzureProvider](https://github.com/octodns/octodns-azure/) | [octodns_azure](https://github.com/octodns/octodns-azure/) | | | | | | [Akamai](/octodns/provider/edgedns.py) | | edgegrid-python | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TXT | No | | | [CloudflareProvider](/octodns/provider/cloudflare.py) | | | A, AAAA, ALIAS, CAA, CNAME, LOC, MX, NS, PTR, SPF, SRV, TXT, URLFWD | No | CAA tags restricted | | [ConstellixProvider](/octodns/provider/constellix.py) | | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | Yes | CAA tags restricted | diff --git a/octodns/provider/azuredns.py b/octodns/provider/azuredns.py index b057f43..d785adf 100644 --- a/octodns/provider/azuredns.py +++ b/octodns/provider/azuredns.py @@ -5,1500 +5,17 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from collections import defaultdict - -from azure.identity import ClientSecretCredential -from azure.common.credentials import ServicePrincipalCredentials -from azure.mgmt.dns import DnsManagementClient -from azure.mgmt.trafficmanager import TrafficManagerManagementClient - -from azure.mgmt.dns.models import ARecord, AaaaRecord, CaaRecord, \ - CnameRecord, MxRecord, SrvRecord, NsRecord, PtrRecord, TxtRecord, Zone -from azure.mgmt.trafficmanager.models import Profile, DnsConfig, \ - MonitorConfig, Endpoint, MonitorConfigCustomHeadersItem - -import logging -from functools import reduce -from ..record import Record, Update, GeoCodes -from . import ProviderException -from .base import BaseProvider - - -class AzureException(ProviderException): - pass - - -def escape_semicolon(s): - assert s - return s.replace(';', '\\;') - - -def unescape_semicolon(s): - assert s - return s.replace('\\;', ';') - - -def azure_chunked_value(val): - CHUNK_SIZE = 255 - val_replace = val.replace('"', '\\"') - value = unescape_semicolon(val_replace) - if len(val) > CHUNK_SIZE: - vs = [value[i:i + CHUNK_SIZE] - for i in range(0, len(value), CHUNK_SIZE)] - else: - vs = value - return vs - - -def azure_chunked_values(s): - values = [] - for v in s: - values.append(azure_chunked_value(v)) - return values - - -class _AzureRecord(object): - '''Wrapper for OctoDNS record for AzureProvider to make dns_client calls. - - azuredns.py: - class: octodns.provider.azuredns._AzureRecord - An _AzureRecord is easily accessible to Azure DNS Management library - functions and is used to wrap all relevant data to create a record in - Azure. - ''' - TYPE_MAP = { - 'A': ARecord, - 'AAAA': AaaaRecord, - 'CAA': CaaRecord, - 'CNAME': CnameRecord, - 'MX': MxRecord, - 'SRV': SrvRecord, - 'NS': NsRecord, - 'PTR': PtrRecord, - 'TXT': TxtRecord - } - - def __init__(self, resource_group, record, delete=False, - traffic_manager=None): - '''Constructor for _AzureRecord. - - Notes on Azure records: An Azure record set has the form - RecordSet(name=<...>, type=<...>, a_records=[...], - aaaa_records=[...], ...) - When constructing an azure record as done in self._apply_Create, - the argument parameters for an A record would be - parameters={'ttl': , 'a_records': [ARecord(),]}. - As another example for CNAME record: - parameters={'ttl': , 'cname_record': CnameRecord()}. - - Below, key_name and class_name are the dictionary key and Azure - Record class respectively. - - :param resource_group: The name of resource group in Azure - :type resource_group: str - :param record: An OctoDNS record - :type record: ..record.Record - :param delete: If true, omit data parsing; not needed to delete - :type delete: bool - - :type return: _AzureRecord - ''' - self.log = logging.getLogger('AzureRecord') - - self.resource_group = resource_group - self.zone_name = record.zone.name[:-1] - self.relative_record_set_name = record.name or '@' - self.record_type = record._type - self._record = record - self.traffic_manager = traffic_manager - - if delete: - return - - # Refer to function docstring for key_name and class_name. - key_name = f'{self.record_type}_records'.lower() - if record._type == 'CNAME': - key_name = key_name[:-1] - azure_class = self.TYPE_MAP[self.record_type] - - params_for = getattr(self, f'_params_for_{record._type}') - self.params = params_for(record.data, key_name, azure_class) - self.params['ttl'] = record.ttl - - def _params_for_A(self, data, key_name, azure_class): - if self._record.dynamic and self.traffic_manager: - return {'target_resource': self.traffic_manager} - - try: - values = data['values'] - except KeyError: - values = [data['value']] - return {key_name: [azure_class(ipv4_address=v) for v in values]} - - def _params_for_AAAA(self, data, key_name, azure_class): - if self._record.dynamic and self.traffic_manager: - return {'target_resource': self.traffic_manager} - - try: - values = data['values'] - except KeyError: - values = [data['value']] - return {key_name: [azure_class(ipv6_address=v) for v in values]} - - def _params_for_CAA(self, data, key_name, azure_class): - params = [] - if 'values' in data: - for vals in data['values']: - params.append(azure_class(flags=vals['flags'], - tag=vals['tag'], - value=vals['value'])) - else: # Else there is a singular data point keyed by 'value'. - params.append(azure_class(flags=data['value']['flags'], - tag=data['value']['tag'], - value=data['value']['value'])) - return {key_name: params} - - def _params_for_CNAME(self, data, key_name, azure_class): - if self._record.dynamic and self.traffic_manager: - return {'target_resource': self.traffic_manager} - - return {key_name: azure_class(cname=data['value'])} - - def _params_for_MX(self, data, key_name, azure_class): - params = [] - if 'values' in data: - for vals in data['values']: - params.append(azure_class(preference=vals['preference'], - exchange=vals['exchange'])) - else: # Else there is a singular data point keyed by 'value'. - params.append(azure_class(preference=data['value']['preference'], - exchange=data['value']['exchange'])) - return {key_name: params} - - def _params_for_SRV(self, data, key_name, azure_class): - params = [] - if 'values' in data: - for vals in data['values']: - params.append(azure_class(priority=vals['priority'], - weight=vals['weight'], - port=vals['port'], - target=vals['target'])) - else: # Else there is a singular data point keyed by 'value'. - params.append(azure_class(priority=data['value']['priority'], - weight=data['value']['weight'], - port=data['value']['port'], - target=data['value']['target'])) - return {key_name: params} - - def _params_for_NS(self, data, key_name, azure_class): - try: - values = data['values'] - except KeyError: - values = [data['value']] - return {key_name: [azure_class(nsdname=v) for v in values]} - - def _params_for_PTR(self, data, key_name, azure_class): - try: - values = data['values'] - except KeyError: - values = [data['value']] - return {key_name: [azure_class(ptrdname=v) for v in values]} - - def _params_for_TXT(self, data, key_name, azure_class): - - params = [] - try: # API for TxtRecord has list of str, even for singleton - values = [v for v in azure_chunked_values(data['values'])] - except KeyError: - values = [azure_chunked_value(data['value'])] - - for v in values: - if isinstance(v, list): - params.append(azure_class(value=v)) - else: - params.append(azure_class(value=[v])) - return {key_name: params} - - def _equals(self, b): - '''Checks whether two records are equal by comparing all fields. - :param b: Another _AzureRecord object - :type b: _AzureRecord - - :type return: bool - ''' - - def key_dict(d): - return sum([hash(f'{k}:{v}') for k, v in d.items()]) - - def parse_dict(params): - vals = [] - for char in params: - if char != 'ttl': - list_records = params[char] - try: - for record in list_records: - vals.append(record.__dict__) - except: - vals.append(list_records.__dict__) - vals.sort(key=key_dict) - return vals - - return (self.resource_group == b.resource_group) & \ - (self.zone_name == b.zone_name) & \ - (self.record_type == b.record_type) & \ - (self.params['ttl'] == b.params['ttl']) & \ - (parse_dict(self.params) == parse_dict(b.params)) & \ - (self.relative_record_set_name == b.relative_record_set_name) - - -def _check_endswith_dot(string): - return string if string.endswith('.') else string + '.' - - -def _parse_azure_type(string): - '''Converts string representing an Azure RecordSet type to usual type. - - :param string: the Azure type. eg: - :type string: str - - :type return: str - ''' - return string.split('/')[-1] - - -def _root_traffic_manager_name(record): - # ATM names can only have letters, numbers and hyphens - # replace dots with double hyphens to ensure unique mapping, - # hoping that real life FQDNs won't have double hyphens - name = record.fqdn[:-1].replace('.', '--') - if record._type != 'CNAME': - name += f'-{record._type}' - return name - - -def _rule_traffic_manager_name(pool, record): - prefix = _root_traffic_manager_name(record) - return f'{prefix}-rule-{pool}' - - -def _pool_traffic_manager_name(pool, record): - prefix = _root_traffic_manager_name(record) - return f'{prefix}-pool-{pool}' - - -def _healthcheck_tolerated_number_of_failures(record): - return record._octodns.get('azuredns', {}) \ - .get('healthcheck', {}) \ - .get('tolerated_number_of_failures') - - -def _healthcheck_interval_in_seconds(record): - return record._octodns.get('azuredns', {}) \ - .get('healthcheck', {}) \ - .get('interval_in_seconds') - - -def _healthcheck_timeout_in_seconds(record): - return record._octodns.get('azuredns', {}) \ - .get('healthcheck', {}) \ - .get('timeout_in_seconds') - - -def _get_monitor(record): - monitor = MonitorConfig( - protocol=record.healthcheck_protocol, - port=record.healthcheck_port, - path=record.healthcheck_path, - interval_in_seconds=_healthcheck_interval_in_seconds(record), - timeout_in_seconds=_healthcheck_timeout_in_seconds(record), - tolerated_number_of_failures= - _healthcheck_tolerated_number_of_failures(record), - ) - host = record.healthcheck_host() - if host: - monitor.custom_headers = [MonitorConfigCustomHeadersItem( - name='Host', value=host - )] - return monitor - - -def _check_valid_dynamic(record): - typ = record._type - if typ in ['A', 'AAAA']: - defaults = set(record.values) - if len(defaults) > 1: - pools = record.dynamic.pools - vals = set( - v['value'] - for _, pool in pools.items() - for v in pool._data()['values'] - ) - if defaults != vals: - # we don't yet support multi-value defaults, specifying all - # pool values allows for Traffic Manager profile optimization - raise AzureException(f'{record.fqdn} {record._type}: Values ' - 'of A/AAAA dynamic records must either ' - 'have a single value or contain all ' - 'values from all pools') - elif typ != 'CNAME': - # dynamic records of unsupported type - raise AzureException(f'{record.fqdn}: Dynamic records in Azure must ' - 'be of type A/AAAA/CNAME') - - -def _profile_is_match(have, desired): - if have is None or desired is None: - return False - - log = logging.getLogger('azuredns._profile_is_match').debug - - def false(have, desired, name=None): - prefix = f'profile={name}' if name else '' - attr = have.__class__.__name__ - log('%s have.%s = %s', prefix, attr, have) - log('%s desired.%s = %s', prefix, attr, desired) - return False - - # compare basic attributes - if have.name != desired.name or \ - have.traffic_routing_method != desired.traffic_routing_method or \ - len(have.endpoints) != len(desired.endpoints): - return false(have, desired) - - # compare dns config - dns_have = have.dns_config - dns_desired = desired.dns_config - if dns_have.ttl != dns_desired.ttl or \ - dns_have.relative_name is None or \ - dns_desired.relative_name is None or \ - dns_have.relative_name != dns_desired.relative_name: - return false(dns_have, dns_desired, have.name) - - # compare monitoring configuration - monitor_have = have.monitor_config - monitor_desired = desired.monitor_config - if monitor_have.protocol != monitor_desired.protocol or \ - monitor_have.port != monitor_desired.port or \ - monitor_have.path != monitor_desired.path or \ - monitor_have.tolerated_number_of_failures != \ - monitor_desired.tolerated_number_of_failures or \ - monitor_have.interval_in_seconds != \ - monitor_desired.interval_in_seconds or \ - monitor_have.timeout_in_seconds != \ - monitor_desired.timeout_in_seconds or \ - monitor_have.custom_headers != monitor_desired.custom_headers: - return false(monitor_have, monitor_desired, have.name) - - # compare endpoints - method = have.traffic_routing_method - if method == 'Priority': - have_endpoints = sorted(have.endpoints, key=lambda e: e.priority) - desired_endpoints = sorted(desired.endpoints, - key=lambda e: e.priority) - elif method == 'Weighted': - have_endpoints = sorted(have.endpoints, key=lambda e: e.target) - desired_endpoints = sorted(desired.endpoints, key=lambda e: e.target) - else: - have_endpoints = have.endpoints - desired_endpoints = desired.endpoints - endpoints = zip(have_endpoints, desired_endpoints) - for have_endpoint, desired_endpoint in endpoints: - have_status = have_endpoint.endpoint_status or 'Enabled' - desired_status = desired_endpoint.endpoint_status or 'Enabled' - - # compare basic attributes - if have_endpoint.name != desired_endpoint.name or \ - have_endpoint.type != desired_endpoint.type or \ - have_status != desired_status: - return false(have_endpoint, desired_endpoint, have.name) - - # compare geos - if method == 'Geographic': - have_geos = sorted(have_endpoint.geo_mapping) - desired_geos = sorted(desired_endpoint.geo_mapping) - if have_geos != desired_geos: - return false(have_endpoint, desired_endpoint, have.name) - - # compare priorities - if method == 'Priority' and \ - have_endpoint.priority != desired_endpoint.priority: - return false(have_endpoint, desired_endpoint, have.name) - - # compare weights - if method == 'Weighted' and \ - have_endpoint.weight != desired_endpoint.weight: - return false(have_endpoint, desired_endpoint, have.name) - - # compare targets - target_type = have_endpoint.type.split('/')[-1] - if target_type == 'externalEndpoints': - if have_endpoint.target != desired_endpoint.target: - return false(have_endpoint, desired_endpoint, have.name) - elif target_type == 'nestedEndpoints': - if have_endpoint.target_resource_id != \ - desired_endpoint.target_resource_id: - return false(have_endpoint, desired_endpoint, have.name) - else: - # unexpected, give up - return False - - return True - - -class AzureProvider(BaseProvider): - ''' - Azure DNS Provider - - azuredns.py: - class: octodns.provider.azuredns.AzureProvider - # Current support of authentication of access to Azure services only - # includes using a Service Principal: - # https://docs.microsoft.com/en-us/azure/azure-resource-manager/ - # resource-group-create-service-principal-portal - # The Azure Active Directory Application ID (aka client ID): - client_id: - # Authentication Key Value: (note this should be secret) - key: - # Directory ID (aka tenant ID): - directory_id: - # Subscription ID: - sub_id: - # Resource Group name: - resource_group: - # All are required to authenticate. - - Example config file with variables: - " - --- - providers: - config: - class: octodns.provider.yaml.YamlProvider - directory: ./config (example path to directory of zone files) - azuredns: - class: octodns.provider.azuredns.AzureProvider - client_id: env/AZURE_APPLICATION_ID - key: env/AZURE_AUTHENTICATION_KEY - directory_id: env/AZURE_DIRECTORY_ID - sub_id: env/AZURE_SUBSCRIPTION_ID - resource_group: 'TestResource1' - - zones: - example.com.: - sources: - - config - targets: - - azuredns - " - The first four variables above can be hidden in environment variables - and octoDNS will automatically search for them in the shell. It is - possible to also hard-code into the config file: eg, resource_group. - - Please read https://github.com/octodns/octodns/pull/706 for an overview - of how dynamic records are designed and caveats of using them. - ''' - SUPPORTS_GEO = False - SUPPORTS_DYNAMIC = True - SUPPORTS_POOL_VALUE_STATUS = True - SUPPORTS_MULTIVALUE_PTR = True - SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', - 'TXT')) - - def __init__(self, id, client_id, key, directory_id, sub_id, - resource_group, *args, **kwargs): - self.log = logging.getLogger(f'AzureProvider[{id}]') - self.log.debug('__init__: id=%s, client_id=%s, ' - 'key=***, directory_id:%s', id, client_id, directory_id) - super(AzureProvider, self).__init__(id, *args, **kwargs) - - # Store necessary initialization params - self._dns_client_handle = None - self._dns_client_client_id = client_id - self._dns_client_key = key - self._dns_client_directory_id = directory_id - self._dns_client_subscription_id = sub_id - self.__dns_client = None - self.__tm_client = None - - self._resource_group = resource_group - self._azure_zones = set() - self._traffic_managers = dict() - - @property - def _dns_client(self): - if self.__dns_client is None: - # Azure's logger spits out a lot of debug messages at 'INFO' - # level, override it by re-assigning `info` method to `debug` - # (ugly hack until I find a better way) - logger_name = 'azure.core.pipeline.policies.http_logging_policy' - logger = logging.getLogger(logger_name) - logger.info = logger.debug - self.__dns_client = DnsManagementClient( - credential=ClientSecretCredential( - client_id=self._dns_client_client_id, - client_secret=self._dns_client_key, - tenant_id=self._dns_client_directory_id, - logger=logger, - ), - subscription_id=self._dns_client_subscription_id, - ) - return self.__dns_client - - @property - def _tm_client(self): - if self.__tm_client is None: - self.__tm_client = TrafficManagerManagementClient( - ServicePrincipalCredentials( - self._dns_client_client_id, - secret=self._dns_client_key, - tenant=self._dns_client_directory_id, - ), - self._dns_client_subscription_id, - ) - return self.__tm_client - - def _populate_zones(self): - self.log.debug('azure_zones: loading') - list_zones = self._dns_client.zones.list_by_resource_group - for zone in list_zones(self._resource_group): - self._azure_zones.add(zone.name.rstrip('.')) - - def _check_zone(self, name, create=False): - '''Checks whether a zone specified in a source exist in Azure server. - - Note that Azure zones omit end '.' eg: contoso.com vs contoso.com. - Returns the name if it exists. - - :param name: Name of a zone to checks - :type name: str - :param create: If True, creates the zone of that name. - :type create: bool - - :type return: str or None - ''' - self.log.debug('_check_zone: name=%s create=%s', name, create) - # Check if the zone already exists in our set - if name in self._azure_zones: - return name - # If not, and its time to create, lets do it. - if create: - self.log.debug('_check_zone:no matching zone; creating %s', name) - create_zone = self._dns_client.zones.create_or_update - create_zone(self._resource_group, name, Zone(location='global')) - self._azure_zones.add(name) - return name - else: - # Else return nothing (aka false) - return - - def _populate_traffic_managers(self): - self.log.debug('traffic managers: loading') - list_profiles = self._tm_client.profiles.list_by_resource_group - for profile in list_profiles(self._resource_group): - self._traffic_managers[profile.id] = profile - # link nested profiles in advance for convenience - for _, profile in self._traffic_managers.items(): - self._populate_nested_profiles(profile) - - def _populate_nested_profiles(self, profile): - for ep in profile.endpoints: - target_id = ep.target_resource_id - if target_id and target_id in self._traffic_managers: - target = self._traffic_managers[target_id] - ep.target_resource = self._populate_nested_profiles(target) - return profile - - def _get_tm_profile_by_id(self, resource_id): - if not self._traffic_managers: - self._populate_traffic_managers() - return self._traffic_managers.get(resource_id) - - def _profile_name_to_id(self, name): - return '/subscriptions/' + self._dns_client_subscription_id + \ - '/resourceGroups/' + self._resource_group + \ - '/providers/Microsoft.Network/trafficManagerProfiles/' + \ - name - - def _get_tm_profile_by_name(self, name): - profile_id = self._profile_name_to_id(name) - return self._get_tm_profile_by_id(profile_id) - - def _get_tm_for_dynamic_record(self, record): - name = _root_traffic_manager_name(record) - return self._get_tm_profile_by_name(name) - - def populate(self, zone, target=False, lenient=False): - '''Required function of manager.py to collect records from zone. - - Special notes for Azure. - Azure zone names omit final '.' - Azure root records names are represented by '@'. OctoDNS uses '' - Azure records created through online interface may have null values - (eg, no IP address for A record). - Azure online interface allows constructing records with null values - which are destroyed by _apply. - - Specific quirks such as these are responsible for any non-obvious - parsing in this function and the functions '_params_for_*'. - - :param zone: A dns zone - :type zone: octodns.zone.Zone - :param target: Checks if Azure is source or target of config. - Currently only supports as a target. Unused. - :type target: bool - :param lenient: Unused. Check octodns.manager for usage. - :type lenient: bool - - :type return: void - ''' - self.log.debug('populate: name=%s', zone.name) - - exists = False - before = len(zone.records) - - zone_name = zone.name[:-1] - self._populate_zones() - - records = self._dns_client.record_sets.list_by_dns_zone - if self._check_zone(zone_name): - exists = True - for azrecord in records(self._resource_group, zone_name): - typ = _parse_azure_type(azrecord.type) - if typ not in self.SUPPORTS: - continue - - record = self._populate_record(zone, azrecord, lenient) - zone.add_record(record, lenient=lenient) - - self.log.info('populate: found %s records, exists=%s', - len(zone.records) - before, exists) - return exists - - def _populate_record(self, zone, azrecord, lenient=False): - record_name = azrecord.name if azrecord.name != '@' else '' - typ = _parse_azure_type(azrecord.type) - - data_for = getattr(self, f'_data_for_{typ}') - data = data_for(azrecord) - data['type'] = typ - data['ttl'] = azrecord.ttl - return Record.new(zone, record_name, data, source=self, - lenient=lenient) - - def _data_for_A(self, azrecord): - if azrecord.a_records is None: - if azrecord.target_resource.id: - return self._data_for_dynamic(azrecord) - - # dynamic record alias is broken, return dummy value and apply - # will likely overwrite/fix it - self.log.warn('_data_for_A: Missing Traffic Manager alias for ' - 'dynamic record %s', azrecord.fqdn) - return {'values': []} - - return {'values': [ar.ipv4_address for ar in azrecord.a_records]} - - def _data_for_AAAA(self, azrecord): - if azrecord.aaaa_records is None: - if azrecord.target_resource.id: - return self._data_for_dynamic(azrecord) - - # dynamic record alias is broken, return dummy value and apply - # will likely overwrite/fix it - self.log.warn('_data_for_AAAA: Missing Traffic Manager alias for ' - 'dynamic record %s', azrecord.fqdn) - return {'values': []} - - return {'values': [ar.ipv6_address for ar in azrecord.aaaa_records]} - - def _data_for_CAA(self, azrecord): - return {'values': [{'flags': ar.flags, - 'tag': ar.tag, - 'value': ar.value} - for ar in azrecord.caa_records]} - - def _data_for_CNAME(self, azrecord): - '''Parsing data from Azure DNS Client record call - :param azrecord: a return of a call to list azure records - :type azrecord: azure.mgmt.dns.models.RecordSet - - :type return: dict - ''' - if azrecord.cname_record is None: - if azrecord.target_resource.id: - return self._data_for_dynamic(azrecord) - - # dynamic record alias is broken, return dummy value and apply - # will likely overwrite/fix it - self.log.warn('_data_for_CNAME: Missing Traffic Manager alias for ' - 'dynamic record %s', azrecord.fqdn) - return {'value': None} - - return {'value': _check_endswith_dot(azrecord.cname_record.cname)} - - def _data_for_MX(self, azrecord): - return {'values': [{'preference': ar.preference, - 'exchange': ar.exchange} - for ar in azrecord.mx_records]} - - def _data_for_NS(self, azrecord): - vals = [ar.nsdname for ar in azrecord.ns_records] - return {'values': [_check_endswith_dot(val) for val in vals]} - - def _data_for_PTR(self, azrecord): - vals = [ar.ptrdname for ar in azrecord.ptr_records] - return {'values': [_check_endswith_dot(val) for val in vals]} - - def _data_for_SRV(self, azrecord): - return {'values': [{'priority': ar.priority, 'weight': ar.weight, - 'port': ar.port, 'target': ar.target} - for ar in azrecord.srv_records]} - - def _data_for_TXT(self, azrecord): - return {'values': [escape_semicolon(reduce((lambda a, b: a + b), - ar.value)) - for ar in azrecord.txt_records]} - - def _get_geo_endpoints(self, root_profile): - if root_profile.traffic_routing_method != 'Geographic': - # This record does not use geo fencing, so we skip the Geographic - # profile hop; let's pretend to be a geo-profile's only endpoint - geo_ep = Endpoint( - name=root_profile.endpoints[0].name.split('--', 1)[0], - target_resource_id=root_profile.id - ) - geo_ep.target_resource = root_profile - return [geo_ep] - - return root_profile.endpoints - - def _get_rule_endpoints(self, geo_ep): - if geo_ep.target_resource_id and \ - geo_ep.target_resource.traffic_routing_method == 'Priority': - return sorted( - geo_ep.target_resource.endpoints, key=lambda e: e.priority) - else: - # this geo directly points to a pool containing the default - # so we skip the Priority profile hop and directly use an - # external endpoint or Weighted profile - # let's pretend to be a Priority profile's only endpoint - return [geo_ep] - - def _get_pool_endpoints(self, rule_ep): - if rule_ep.target_resource_id: - # third (and last) level weighted RR profile - return rule_ep.target_resource.endpoints - else: - # single-value pool, so we skip the Weighted profile hop and - # directly use an external endpoint; let's pretend to be a - # Weighted profile's only endpoint - return [rule_ep] - - def _populate_geos(self, geo_map, name, fqdn): - if 'GEO-ME' in geo_map: - # Azure treats Middle East as a separate group, but its part of - # Asia in octoDNS, so we need to remove GEO-ME if GEO-AS is also - # in the list. Throw exception otherwise, which should not happen - # if the profile was generated by octoDNS. - if 'GEO-AS' not in geo_map: - msg = f'Profile={name} for record {fqdn}: Middle East ' \ - '(GEO-ME) is not supported by octoDNS. It needs to be ' \ - 'either paired with Asia (GEO-AS) or expanded into ' \ - 'individual list of countries.' - raise AzureException(msg) - geo_map.remove('GEO-ME') - - geos = [] - for code in geo_map: - if code.startswith('GEO-'): - # continent - if code == 'GEO-AP': - # Azure uses Australia/Pacific (AP) instead of Oceania - # https://docs.microsoft.com/en-us/azure/traffic-manager/ - # traffic-manager-geographic-regions - geos.append('OC') - else: - geos.append(code[len('GEO-'):]) - elif '-' in code: - # state - country, province = code.split('-', 1) - country = GeoCodes.country_to_code(country) - geos.append(f'{country}-{province}') - elif code == 'WORLD': - geos.append(code) - else: - # country - geos.append(GeoCodes.country_to_code(code)) - - return geos - - def _populate_pool_values(self, rule_ep, typ, defaults): - values = [] - for pool_ep in self._get_pool_endpoints(rule_ep): - val = pool_ep.target - if typ == 'CNAME': - val = _check_endswith_dot(val) - - ep_name = pool_ep.name - if ep_name.endswith('--default--'): - defaults.add(val) - ep_name = ep_name[:-len('--default--')] - - status = 'obey' - if pool_ep.endpoint_status == 'Disabled': - status = 'down' - - values.append({ - 'value': val, - 'weight': pool_ep.weight or 1, - 'status': status, - }) - - return values - - def _populate_pools(self, geo_ep, typ, defaults, pools): - rule_endpoints = self._get_rule_endpoints(geo_ep) - rule_pool = None - pool = None - for rule_ep in rule_endpoints: - pool_name = rule_ep.name - - # last/default pool - if pool_name.endswith('--default--'): - defaults.add(rule_ep.target) - if pool_name == '--default--': - # this should be the last one, so let's break here - break - # last pool is a single value pool and its value is same as - # record's default value - pool_name = pool_name[:-len('--default--')] - - # set first priority endpoint as the rule's primary pool - if rule_pool is None: - rule_pool = pool_name - - if pool: - # set current pool as fallback of the previous pool - pool['fallback'] = pool_name - - if pool_name in pools: - # we've already populated this and subsequent pools - break - - # populate the pool from Weighted profile - # these should be leaf node entries with no further nesting - pool = pools[pool_name] - pool['values'] = self._populate_pool_values(rule_ep, typ, defaults) - - return rule_pool - - def _data_for_dynamic(self, azrecord): - typ = _parse_azure_type(azrecord.type) - defaults = set() - pools = defaultdict(lambda: {'fallback': None, 'values': []}) - rules = [] - - # top level profile - root_profile = self._get_tm_profile_by_id(azrecord.target_resource.id) - - # construct rules and, in turn, pools - for geo_ep in self._get_geo_endpoints(root_profile): - rule = {} - - # resolve list of regions - geo_map = list(geo_ep.geo_mapping or []) - if geo_map and geo_map != ['WORLD']: - rule['geos'] = self._populate_geos( - geo_map, root_profile.name, azrecord.fqdn) - - # build pool fallback chain from second level priority profile - rule['pool'] = self._populate_pools(geo_ep, typ, defaults, pools) - - rules.append(rule) - - # add separate rule for re-used world pool - for rule in list(rules): - geos = rule.get('geos', []) - if len(geos) > 1 and 'WORLD' in geos: - geos.remove('WORLD') - rules.append({'pool': rule['pool']}) - - # Order and convert to a list - defaults = sorted(defaults) - - data = { - 'dynamic': { - 'pools': pools, - 'rules': rules, - }, - } - - if typ == 'CNAME': - data['value'] = _check_endswith_dot(defaults[0]) - else: - data['values'] = defaults - - return data - - def _process_desired_zone(self, desired): - # check for status=up values - for record in desired.records: - if not getattr(record, 'dynamic', False): - continue - - up_pools = [] - for name, pool in record.dynamic.pools.items(): - for value in pool.data['values']: - if value['status'] == 'up': - # Azure only supports obey and down, not up - up_pools.append(name) - break - if not up_pools: - continue - - up_pools = ','.join(up_pools) - msg = f'status=up is not supported for pools {up_pools} in ' \ - f'{record.fqdn}' - fallback = 'will ignore it and respect the healthcheck' - self.supports_warn_or_except(msg, fallback) - - record = record.copy() - for pool in record.dynamic.pools.values(): - for value in pool.data['values']: - if value['status'] == 'up': - value['status'] = 'obey' - desired.add_record(record, replace=True) - - return super()._process_desired_zone(desired) - - def _extra_changes(self, existing, desired, changes): - changed = set(c.record for c in changes) - - log = self.log.info - seen_profiles = {} - extra = [] - for record in desired.records: - if not getattr(record, 'dynamic', False): - # Already changed, or not dynamic, no need to check it - continue - - # Abort if there are unsupported dynamic record configurations - _check_valid_dynamic(record) - - # let's walk through and show what will be changed even if - # the record is already in list of changes - added = (record in changed) - - active = set() - profiles = self._generate_traffic_managers(record) - - for profile in profiles: - name = profile.name - - endpoints = set() - for ep in profile.endpoints: - if not ep.target: - continue - if ep.target in endpoints: - raise AzureException(f'{name} contains duplicate ' - f'endpoint {ep.target}') - endpoints.add(ep.target) - - if name in seen_profiles: - # exit if a possible collision is detected, even though - # we've tried to ensure unique mapping - raise AzureException('Collision in Traffic Manager names ' - f'detected: {seen_profiles[name]} ' - f'and {record.fqdn} both want to ' - f'use {name}') - else: - seen_profiles[name] = record.fqdn - - active.add(name) - existing_profile = self._get_tm_profile_by_name(name) - if not _profile_is_match(existing_profile, profile): - log('_extra_changes: Profile name=%s will be synced', - name) - if not added: - extra.append(Update(record, record)) - added = True - - existing_profiles = self._find_traffic_managers(record) - for name in existing_profiles - active: - log('_extra_changes: Profile name=%s will be destroyed', name) - if not added: - extra.append(Update(record, record)) - added = True - - return extra - - def _generate_tm_profile(self, routing, endpoints, record, label=None): - # figure out profile name and Traffic Manager FQDN - name = _root_traffic_manager_name(record) - if routing == 'Weighted' and label: - name = _pool_traffic_manager_name(label, record) - elif routing == 'Priority' and label: - name = _rule_traffic_manager_name(label, record) - - # set appropriate endpoint types - endpoint_type_prefix = 'Microsoft.Network/trafficManagerProfiles/' - for ep in endpoints: - if ep.target_resource_id: - ep.type = endpoint_type_prefix + 'nestedEndpoints' - elif ep.target: - ep.type = endpoint_type_prefix + 'externalEndpoints' - else: - raise AzureException(f'Invalid endpoint {ep.name} in profile ' - f'{name}, needs to have either target ' - 'or target_resource_id') - - # build and return - return Profile( - id=self._profile_name_to_id(name), - name=name, - traffic_routing_method=routing, - dns_config=DnsConfig( - relative_name=name.lower(), - ttl=record.ttl, - ), - monitor_config=_get_monitor(record), - endpoints=endpoints, - location='global', - ) - - def _convert_tm_to_root(self, profile, record): - profile.name = _root_traffic_manager_name(record) - profile.id = self._profile_name_to_id(profile.name) - profile.dns_config.relative_name = profile.name.lower() - - return profile - - def _make_azure_geos(self, rule_geos): - geos = [] - for geo in rule_geos: - if '-' in geo: - # country/state - geos.append(geo.split('-', 1)[-1]) - else: - # continent - if geo == 'AS': - # Middle East is part of Asia in octoDNS, but Azure treats - # it as a separate "group", so let's add it in the list of - # geo mappings. We will drop it when we later parse the - # list of regions. - geos.append('GEO-ME') - elif geo == 'OC': - # Azure uses Australia/Pacific (AP) instead of Oceania - geo = 'AP' - - geos.append(f'GEO-{geo}') - - return geos - - def _make_pool_profile(self, pool, record, defaults): - pool_name = pool._id - default_seen = False - - endpoints = [] - for val in pool.data['values']: - target = val['value'] - # strip trailing dot from CNAME value - if record._type == 'CNAME': - target = target[:-1] - ep_name = f'{pool_name}--{target}' - # Endpoint names cannot have colons, drop them from IPv6 addresses - ep_name = ep_name.replace(':', '-') - if target in defaults: - # mark default - ep_name += '--default--' - default_seen = True - ep_status = 'Disabled' if val['status'] == 'down' else \ - 'Enabled' - endpoints.append(Endpoint( - name=ep_name, - target=target, - weight=val.get('weight', 1), - endpoint_status=ep_status, - )) - - pool_profile = self._generate_tm_profile( - 'Weighted', endpoints, record, pool_name) - - return pool_profile, default_seen - - def _make_pool(self, pool, priority, pool_profiles, record, defaults, - traffic_managers): - pool_name = pool._id - pool_values = pool.data['values'] - default_seen = False - - if len(pool_values) > 1: - # create Weighted profile for multi-value pool - pool_profile = pool_profiles.get(pool_name) - # TODO: what if a cached pool_profile had seen the default - if pool_profile is None: - pool_profile, default_seen = self._make_pool_profile( - pool, record, defaults) - traffic_managers.append(pool_profile) - pool_profiles[pool_name] = pool_profile - - # append pool to endpoint list of fallback rule profile - return Endpoint( - name=pool_name, - target_resource_id=pool_profile.id, - priority=priority, - ), default_seen - else: - # Skip Weighted profile hop for single-value pool; append its - # value as an external endpoint to fallback rule profile - value = pool_values[0] - target = value['value'] - if record._type == 'CNAME': - target = target[:-1] - ep_name = pool_name - if target in defaults: - # mark default - ep_name += '--default--' - default_seen = True - ep_status = 'Disabled' if value['status'] == 'down' else \ - 'Enabled' - return Endpoint( - name=ep_name, - target=target, - priority=priority, - endpoint_status=ep_status, - ), default_seen - - def _make_rule_profile(self, rule_endpoints, rule_name, record, geos, - traffic_managers): - if len(rule_endpoints) > 1: - # create rule profile with fallback chain - rule_profile = self._generate_tm_profile( - 'Priority', rule_endpoints, record, rule_name) - traffic_managers.append(rule_profile) - - # append rule profile to top-level geo profile - return Endpoint( - name=rule_name, - target_resource_id=rule_profile.id, - geo_mapping=geos, - ) - else: - # Priority profile has only one endpoint; skip the hop and append - # its only endpoint to the top-level profile - rule_ep = rule_endpoints[0] - if rule_ep.target_resource_id: - # point directly to the Weighted pool profile - return Endpoint( - name=rule_ep.name, - target_resource_id=rule_ep.target_resource_id, - geo_mapping=geos, - ) - else: - # just add the value of single-value pool - return Endpoint( - name=rule_ep.name, - target=rule_ep.target, - geo_mapping=geos, - ) - - def _make_rule(self, pool_name, pool_profiles, record, geos, - traffic_managers): - endpoints = [] - rule_name = pool_name - - if record._type == 'CNAME': - defaults = [record.value[:-1]] - else: - defaults = record.values - - priority = 1 - default_seen = False - - while pool_name: - # iterate until we reach end of fallback chain - pool = record.dynamic.pools[pool_name] - - rule_ep, saw_default = self._make_pool( - pool, priority, pool_profiles, record, defaults, - traffic_managers - ) - endpoints.append(rule_ep) - if saw_default: - default_seen = True - - priority += 1 - pool_name = pool.data.get('fallback') - - # append default endpoint unless it is already included in last pool - # of rule profile - if not default_seen: - endpoints.append(Endpoint( - name='--default--', - target=defaults[0], - priority=priority, - )) - - return self._make_rule_profile( - endpoints, rule_name, record, geos, traffic_managers - ) - - def _make_geo_rules(self, record): - rules = record.dynamic.rules - - # a pool can be re-used only with a world pool, record the pool - # to later consolidate it with a geo pool if one exists since we - # can't have multiple endpoints with the same target in ATM - world_pool = None - for rule in rules: - if not rule.data.get('geos', []): - world_pool = rule.data['pool'] - - traffic_managers = [] - geo_endpoints = [] - pool_profiles = {} - world_seen = False - - for rule in rules: - rule = rule.data - pool_name = rule['pool'] - rule_geos = rule.get('geos', []) - - if pool_name == world_pool and world_seen: - # this world pool is already mentioned in another geo rule - continue - - # Prepare the list of Traffic manager geos - geos = self._make_azure_geos(rule_geos) - if not geos or pool_name == world_pool: - geos.append('WORLD') - world_seen = True - - geo_endpoints.append(self._make_rule( - pool_name, pool_profiles, record, geos, traffic_managers - )) - - return geo_endpoints, traffic_managers - - def _generate_traffic_managers(self, record): - geo_endpoints, traffic_managers = self._make_geo_rules(record) - - if len(geo_endpoints) == 1 and \ - geo_endpoints[0].geo_mapping == ['WORLD'] and \ - geo_endpoints[0].target_resource_id: - # Single WORLD rule does not require a Geographic profile, use the - # target profile (which is at the end) as the root profile - self._convert_tm_to_root(traffic_managers[-1], record) - else: - geo_profile = self._generate_tm_profile( - 'Geographic', geo_endpoints, record) - traffic_managers.append(geo_profile) - - return traffic_managers - - def _sync_traffic_managers(self, desired_profiles): - seen = set() - - tm_sync = self._tm_client.profiles.create_or_update - populate = self._populate_nested_profiles - - for desired in desired_profiles: - name = desired.name - if name in seen: - continue - - existing = self._get_tm_profile_by_name(name) - if not _profile_is_match(existing, desired): - self.log.info( - '_sync_traffic_managers: Syncing profile=%s', name) - profile = tm_sync(self._resource_group, name, desired) - self._traffic_managers[profile.id] = populate(profile) - else: - self.log.debug( - '_sync_traffic_managers: Skipping profile=%s: up to date', - name) - seen.add(name) - - return seen - - def _find_traffic_managers(self, record): - tm_prefix = _root_traffic_manager_name(record) - - profiles = set() - for profile_id in self._traffic_managers: - # match existing profiles with record's prefix - name = profile_id.split('/')[-1] - if name == tm_prefix or \ - name.startswith(f'{tm_prefix}-pool-') or \ - name.startswith(f'{tm_prefix}-rule-'): - profiles.add(name) - - return profiles - - def _traffic_managers_gc(self, record, active_profiles): - existing_profiles = self._find_traffic_managers(record) - - # delete unused profiles - for profile_name in existing_profiles - active_profiles: - self.log.info('_traffic_managers_gc: Deleting profile=%s', - profile_name) - self._tm_client.profiles.delete(self._resource_group, profile_name) - - def _apply_Create(self, change): - '''A record from change must be created. - - :param change: a change object - :type change: octodns.record.Change - - :type return: void - ''' - record = change.new - - dynamic = getattr(record, 'dynamic', False) - root_profile = None - endpoints = [] - if dynamic: - profiles = self._generate_traffic_managers(record) - root_profile = profiles[-1] - if record._type in ['A', 'AAAA'] and len(profiles) > 1: - # A/AAAA records cannot be aliased to Traffic Managers that - # contain other nested Traffic Managers. To work around this - # limitation, we remove nesting before adding the record, and - # then add the nested endpoints later. - endpoints = root_profile.endpoints - root_profile.endpoints = [] - self._sync_traffic_managers(profiles) - - ar = _AzureRecord(self._resource_group, record, - traffic_manager=root_profile) - create = self._dns_client.record_sets.create_or_update - - create(resource_group_name=ar.resource_group, - zone_name=ar.zone_name, - relative_record_set_name=ar.relative_record_set_name, - record_type=ar.record_type, - parameters=ar.params) - - if endpoints: - # add nested endpoints for A/AAAA dynamic record limitation after - # record creation - root_profile.endpoints = endpoints - self._sync_traffic_managers([root_profile]) - - self.log.debug('* Success Create: %s', record) - - def _apply_Update(self, change): - '''A record from change must be created. - - :param change: a change object - :type change: octodns.record.Change - - :type return: void - ''' - existing = change.existing - new = change.new - existing_is_dynamic = getattr(existing, 'dynamic', False) - new_is_dynamic = getattr(new, 'dynamic', False) - - update_record = True - - if new_is_dynamic: - endpoints = [] - profiles = self._generate_traffic_managers(new) - root_profile = profiles[-1] - - if new._type in ['A', 'AAAA']: - if existing_is_dynamic: - # update to the record is not needed - update_record = False - elif len(profiles) > 1: - # record needs to aliased; remove nested endpoints, we - # will add them at the end - endpoints = root_profile.endpoints - root_profile.endpoints = [] - elif existing.ttl == new.ttl and existing_is_dynamic: - # CNAME dynamic records only have TTL in them, everything else - # goes inside the aliased traffic managers; skip update if TTL - # is unchanged and existing record is already aliased to its - # traffic manager - update_record = False - - active = self._sync_traffic_managers(profiles) - - if update_record: - profile = self._get_tm_for_dynamic_record(new) - ar = _AzureRecord(self._resource_group, new, - traffic_manager=profile) - update = self._dns_client.record_sets.create_or_update - - update(resource_group_name=ar.resource_group, - zone_name=ar.zone_name, - relative_record_set_name=ar.relative_record_set_name, - record_type=ar.record_type, - parameters=ar.params) - - if new_is_dynamic: - # add any pending nested endpoints - if endpoints: - root_profile.endpoints = endpoints - self._sync_traffic_managers([root_profile]) - # let's cleanup unused traffic managers - self._traffic_managers_gc(new, active) - elif existing_is_dynamic: - # cleanup traffic managers when a dynamic record gets - # changed to a simple record - self._traffic_managers_gc(existing, set()) - - self.log.debug('* Success Update: %s', new) - - def _apply_Delete(self, change): - '''A record from change must be deleted. - - :param change: a change object - :type change: octodns.record.Change - - :type return: void - ''' - record = change.record - ar = _AzureRecord(self._resource_group, record, delete=True) - delete = self._dns_client.record_sets.delete - - delete(self._resource_group, ar.zone_name, ar.relative_record_set_name, - ar.record_type) - - if getattr(record, 'dynamic', False): - self._traffic_managers_gc(record, set()) - - self.log.debug('* Success Delete: %s', record) - - def _apply(self, plan): - '''Required function of manager.py to actually apply a record change. - - :param plan: Contains the zones and changes to be made - :type plan: octodns.provider.base.Plan - - :type return: void - ''' - desired = plan.desired - changes = plan.changes - self.log.debug('_apply: zone=%s, len(changes)=%d', desired.name, - len(changes)) - - azure_zone_name = desired.name[:len(desired.name) - 1] - self._check_zone(azure_zone_name, create=True) - - ''' - Force the operation order to be Delete() before all other operations. - Helps avoid problems in updating - - a CNAME record into an A record. - - an A record into a CNAME record. - ''' - - for change in changes: - class_name = change.__class__.__name__ - if class_name == 'Delete': - self._apply_Delete(change) - - for change in changes: - class_name = change.__class__.__name__ - if class_name != 'Delete': - getattr(self, f'_apply_{class_name}')(change) +from logging import getLogger + +logger = getLogger('Azure') +try: + logger.warn('octodns_azure shimmed. Update your provider class to ' + 'octodns_azure.AzureProvider. ' + 'Shim will be removed in 1.0') + from octodns_azure import AzureProvider + AzureProvider # pragma: no cover +except ModuleNotFoundError: + logger.exception('AzureProvider has been moved into a seperate module, ' + 'octodns_azure is now required. Provider class should ' + 'be updated to octodns_azure.AzureProvider') + raise diff --git a/tests/test_octodns_provider_azuredns.py b/tests/test_octodns_provider_azuredns.py index 60b1386..4990ad3 100644 --- a/tests/test_octodns_provider_azuredns.py +++ b/tests/test_octodns_provider_azuredns.py @@ -5,2464 +5,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from octodns.record import Create, Update, Delete, Record -from octodns.provider.azuredns import _AzureRecord, AzureProvider, \ - _check_endswith_dot, _parse_azure_type, _root_traffic_manager_name, \ - _get_monitor, _profile_is_match, AzureException -from octodns.zone import Zone -from octodns.provider.base import Plan - -from azure.mgmt.dns.models import ARecord, AaaaRecord, CaaRecord, \ - CnameRecord, MxRecord, SrvRecord, NsRecord, PtrRecord, TxtRecord, \ - RecordSet, SoaRecord, SubResource, Zone as AzureZone -from azure.mgmt.trafficmanager.models import Profile, DnsConfig, \ - MonitorConfig, Endpoint, MonitorConfigCustomHeadersItem -from msrestazure.azure_exceptions import CloudError - from unittest import TestCase -from mock import Mock, patch, call -zone = Zone(name='unit.tests.', sub_zones=[]) -octo_records = [] -octo_records.append(Record.new(zone, '', { - 'ttl': 0, - 'type': 'A', - 'values': ['1.2.3.4', '10.10.10.10']})) -octo_records.append(Record.new(zone, 'a', { - 'ttl': 1, - 'type': 'A', - 'values': ['1.2.3.4', '1.1.1.1']})) -octo_records.append(Record.new(zone, 'aa', { - 'ttl': 9001, - 'type': 'A', - 'values': ['1.2.4.3']})) -octo_records.append(Record.new(zone, 'aaa', { - 'ttl': 2, - 'type': 'A', - 'values': ['1.1.1.3']})) -octo_records.append(Record.new(zone, 'aaaa1', { - 'ttl': 300, - 'type': 'AAAA', - 'values': ['2601:644:500:e210:62f8:1dff:feb8:947a', - '2601:642:500:e210:62f8:1dff:feb8:947a'], -})) -octo_records.append(Record.new(zone, 'aaaa2', { - 'ttl': 300, - 'type': 'AAAA', - 'value': '2601:644:500:e210:62f8:1dff:feb8:947a' -})) -octo_records.append(Record.new(zone, 'caa1', { - 'ttl': 9, - 'type': 'CAA', - 'value': { - 'flags': 0, - 'tag': 'issue', - 'value': 'ca.unit.tests', - }})) -octo_records.append(Record.new(zone, 'caa2', { - 'ttl': 9, - 'type': 'CAA', - 'values': [{ - 'flags': 0, - 'tag': 'issue', - 'value': 'ca1.unit.tests', - }, { - 'flags': 0, - 'tag': 'issue', - 'value': 'ca2.unit.tests', - }]})) -octo_records.append(Record.new(zone, 'cname', { - 'ttl': 3, - 'type': 'CNAME', - 'value': 'a.unit.tests.'})) -octo_records.append(Record.new(zone, 'mx1', { - 'ttl': 3, - 'type': 'MX', - 'values': [{ - 'priority': 10, - 'value': 'mx1.unit.tests.', - }, { - 'priority': 20, - 'value': 'mx2.unit.tests.', - }]})) -octo_records.append(Record.new(zone, 'mx2', { - 'ttl': 3, - 'type': 'MX', - 'values': [{ - 'priority': 10, - 'value': 'mx1.unit.tests.', - }]})) -octo_records.append(Record.new(zone, '', { - 'ttl': 4, - 'type': 'NS', - 'values': ['ns1.unit.tests.', 'ns2.unit.tests.']})) -octo_records.append(Record.new(zone, 'foo', { - 'ttl': 5, - 'type': 'NS', - 'value': 'ns1.unit.tests.'})) -octo_records.append(Record.new(zone, 'ptr1', { - 'ttl': 5, - 'type': 'PTR', - 'value': 'ptr1.unit.tests.'})) -octo_records.append(Record.new(zone, '_srv._tcp', { - 'ttl': 6, - 'type': 'SRV', - 'values': [{ - 'priority': 10, - 'weight': 20, - 'port': 30, - 'target': 'foo-1.unit.tests.', - }, { - 'priority': 12, - 'weight': 30, - 'port': 30, - 'target': 'foo-2.unit.tests.', - }]})) -octo_records.append(Record.new(zone, '_srv2._tcp', { - 'ttl': 7, - 'type': 'SRV', - 'values': [{ - 'priority': 12, - 'weight': 17, - 'port': 1, - 'target': 'srvfoo.unit.tests.', - }]})) -octo_records.append(Record.new(zone, 'txt1', { - 'ttl': 8, - 'type': 'TXT', - 'value': 'txt singleton test'})) -octo_records.append(Record.new(zone, 'txt2', { - 'ttl': 9, - 'type': 'TXT', - 'values': ['txt multiple test', 'txt multiple test 2']})) +class TestAzureShim(TestCase): -long_txt = "v=spf1 ip4:10.10.0.0/24 ip4:10.10.1.0/24 ip4:10.10.2.0/24" -long_txt += " ip4:10.10.3.0/24 ip4:10.10.4.0/24 ip4:10.10.5.0/24 " -long_txt += " 10.6.0/24 ip4:10.10.7.0/24 ip4:10.10.8.0/24 " -long_txt += " ip4:10.10.10.0/24 ip4:10.10.11.0/24 ip4:10.10.12.0/24" -long_txt += " ip4:10.10.13.0/24 ip4:10.10.14.0/24 ip4:10.10.15.0/24" -long_txt += " ip4:10.10.16.0/24 ip4:10.10.17.0/24 ip4:10.10.18.0/24" -long_txt += " ip4:10.10.19.0/24 ip4:10.10.20.0/24 ~all" -octo_records.append(Record.new(zone, 'txt3', { - 'ttl': 10, - 'type': 'TXT', - 'values': ['txt multiple test', long_txt]})) - -octo_records.append(Record.new(zone, 'ptr2', { - 'ttl': 11, - 'type': 'PTR', - 'values': ['ptr21.unit.tests.', 'ptr22.unit.tests.']})) - -azure_records = [] -_base0 = _AzureRecord('TestAzure', octo_records[0]) -_base0.zone_name = 'unit.tests' -_base0.relative_record_set_name = '@' -_base0.record_type = 'A' -_base0.params['ttl'] = 0 -_base0.params['a_records'] = [ARecord(ipv4_address='1.2.3.4'), - ARecord(ipv4_address='10.10.10.10')] -azure_records.append(_base0) - -_base1 = _AzureRecord('TestAzure', octo_records[1]) -_base1.zone_name = 'unit.tests' -_base1.relative_record_set_name = 'a' -_base1.record_type = 'A' -_base1.params['ttl'] = 1 -_base1.params['a_records'] = [ARecord(ipv4_address='1.2.3.4'), - ARecord(ipv4_address='1.1.1.1')] -azure_records.append(_base1) - -_base2 = _AzureRecord('TestAzure', octo_records[2]) -_base2.zone_name = 'unit.tests' -_base2.relative_record_set_name = 'aa' -_base2.record_type = 'A' -_base2.params['ttl'] = 9001 -_base2.params['a_records'] = ARecord(ipv4_address='1.2.4.3') -azure_records.append(_base2) - -_base3 = _AzureRecord('TestAzure', octo_records[3]) -_base3.zone_name = 'unit.tests' -_base3.relative_record_set_name = 'aaa' -_base3.record_type = 'A' -_base3.params['ttl'] = 2 -_base3.params['a_records'] = ARecord(ipv4_address='1.1.1.3') -azure_records.append(_base3) - -_base4 = _AzureRecord('TestAzure', octo_records[4]) -_base4.zone_name = 'unit.tests' -_base4.relative_record_set_name = 'aaaa1' -_base4.record_type = 'AAAA' -_base4.params['ttl'] = 300 -aaaa1 = AaaaRecord(ipv6_address='2601:644:500:e210:62f8:1dff:feb8:947a') -aaaa2 = AaaaRecord(ipv6_address='2601:642:500:e210:62f8:1dff:feb8:947a') -_base4.params['aaaa_records'] = [aaaa1, aaaa2] -azure_records.append(_base4) - -_base5 = _AzureRecord('TestAzure', octo_records[5]) -_base5.zone_name = 'unit.tests' -_base5.relative_record_set_name = 'aaaa2' -_base5.record_type = 'AAAA' -_base5.params['ttl'] = 300 -_base5.params['aaaa_records'] = [aaaa1] -azure_records.append(_base5) - -_base6 = _AzureRecord('TestAzure', octo_records[6]) -_base6.zone_name = 'unit.tests' -_base6.relative_record_set_name = 'caa1' -_base6.record_type = 'CAA' -_base6.params['ttl'] = 9 -_base6.params['caa_records'] = [CaaRecord(flags=0, - tag='issue', - value='ca.unit.tests')] -azure_records.append(_base6) - -_base7 = _AzureRecord('TestAzure', octo_records[7]) -_base7.zone_name = 'unit.tests' -_base7.relative_record_set_name = 'caa2' -_base7.record_type = 'CAA' -_base7.params['ttl'] = 9 -_base7.params['caa_records'] = [CaaRecord(flags=0, - tag='issue', - value='ca1.unit.tests'), - CaaRecord(flags=0, - tag='issue', - value='ca2.unit.tests')] -azure_records.append(_base7) - -_base8 = _AzureRecord('TestAzure', octo_records[8]) -_base8.zone_name = 'unit.tests' -_base8.relative_record_set_name = 'cname' -_base8.record_type = 'CNAME' -_base8.params['ttl'] = 3 -_base8.params['cname_record'] = CnameRecord(cname='a.unit.tests.') -azure_records.append(_base8) - -_base9 = _AzureRecord('TestAzure', octo_records[9]) -_base9.zone_name = 'unit.tests' -_base9.relative_record_set_name = 'mx1' -_base9.record_type = 'MX' -_base9.params['ttl'] = 3 -_base9.params['mx_records'] = [MxRecord(preference=10, - exchange='mx1.unit.tests.'), - MxRecord(preference=20, - exchange='mx2.unit.tests.')] -azure_records.append(_base9) - -_base10 = _AzureRecord('TestAzure', octo_records[10]) -_base10.zone_name = 'unit.tests' -_base10.relative_record_set_name = 'mx2' -_base10.record_type = 'MX' -_base10.params['ttl'] = 3 -_base10.params['mx_records'] = [MxRecord(preference=10, - exchange='mx1.unit.tests.')] -azure_records.append(_base10) - -_base11 = _AzureRecord('TestAzure', octo_records[11]) -_base11.zone_name = 'unit.tests' -_base11.relative_record_set_name = '@' -_base11.record_type = 'NS' -_base11.params['ttl'] = 4 -_base11.params['ns_records'] = [NsRecord(nsdname='ns1.unit.tests.'), - NsRecord(nsdname='ns2.unit.tests.')] -azure_records.append(_base11) - -_base12 = _AzureRecord('TestAzure', octo_records[12]) -_base12.zone_name = 'unit.tests' -_base12.relative_record_set_name = 'foo' -_base12.record_type = 'NS' -_base12.params['ttl'] = 5 -_base12.params['ns_records'] = [NsRecord(nsdname='ns1.unit.tests.')] -azure_records.append(_base12) - -_base13 = _AzureRecord('TestAzure', octo_records[13]) -_base13.zone_name = 'unit.tests' -_base13.relative_record_set_name = 'ptr1' -_base13.record_type = 'PTR' -_base13.params['ttl'] = 5 -_base13.params['ptr_records'] = [PtrRecord(ptrdname='ptr1.unit.tests.')] -azure_records.append(_base13) - -_base14 = _AzureRecord('TestAzure', octo_records[14]) -_base14.zone_name = 'unit.tests' -_base14.relative_record_set_name = '_srv._tcp' -_base14.record_type = 'SRV' -_base14.params['ttl'] = 6 -_base14.params['srv_records'] = [SrvRecord(priority=10, - weight=20, - port=30, - target='foo-1.unit.tests.'), - SrvRecord(priority=12, - weight=30, - port=30, - target='foo-2.unit.tests.')] -azure_records.append(_base14) - -_base15 = _AzureRecord('TestAzure', octo_records[15]) -_base15.zone_name = 'unit.tests' -_base15.relative_record_set_name = '_srv2._tcp' -_base15.record_type = 'SRV' -_base15.params['ttl'] = 7 -_base15.params['srv_records'] = [SrvRecord(priority=12, - weight=17, - port=1, - target='srvfoo.unit.tests.')] -azure_records.append(_base15) - -_base16 = _AzureRecord('TestAzure', octo_records[16]) -_base16.zone_name = 'unit.tests' -_base16.relative_record_set_name = 'txt1' -_base16.record_type = 'TXT' -_base16.params['ttl'] = 8 -_base16.params['txt_records'] = [TxtRecord(value=['txt singleton test'])] -azure_records.append(_base16) - -_base17 = _AzureRecord('TestAzure', octo_records[17]) -_base17.zone_name = 'unit.tests' -_base17.relative_record_set_name = 'txt2' -_base17.record_type = 'TXT' -_base17.params['ttl'] = 9 -_base17.params['txt_records'] = [TxtRecord(value=['txt multiple test']), - TxtRecord(value=['txt multiple test 2'])] -azure_records.append(_base17) - -long_txt_az1 = "v=spf1 ip4:10.10.0.0/24 ip4:10.10.1.0/24 ip4:10.10.2.0/24" -long_txt_az1 += " ip4:10.10.3.0/24 ip4:10.10.4.0/24 ip4:10.10.5.0/24 " -long_txt_az1 += " 10.6.0/24 ip4:10.10.7.0/24 ip4:10.10.8.0/24 " -long_txt_az1 += " ip4:10.10.10.0/24 ip4:10.10.11.0/24 ip4:10.10.12.0/24" -long_txt_az1 += " ip4:10.10.13.0/24 ip4:10.10.14.0/24 ip4:10.10." -long_txt_az2 = "15.0/24 ip4:10.10.16.0/24 ip4:10.10.17.0/24 ip4:10.10.18.0/24" -long_txt_az2 += " ip4:10.10.19.0/24 ip4:10.10.20.0/24 ~all" -_base18 = _AzureRecord('TestAzure', octo_records[18]) -_base18.zone_name = 'unit.tests' -_base18.relative_record_set_name = 'txt3' -_base18.record_type = 'TXT' -_base18.params['ttl'] = 10 -_base18.params['txt_records'] = [TxtRecord(value=['txt multiple test']), - TxtRecord(value=[long_txt_az1, long_txt_az2])] -azure_records.append(_base18) - -_base19 = _AzureRecord('TestAzure', octo_records[19]) -_base19.zone_name = 'unit.tests' -_base19.relative_record_set_name = 'ptr2' -_base19.record_type = 'PTR' -_base19.params['ttl'] = 11 -_base19.params['ptr_records'] = [PtrRecord(ptrdname='ptr21.unit.tests.'), - PtrRecord(ptrdname='ptr22.unit.tests.')] -azure_records.append(_base19) - - -class Test_AzureRecord(TestCase): - def test_azure_record(self): - assert(len(azure_records) == len(octo_records)) - for i in range(len(azure_records)): - octo = _AzureRecord('TestAzure', octo_records[i]) - assert(azure_records[i]._equals(octo)) - - -class Test_DynamicAzureRecord(TestCase): - def test_azure_record(self): - tm_profile = Profile() - data = { - 'ttl': 60, - 'type': 'CNAME', - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'one': { - 'values': [ - {'value': 'one.unit.tests.', 'weight': 1} - ], - 'fallback': 'two', - }, - 'two': { - 'values': [ - {'value': 'two.unit.tests.', 'weight': 1} - ], - }, - }, - 'rules': [ - {'geos': ['AF'], 'pool': 'one'}, - {'pool': 'two'}, - ], - } - } - octo_record = Record.new(zone, 'foo', data) - azure_record = _AzureRecord('TestAzure', octo_record, - traffic_manager=tm_profile) - self.assertEqual(azure_record.zone_name, zone.name[:-1]) - self.assertEqual(azure_record.relative_record_set_name, 'foo') - self.assertEqual(azure_record.record_type, 'CNAME') - self.assertEqual(azure_record.params['ttl'], 60) - self.assertEqual(azure_record.params['target_resource'], tm_profile) - - -class Test_ParseAzureType(TestCase): - def test_parse_azure_type(self): - for expected, test in [['A', 'Microsoft.Network/dnszones/A'], - ['AAAA', 'Microsoft.Network/dnszones/AAAA'], - ['NS', 'Microsoft.Network/dnszones/NS'], - ['MX', 'Microsoft.Network/dnszones/MX']]: - self.assertEquals(expected, _parse_azure_type(test)) - - -class Test_CheckEndswithDot(TestCase): - def test_check_endswith_dot(self): - for expected, test in [['a.', 'a'], - ['a.', 'a.'], - ['foo.bar.', 'foo.bar.'], - ['foo.bar.', 'foo.bar']]: - self.assertEquals(expected, _check_endswith_dot(test)) - - -class Test_RootTrafficManagerName(TestCase): - def test_root_traffic_manager_name(self): - test = Record.new(zone, 'foo', data={ - 'ttl': 60, 'type': 'CNAME', 'value': 'default.unit.tests.', - }) - self.assertEqual(_root_traffic_manager_name(test), 'foo--unit--tests') - - -class Test_GetMonitor(TestCase): - def test_get_monitor(self): - record = Record.new(zone, 'foo', data={ - 'type': 'CNAME', 'ttl': 60, 'value': 'default.unit.tests.', - 'octodns': { - 'healthcheck': { - 'path': '/_ping', - 'port': 4443, - 'protocol': 'HTTPS', - } - }, - }) - - monitor = _get_monitor(record) - self.assertEqual(monitor.protocol, 'HTTPS') - self.assertEqual(monitor.port, 4443) - self.assertEqual(monitor.path, '/_ping') - headers = monitor.custom_headers - self.assertIsInstance(headers, list) - self.assertEquals(len(headers), 1) - headers = headers[0] - self.assertEqual(headers.name, 'Host') - self.assertEqual(headers.value, record.healthcheck_host()) - - # test TCP monitor - record._octodns['healthcheck']['protocol'] = 'TCP' - monitor = _get_monitor(record) - self.assertEqual(monitor.protocol, 'TCP') - self.assertIsNone(monitor.custom_headers) - - -class Test_ProfileIsMatch(TestCase): - def test_profile_is_match(self): - is_match = _profile_is_match - - self.assertFalse(is_match(None, Profile())) - - # Profile object builder with default property values that can be - # overridden for testing below - def profile( - name = 'foo-unit-tests', - ttl = 60, - method = 'Geographic', - dns_name = None, - monitor_proto = 'HTTPS', - monitor_port = 4443, - monitor_path = '/_ping', - monitor_interval_in_seconds = None, - monitor_timeout_in_seconds = None, - monitor_tolerated_number_of_failures = None, - endpoints = 1, - endpoint_name = 'name', - endpoint_type = 'profile/nestedEndpoints', - endpoint_status = None, - target = 'target.unit.tests', - target_id = 'resource/id', - geos = ['GEO-AF'], - weight = 1, - priority = 1, - ): - dns = DnsConfig(relative_name=(dns_name or name), ttl=ttl) - return Profile( - name=name, traffic_routing_method=method, dns_config=dns, - monitor_config=MonitorConfig( - protocol=monitor_proto, - port=monitor_port, - path=monitor_path, - interval_in_seconds=monitor_interval_in_seconds, - timeout_in_seconds=monitor_timeout_in_seconds, - tolerated_number_of_failures= - monitor_tolerated_number_of_failures, - ), - endpoints=[Endpoint( - name=endpoint_name, - type=endpoint_type, - endpoint_status=endpoint_status, - target=target, - target_resource_id=target_id, - geo_mapping=geos, - weight=weight, - priority=priority, - )] + [Endpoint()] * (endpoints - 1), - ) - - self.assertTrue(is_match(profile(), profile())) - - self.assertFalse(is_match(profile(), profile(name='two'))) - self.assertFalse(is_match(profile(), profile(endpoints=2))) - self.assertFalse(is_match(profile(), profile(dns_name='two'))) - self.assertFalse(is_match(profile(), profile(monitor_proto='HTTP'))) - self.assertFalse(is_match( - profile(), - profile(monitor_interval_in_seconds=9), - )) - self.assertFalse(is_match( - profile(), - profile(monitor_timeout_in_seconds=3), - )) - self.assertFalse(is_match( - profile(), - profile(monitor_tolerated_number_of_failures=2), - )) - self.assertFalse(is_match(profile(), profile(endpoint_name='a'))) - self.assertFalse(is_match(profile(), profile(endpoint_type='b'))) - self.assertFalse( - is_match(profile(), profile(endpoint_status='Disabled')) - ) - self.assertFalse( - is_match(profile(endpoint_type='b'), profile(endpoint_type='b')) - ) - self.assertFalse(is_match(profile(), profile(target_id='rsrc/id2'))) - self.assertFalse(is_match(profile(), profile(geos=['IN']))) - self.assertFalse(is_match( - profile(endpoint_type='profile/externalEndpoints'), - profile( - endpoint_type='profile/externalEndpoints', - geos=['IN'] - ) - )) - self.assertFalse(is_match(profile(method='Priority'), profile( - method='Priority', priority=2 - ))) - - def wprofile(**kwargs): - kwargs['method'] = 'Weighted' - kwargs['endpoint_type'] = 'profile/externalEndpoints' - return profile(**kwargs) - - self.assertFalse(is_match(wprofile(), wprofile(target='bar.unit'))) - self.assertFalse(is_match(wprofile(), wprofile(weight=3))) - - -class TestAzureDnsProvider(TestCase): - def _provider(self): - return self._get_provider('mock_spc', 'mock_dns_client') - - @patch('octodns.provider.azuredns.TrafficManagerManagementClient') - @patch('octodns.provider.azuredns.DnsManagementClient') - @patch('octodns.provider.azuredns.ClientSecretCredential') - @patch('octodns.provider.azuredns.ServicePrincipalCredentials') - def _get_provider(self, mock_spc, mock_css, mock_dns_client, - mock_tm_client): - '''Returns a mock AzureProvider object to use in testing. - - :param mock_spc: placeholder - :type mock_spc: str - :param mock_dns_client: placeholder - :type mock_dns_client: str - :param mock_tm_client: placeholder - :type mock_tm_client: str - - :type return: AzureProvider - ''' - provider = AzureProvider('mock_id', 'mock_client', 'mock_key', - 'mock_directory', 'mock_sub', 'mock_rg' - ) - - # Fetch the client to force it to load the creds - provider._dns_client - - # set critical functions to return properly - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = [] - tm_sync = provider._tm_client.profiles.create_or_update - - def side_effect(rg, name, profile): - return Profile( - id=profile.id, - name=profile.name, - traffic_routing_method=profile.traffic_routing_method, - dns_config=profile.dns_config, - monitor_config=profile.monitor_config, - endpoints=profile.endpoints, - ) - - tm_sync.side_effect = side_effect - - return provider - - def _get_dynamic_record(self, zone): - return Record.new(zone, 'foo', data={ - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'one': { - 'values': [ - {'value': 'one.unit.tests.', 'weight': 1}, - ], - 'fallback': 'two', - }, - 'two': { - 'values': [ - {'value': 'two1.unit.tests.', 'weight': 3}, - {'value': 'two2.unit.tests.', 'weight': 4}, - ], - 'fallback': 'three', - }, - 'three': { - 'values': [ - {'value': 'three.unit.tests.', 'weight': 1}, - ], - }, - }, - 'rules': [ - {'geos': ['AF', 'EU-DE', 'NA-US-CA', 'OC'], 'pool': 'one'}, - {'pool': 'two'}, - ], - }, - 'octodns': { - 'healthcheck': { - 'path': '/_ping', - 'port': 4443, - 'protocol': 'HTTPS', - } - }, - }) - - def _get_tm_profiles(self, provider): - sub = provider._dns_client_subscription_id - rg = provider._resource_group - base_id = '/subscriptions/' + sub + \ - '/resourceGroups/' + rg + \ - '/providers/Microsoft.Network/trafficManagerProfiles/' - prefix = 'foo--unit--tests' - name_format = prefix + '-' - id_format = base_id + name_format - - header = MonitorConfigCustomHeadersItem(name='Host', - value='foo.unit.tests') - monitor = MonitorConfig(protocol='HTTPS', port=4443, path='/_ping', - custom_headers=[header]) - external = 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - nested = 'Microsoft.Network/trafficManagerProfiles/nestedEndpoints' - - profiles = [ - Profile( - id=f'{id_format}pool-two', - name=f'{name_format}pool-two', - traffic_routing_method='Weighted', - dns_config=DnsConfig(ttl=60), - monitor_config=monitor, - endpoints=[ - Endpoint( - name='two--two1.unit.tests', - type=external, - target='two1.unit.tests', - weight=3, - ), - Endpoint( - name='two--two2.unit.tests', - type=external, - target='two2.unit.tests', - weight=4, - ), - ], - ), - Profile( - id=f'{id_format}rule-one', - name=f'{name_format}rule-one', - traffic_routing_method='Priority', - dns_config=DnsConfig(ttl=60), - monitor_config=monitor, - endpoints=[ - Endpoint( - name='one', - type=external, - target='one.unit.tests', - priority=1, - ), - Endpoint( - name='two', - type=nested, - target_resource_id=f'{id_format}pool-two', - priority=2, - ), - Endpoint( - name='three', - type=external, - target='three.unit.tests', - priority=3, - ), - Endpoint( - name='--default--', - type=external, - target='default.unit.tests', - priority=4, - ), - ], - ), - Profile( - id=f'{id_format}rule-two', - name=f'{name_format}rule-two', - traffic_routing_method='Priority', - dns_config=DnsConfig(ttl=60), - monitor_config=monitor, - endpoints=[ - Endpoint( - name='two', - type=nested, - target_resource_id=f'{id_format}pool-two', - priority=1, - ), - Endpoint( - name='three', - type=external, - target='three.unit.tests', - priority=2, - ), - Endpoint( - name='--default--', - type=external, - target='default.unit.tests', - priority=3, - ), - ], - ), - Profile( - id=base_id + prefix, - name=prefix, - traffic_routing_method='Geographic', - dns_config=DnsConfig(ttl=60), - monitor_config=monitor, - endpoints=[ - Endpoint( - geo_mapping=['GEO-AF', 'DE', 'US-CA', 'GEO-AP'], - name='one', - type=nested, - target_resource_id=f'{id_format}rule-one', - ), - Endpoint( - geo_mapping=['WORLD'], - name='two', - type=nested, - target_resource_id=f'{id_format}rule-two', - ), - ], - ), - ] - - for profile in profiles: - profile.dns_config.relative_name = profile.name - - return profiles - - def _get_dynamic_package(self): - '''Convenience function to setup a sample dynamic record. - ''' - provider = self._get_provider() - - # setup traffic manager profiles - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = self._get_tm_profiles(provider) - - # setup zone with dynamic record - zone = Zone(name='unit.tests.', sub_zones=[]) - record = self._get_dynamic_record(zone) - zone.add_record(record) - - # return everything - return provider, zone, record - - def test_populate_records(self): - provider = self._get_provider() - - rs = [] - recordSet = RecordSet(a_records=[ARecord(ipv4_address='1.1.1.1')]) - recordSet.name, recordSet.ttl, recordSet.type = 'a1', 0, 'A' - recordSet.target_resource = SubResource() - rs.append(recordSet) - recordSet = RecordSet(a_records=[ARecord(ipv4_address='1.1.1.1'), - ARecord(ipv4_address='2.2.2.2')]) - recordSet.name, recordSet.ttl, recordSet.type = 'a2', 1, 'A' - recordSet.target_resource = SubResource() - rs.append(recordSet) - aaaa1 = AaaaRecord(ipv6_address='1:1ec:1::1') - recordSet = RecordSet(aaaa_records=[aaaa1]) - recordSet.name, recordSet.ttl, recordSet.type = 'aaaa1', 2, 'AAAA' - recordSet.target_resource = SubResource() - rs.append(recordSet) - aaaa2 = AaaaRecord(ipv6_address='1:1ec:1::2') - recordSet = RecordSet(aaaa_records=[aaaa1, - aaaa2]) - recordSet.name, recordSet.ttl, recordSet.type = 'aaaa2', 3, 'AAAA' - recordSet.target_resource = SubResource() - rs.append(recordSet) - recordSet = RecordSet(caa_records=[CaaRecord(flags=0, - tag='issue', - value='caa1.unit.tests')]) - recordSet.name, recordSet.ttl, recordSet.type = 'caa1', 4, 'CAA' - rs.append(recordSet) - recordSet = RecordSet(caa_records=[CaaRecord(flags=0, - tag='issue', - value='caa1.unit.tests'), - CaaRecord(flags=0, - tag='issue', - value='caa2.unit.tests')]) - recordSet.name, recordSet.ttl, recordSet.type = 'caa2', 4, 'CAA' - rs.append(recordSet) - cname1 = CnameRecord(cname='cname.unit.test.') - recordSet = RecordSet(cname_record=cname1) - recordSet.name, recordSet.ttl, recordSet.type = 'cname1', 5, 'CNAME' - recordSet.target_resource = SubResource() - rs.append(recordSet) - recordSet = RecordSet(mx_records=[MxRecord(preference=10, - exchange='mx1.unit.test.')]) - recordSet.name, recordSet.ttl, recordSet.type = 'mx1', 7, 'MX' - rs.append(recordSet) - recordSet = RecordSet(mx_records=[MxRecord(preference=10, - exchange='mx1.unit.test.'), - MxRecord(preference=11, - exchange='mx2.unit.test.')]) - recordSet.name, recordSet.ttl, recordSet.type = 'mx2', 8, 'MX' - rs.append(recordSet) - recordSet = RecordSet(ns_records=[NsRecord(nsdname='ns1.unit.test.')]) - recordSet.name, recordSet.ttl, recordSet.type = 'ns1', 9, 'NS' - rs.append(recordSet) - recordSet = RecordSet(ns_records=[NsRecord(nsdname='ns1.unit.test.'), - NsRecord(nsdname='ns2.unit.test.')]) - recordSet.name, recordSet.ttl, recordSet.type = 'ns2', 10, 'NS' - rs.append(recordSet) - ptr1 = PtrRecord(ptrdname='ptr1.unit.test.') - recordSet = RecordSet(ptr_records=[ptr1]) - recordSet.name, recordSet.ttl, recordSet.type = 'ptr1', 11, 'PTR' - rs.append(recordSet) - recordSet = RecordSet(srv_records=[SrvRecord(priority=1, - weight=2, - port=3, - target='1unit.tests.')]) - recordSet.name, recordSet.ttl, recordSet.type = '_srv1._tcp', 13, 'SRV' - rs.append(recordSet) - recordSet = RecordSet(srv_records=[SrvRecord(priority=1, - weight=2, - port=3, - target='1unit.tests.'), - SrvRecord(priority=4, - weight=5, - port=6, - target='2unit.tests.')]) - recordSet.name, recordSet.ttl, recordSet.type = '_srv2._tcp', 14, 'SRV' - rs.append(recordSet) - recordSet = RecordSet(txt_records=[TxtRecord(value='sample text1')]) - recordSet.name, recordSet.ttl, recordSet.type = 'txt1', 15, 'TXT' - recordSet.target_resource = SubResource() - rs.append(recordSet) - recordSet = RecordSet(txt_records=[TxtRecord(value='sample text1'), - TxtRecord(value='sample text2')]) - recordSet.name, recordSet.ttl, recordSet.type = 'txt2', 16, 'TXT' - recordSet.target_resource = SubResource() - rs.append(recordSet) - recordSet = RecordSet(soa_record=[SoaRecord()]) - recordSet.name, recordSet.ttl, recordSet.type = '', 17, 'SOA' - rs.append(recordSet) - long_txt = "v=spf1 ip4:10.10.0.0/24 ip4:10.10.1.0/24 ip4:10.10.2.0/24" - long_txt += " ip4:10.10.3.0/24 ip4:10.10.4.0/24 ip4:10.10.5.0/24 " - long_txt += " 10.6.0/24 ip4:10.10.7.0/24 ip4:10.10.8.0/24 " - long_txt += " ip4:10.10.10.0/24 ip4:10.10.11.0/24 ip4:10.10.12.0/24" - long_txt += " ip4:10.10.13.0/24 ip4:10.10.14.0/24 ip4:10.10.15.0/24" - long_txt += " ip4:10.10.16.0/24 ip4:10.10.17.0/24 ip4:10.10.18.0/24" - long_txt += " ip4:10.10.19.0/24 ip4:10.10.20.0/24 ~all" - recordSet = RecordSet(txt_records=[TxtRecord(value='sample value1'), - TxtRecord(value=long_txt)]) - recordSet.name, recordSet.ttl, recordSet.type = 'txt3', 18, 'TXT' - recordSet.target_resource = SubResource() - rs.append(recordSet) - - record_list = provider._dns_client.record_sets.list_by_dns_zone - record_list.return_value = rs - - zone_list = provider._dns_client.zones.list_by_resource_group - zone_list.return_value = [zone] - - exists = provider.populate(zone) - - self.assertEquals(len(zone.records), 17) - self.assertTrue(exists) - - def test_populate_zone(self): - provider = self._get_provider() - - zone_list = provider._dns_client.zones.list_by_resource_group - zone_1 = AzureZone(location='global') - # This is far from ideal but the - # zone constructor doesn't let me set it on creation - zone_1.name = "zone-1" - zone_2 = AzureZone(location='global') - # This is far from ideal but the - # zone constructor doesn't let me set it on creation - zone_2.name = "zone-2" - zone_list.return_value = [zone_1, - zone_2, - zone_1] - - provider._populate_zones() - - # This should be returning two zones since two zones are the same - self.assertEquals(len(provider._azure_zones), 2) - - def test_bad_zone_response(self): - provider = self._get_provider() - - _get = provider._dns_client.zones.get - _get.side_effect = CloudError(Mock(status=404), 'Azure Error') - self.assertEquals( - provider._check_zone('unit.test', create=False), - None - ) - - def test_extra_changes(self): - provider, existing, record = self._get_dynamic_package() - - # test simple records produce no extra changes - desired = Zone(name=existing.name, sub_zones=[]) - simple = Record.new(desired, 'simple', data={ - 'type': record._type, - 'ttl': record.ttl, - 'value': record.value, - }) - desired.add_record(simple) - extra = provider._extra_changes(desired, desired, [Create(simple)]) - self.assertEqual(len(extra), 0) - - # test an unchanged dynamic record produces no extra changes - desired.add_record(record) - extra = provider._extra_changes(existing, desired, []) - self.assertEqual(len(extra), 0) - - # test unused TM produces the extra change for clean up - sample_profile = self._get_tm_profiles(provider)[0] - tm_id = provider._profile_name_to_id - root_profile_name = _root_traffic_manager_name(record) - extra_profile = Profile( - id=tm_id(f'{root_profile_name}-pool-random'), - name=f'{root_profile_name}-pool-random', - traffic_routing_method='Weighted', - dns_config=sample_profile.dns_config, - monitor_config=sample_profile.monitor_config, - endpoints=sample_profile.endpoints, - ) - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value.append(extra_profile) - provider._populate_traffic_managers() - extra = provider._extra_changes(existing, desired, []) - self.assertEqual(len(extra), 1) - extra = extra[0] - self.assertIsInstance(extra, Update) - self.assertEqual(extra.new, record) - desired._remove_record(record) - tm_list.return_value.pop() - - # test new dynamic record does not produce an extra change for it - new_dynamic = Record.new(desired, record.name + '2', data={ - 'type': record._type, - 'ttl': record.ttl, - 'value': record.value, - 'dynamic': record.dynamic._data(), - 'octodns': record._octodns, - }) - # test change in healthcheck by using a different port number - update_dynamic = Record.new(desired, record.name, data={ - 'type': record._type, - 'ttl': record.ttl, - 'value': record.value, - 'dynamic': record.dynamic._data(), - 'octodns': { - 'healthcheck': { - 'path': '/_ping', - 'port': 443, - 'protocol': 'HTTPS', - }, - }, - }) - desired.add_record(new_dynamic) - desired.add_record(update_dynamic) - changes = [Create(new_dynamic)] - extra = provider._extra_changes(existing, desired, changes) - # implicitly asserts that new_dynamic was not added to extra changes - # as it was already in the `changes` list - self.assertEqual(len(extra), 1) - extra = extra[0] - self.assertIsInstance(extra, Update) - self.assertEqual(extra.new, update_dynamic) - - # test dynamic record of unsupported type throws exception - unsupported_dynamic = Record.new(desired, record.name + '3', data={ - 'type': 'DNAME', - 'ttl': record.ttl, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'one': {'values': [{'value': 'one.unit.tests.'}]}, - }, - 'rules': [ - {'pool': 'one'}, - ], - }, - }) - desired.add_record(unsupported_dynamic) - changes = [Create(unsupported_dynamic)] - with self.assertRaises(AzureException) as ctx: - provider._extra_changes(existing, desired, changes) - self.assertTrue(str(ctx).endswith('must be of type CNAME')) - desired._remove_record(unsupported_dynamic) - - # test colliding ATM names throws exception - record1 = Record.new(desired, 'sub.www', data={ - 'type': record._type, - 'ttl': record.ttl, - 'value': record.value, - 'dynamic': record.dynamic._data(), - }) - record2 = Record.new(desired, 'sub--www', data={ - 'type': record._type, - 'ttl': record.ttl, - 'value': record.value, - 'dynamic': record.dynamic._data(), - }) - desired.add_record(record1) - desired.add_record(record2) - changes = [Create(record1), Create(record2)] - with self.assertRaises(AzureException) as ctx: - provider._extra_changes(existing, desired, changes) - self.assertTrue(str(ctx) - .startswith('Collision in Traffic Manager')) - - @patch( - 'octodns.provider.azuredns.AzureProvider._generate_traffic_managers') - def test_extra_changes_non_last_fallback_contains_default(self, mock_gtm): - provider = self._get_provider() - - desired = Zone(zone.name, sub_zones=[]) - record = Record.new(desired, 'foo', { - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'one': { - 'values': [{'value': 'one.unit.tests.'}], - 'fallback': 'def', - }, - 'def': { - 'values': [{'value': 'default.unit.tests.'}], - 'fallback': 'two', - }, - 'two': { - 'values': [{'value': 'two.unit.tests.'}], - }, - }, - 'rules': [ - {'pool': 'one'}, - ] - } - }) - desired.add_record(record) - changes = [Create(record)] - - # assert that no exception is raised - provider._extra_changes(zone, desired, changes) - - # simulate duplicate endpoint and assert exception - endpoint = Endpoint(target='dup.unit.tests.') - mock_gtm.return_value = [Profile( - name='test-profile', - endpoints=[endpoint, endpoint], - )] - with self.assertRaises(AzureException) as ctx: - provider._extra_changes(zone, desired, changes) - self.assertTrue('duplicate endpoint' in str(ctx)) - - def test_extra_changes_A_multi_defaults(self): - provider = self._get_provider() - - record = Record.new(zone, 'foo', data={ - 'type': 'A', - 'ttl': 60, - 'values': ['1.1.1.1', '8.8.8.8'], - 'dynamic': { - 'pools': { - 'one': { - 'values': [{'value': '1.1.1.1'}], - }, - }, - 'rules': [ - {'pool': 'one'}, - ], - } - }) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - with self.assertRaises(AzureException) as ctx: - provider._extra_changes(zone, desired, []) - self.assertEqual('single value' in str(ctx)) - - def test_generate_tm_profile(self): - provider, zone, record = self._get_dynamic_package() - profile_gen = provider._generate_tm_profile - - label = 'foobar' - routing = 'Priority' - endpoints = [ - Endpoint(target='one.unit.tests'), - Endpoint(target_resource_id='/s/1/rg/foo/tm/foobar2'), - Endpoint(name='invalid'), - ] - - # invalid endpoint raises exception - with self.assertRaises(AzureException): - profile_gen(routing, endpoints, record, label) - - # regular test - endpoints.pop() - profile = profile_gen(routing, endpoints, record, label) - - # implicitly tests _profile_name_to_id - sub = provider._dns_client_subscription_id - rg = provider._resource_group - expected_name = 'foo--unit--tests-rule-foobar' - expected_id = '/subscriptions/' + sub + \ - '/resourceGroups/' + rg + \ - '/providers/Microsoft.Network/trafficManagerProfiles/' + \ - expected_name - self.assertEqual(profile.id, expected_id) - self.assertEqual(profile.name, expected_name) - self.assertEqual(profile.name, profile.dns_config.relative_name) - self.assertEqual(profile.traffic_routing_method, routing) - self.assertEqual(profile.dns_config.ttl, record.ttl) - self.assertEqual(len(profile.endpoints), len(endpoints)) - - self.assertEqual( - profile.endpoints[0].type, - 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - ) - self.assertEqual( - profile.endpoints[1].type, - 'Microsoft.Network/trafficManagerProfiles/nestedEndpoints' - ) - - def test_dynamic_record(self): - provider, zone, record = self._get_dynamic_package() - profiles = provider._generate_traffic_managers(record) - - # check that every profile is a match with what we expect - expected_profiles = self._get_tm_profiles(provider) - self.assertEqual(len(expected_profiles), len(profiles)) - for have, expected in zip(profiles, expected_profiles): - self.assertTrue(_profile_is_match(have, expected)) - - # check that dynamic record is populated back from profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_generate_traffic_managers_middle_east(self): - # check Asia/Middle East test case - provider, zone, record = self._get_dynamic_package() - record.dynamic._data()['rules'][0]['geos'].append('AS') - profiles = provider._generate_traffic_managers(record) - self.assertIn('GEO-ME', profiles[-1].endpoints[0].geo_mapping) - self.assertIn('GEO-AS', profiles[-1].endpoints[0].geo_mapping) - - def test_populate_dynamic_middle_east(self): - # Middle east without Asia raises exception - provider, zone, record = self._get_dynamic_package() - tm_suffix = _root_traffic_manager_name(record) - tm_id = provider._profile_name_to_id - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = [ - Profile( - id=tm_id(tm_suffix), - name=tm_suffix, - traffic_routing_method='Geographic', - endpoints=[ - Endpoint( - geo_mapping=['GEO-ME'], - ), - ], - ), - ] - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=tm_id(tm_suffix)), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - with self.assertRaises(AzureException) as ctx: - provider._populate_record(zone, azrecord) - self.assertTrue(str(ctx).startswith( - 'Middle East (GEO-ME) is not supported' - )) - - # valid profiles with Middle East test case - provider, zone, record = self._get_dynamic_package() - geo_profile = provider._get_tm_for_dynamic_record(record) - geo_profile.endpoints[0].geo_mapping.extend(['GEO-ME', 'GEO-AS']) - record = provider._populate_record(zone, azrecord) - self.assertIn('AS', record.dynamic.rules[0].data['geos']) - self.assertNotIn('ME', record.dynamic.rules[0].data['geos']) - - def test_dynamic_no_geo(self): - # test that traffic managers are generated as expected - provider = self._get_provider() - external = 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - - record = Record.new(zone, 'foo', data={ - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'one': { - 'values': [ - {'value': 'one.unit.tests.'}, - ], - }, - }, - 'rules': [ - {'pool': 'one'}, - ], - } - }) - profiles = provider._generate_traffic_managers(record) - - self.assertEqual(len(profiles), 1) - self.assertTrue(_profile_is_match(profiles[0], Profile( - name='foo--unit--tests', - traffic_routing_method='Priority', - dns_config=DnsConfig( - relative_name='foo--unit--tests', ttl=60), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='one', - type=external, - target='one.unit.tests', - priority=1, - ), - Endpoint( - name='--default--', - type=external, - target='default.unit.tests', - priority=2, - ), - ], - ))) - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_dynamic_fallback_is_default(self): - # test that traffic managers are generated as expected - provider = self._get_provider() - external = 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - - record = Record.new(zone, 'foo', data={ - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'def': { - 'values': [ - {'value': 'default.unit.tests.'}, - ], - }, - }, - 'rules': [ - {'geos': ['AF'], 'pool': 'def'}, - ], - } - }) - profiles = provider._generate_traffic_managers(record) - - self.assertEqual(len(profiles), 1) - self.assertTrue(_profile_is_match(profiles[0], Profile( - name='foo--unit--tests', - traffic_routing_method='Geographic', - dns_config=DnsConfig( - relative_name='foo--unit--tests', ttl=60), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='def--default--', - type=external, - target='default.unit.tests', - geo_mapping=['GEO-AF'], - ), - ], - ))) - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_dynamic_pool_contains_default(self): - # test that traffic managers are generated as expected - provider = self._get_provider() - external = 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - nested = 'Microsoft.Network/trafficManagerProfiles/nestedEndpoints' - - record = Record.new(zone, 'foo', data={ - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'rr': { - 'values': [ - {'value': 'one.unit.tests.'}, - {'value': 'two.unit.tests.'}, - {'value': 'default.unit.tests.'}, - {'value': 'final.unit.tests.'}, - ], - }, - }, - 'rules': [ - {'geos': ['AF'], 'pool': 'rr'}, - ], - } - }) - profiles = provider._generate_traffic_managers(record) - - self.assertEqual(len(profiles), 2) - self.assertTrue(_profile_is_match(profiles[0], Profile( - name='foo--unit--tests-pool-rr', - traffic_routing_method='Weighted', - dns_config=DnsConfig( - relative_name='foo--unit--tests-pool-rr', ttl=60), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='rr--one.unit.tests', - type=external, - target='one.unit.tests', - weight=1, - ), - Endpoint( - name='rr--two.unit.tests', - type=external, - target='two.unit.tests', - weight=1, - ), - Endpoint( - name='rr--default.unit.tests--default--', - type=external, - target='default.unit.tests', - weight=1, - ), - Endpoint( - name='rr--final.unit.tests', - type=external, - target='final.unit.tests', - weight=1, - ), - ], - ))) - self.assertTrue(_profile_is_match(profiles[1], Profile( - name='foo--unit--tests', - traffic_routing_method='Geographic', - dns_config=DnsConfig( - relative_name='foo--unit--tests', ttl=60), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='rr', - type=nested, - target_resource_id=profiles[0].id, - geo_mapping=['GEO-AF'], - ), - ], - ))) - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_dynamic_pool_contains_default_no_geo(self): - # test that traffic managers are generated as expected - provider = self._get_provider() - external = 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - - record = Record.new(zone, 'foo', data={ - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'rr': { - 'values': [ - {'value': 'one.unit.tests.'}, - {'value': 'two.unit.tests.'}, - {'value': 'default.unit.tests.'}, - {'value': 'final.unit.tests.'}, - ], - }, - }, - 'rules': [ - {'pool': 'rr'}, - ], - } - }) - profiles = provider._generate_traffic_managers(record) - - self.assertEqual(len(profiles), 1) - self.assertTrue(_profile_is_match(profiles[0], Profile( - name='foo--unit--tests', - traffic_routing_method='Weighted', - dns_config=DnsConfig( - relative_name='foo--unit--tests', ttl=60), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='rr--one.unit.tests', - type=external, - target='one.unit.tests', - weight=1, - ), - Endpoint( - name='rr--two.unit.tests', - type=external, - target='two.unit.tests', - weight=1, - ), - Endpoint( - name='rr--default.unit.tests--default--', - type=external, - target='default.unit.tests', - weight=1, - ), - Endpoint( - name='rr--final.unit.tests', - type=external, - target='final.unit.tests', - weight=1, - ), - ], - ))) - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_dynamic_last_pool_contains_default_no_geo(self): - # test that traffic managers are generated as expected - provider = self._get_provider() - external = 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - nested = 'Microsoft.Network/trafficManagerProfiles/nestedEndpoints' - - record = Record.new(zone, 'foo', data={ - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'cloud': { - 'values': [ - {'value': 'cloud.unit.tests.'}, - ], - 'fallback': 'rr', - }, - 'rr': { - 'values': [ - {'value': 'one.unit.tests.'}, - {'value': 'two.unit.tests.'}, - {'value': 'default.unit.tests.'}, - {'value': 'final.unit.tests.'}, - ], - }, - }, - 'rules': [ - {'pool': 'cloud'}, - ], - } - }) - profiles = provider._generate_traffic_managers(record) - - self.assertEqual(len(profiles), 2) - self.assertTrue(_profile_is_match(profiles[0], Profile( - name='foo--unit--tests-pool-rr', - traffic_routing_method='Weighted', - dns_config=DnsConfig( - relative_name='foo--unit--tests-pool-rr', ttl=60), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='rr--one.unit.tests', - type=external, - target='one.unit.tests', - weight=1, - ), - Endpoint( - name='rr--two.unit.tests', - type=external, - target='two.unit.tests', - weight=1, - ), - Endpoint( - name='rr--default.unit.tests--default--', - type=external, - target='default.unit.tests', - weight=1, - ), - Endpoint( - name='rr--final.unit.tests', - type=external, - target='final.unit.tests', - weight=1, - ), - ], - ))) - self.assertTrue(_profile_is_match(profiles[1], Profile( - name='foo--unit--tests', - traffic_routing_method='Priority', - dns_config=DnsConfig( - relative_name='foo--unit--tests', ttl=60), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='cloud', - type=external, - target='cloud.unit.tests', - priority=1, - ), - Endpoint( - name='rr', - type=nested, - target_resource_id=profiles[0].id, - priority=2, - ), - ], - ))) - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_dynamic_unique_traffic_managers(self): - record = self._get_dynamic_record(zone) - data = { - 'type': record._type, - 'ttl': record.ttl, - 'value': record.value, - 'dynamic': record.dynamic._data() - } - record_names = [ - 'www.foo', 'www-foo' - ] - provider = self._get_provider() - - seen = set() - for name in record_names: - record = Record.new(zone, name, data=data) - tms = provider._generate_traffic_managers(record) - for tm in tms: - self.assertNotIn(tm.name, seen) - seen.add(tm.name) - - def test_dynamic_reused_pool(self): - # test that traffic managers are generated as expected - provider = self._get_provider() - nested = 'Microsoft.Network/trafficManagerProfiles/nestedEndpoints' - - record = Record.new(zone, 'foo', data={ - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'sto': { - 'values': [ - {'value': 'sto.unit.tests.'}, - ], - 'fallback': 'iad', - }, - 'iad': { - 'values': [ - {'value': 'iad.unit.tests.'}, - ], - 'fallback': 'lhr', - }, - 'lhr': { - 'values': [ - {'value': 'lhr.unit.tests.'}, - ], - }, - }, - 'rules': [ - {'geos': ['EU'], 'pool': 'iad'}, - {'geos': ['EU-GB'], 'pool': 'lhr'}, - {'geos': ['EU-SE'], 'pool': 'sto'}, - {'pool': 'lhr'}, - ], - } - }) - profiles = provider._generate_traffic_managers(record) - - self.assertEqual(len(profiles), 4) - self.assertTrue(_profile_is_match(profiles[-1], Profile( - name='foo--unit--tests', - traffic_routing_method='Geographic', - dns_config=DnsConfig( - relative_name='foo--unit--tests', ttl=record.ttl), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='iad', - type=nested, - target_resource_id=profiles[0].id, - geo_mapping=['GEO-EU'], - ), - Endpoint( - name='lhr', - type=nested, - target_resource_id=profiles[1].id, - geo_mapping=['GB', 'WORLD'], - ), - Endpoint( - name='sto', - type=nested, - target_resource_id=profiles[2].id, - geo_mapping=['SE'], - ), - ], - ))) - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_dynamic_pool_status(self): - # test that traffic managers are generated as expected for pool value - # statuses - provider = self._get_provider() - zone1 = Zone('unit.tests.', []) - record1 = Record.new(zone1, 'foo', data={ - 'type': 'CNAME', - 'ttl': 60, - 'value': 'default.unit.tests.', - 'dynamic': { - 'pools': { - 'one': { - 'values': [ - {'value': 'one1.unit.tests.', 'status': 'up'}, - ], - }, - 'two': { - 'values': [ - {'value': 'two1.unit.tests.', 'status': 'down'}, - {'value': 'two2.unit.tests.'}, - ], - }, - }, - 'rules': [ - {'geos': ['AS'], 'pool': 'one'}, - {'pool': 'two'}, - ], - } - }) - zone1.add_record(record1) - zone2 = provider._process_desired_zone(zone1.copy()) - record2 = list(zone2.records)[0] - self.assertTrue( - record2.dynamic.pools['one'].data['values'][0]['status'], - 'obey' - ) - - record1.dynamic.pools['one'].data['values'][0]['status'] = 'down' - profiles = provider._generate_traffic_managers(record1) - self.assertEqual(len(profiles), 4) - self.assertEqual(profiles[0].endpoints[0].endpoint_status, 'Disabled') - self.assertEqual(profiles[1].endpoints[0].endpoint_status, 'Disabled') - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record1.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record1._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record1.dynamic._data(), record2.dynamic._data()) - - # _process_desired_zone shouldn't change anything when status value is - # supported - zone1 = Zone(zone.name, sub_zones=[]) - zone1.add_record(record1) - zone2 = provider._process_desired_zone(zone1.copy()) - record2 = list(zone2.records)[0] - self.assertTrue(record1.data, record2.data) - - # simple records should not get changed by _process_desired_zone - zone1 = Zone(zone.name, sub_zones=[]) - record1 = Record.new(zone1, 'foo', data={ - 'type': 'CNAME', - 'ttl': 86400, - 'value': 'one.unit.tests.', - }) - zone1.add_record(record1) - zone2 = provider._process_desired_zone(zone1.copy()) - record2 = list(zone2.records)[0] - self.assertTrue(record1.data, record2.data) - - def test_dynamic_A(self): - provider = self._get_provider() - external = 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - nested = 'Microsoft.Network/trafficManagerProfiles/nestedEndpoints' - - record = Record.new(zone, 'foo', data={ - 'type': 'A', - 'ttl': 60, - 'values': ['9.9.9.9'], - 'dynamic': { - 'pools': { - 'one': { - 'values': [ - {'value': '11.11.11.11'}, - {'value': '12.12.12.12'}, - ], - 'fallback': 'two' - }, - 'two': { - 'values': [ - {'value': '2.2.2.2'}, - ], - }, - }, - 'rules': [ - {'geos': ['AF'], 'pool': 'one'}, - {'pool': 'two'}, - ], - } - }) - - profiles = provider._generate_traffic_managers(record) - - self.assertEqual(len(profiles), 4) - self.assertTrue(_profile_is_match(profiles[0], Profile( - name='foo--unit--tests-A-pool-one', - traffic_routing_method='Weighted', - dns_config=DnsConfig( - relative_name='foo--unit--tests-a-pool-one', ttl=record.ttl), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='one--11.11.11.11', - type=external, - target='11.11.11.11', - weight=1, - ), - Endpoint( - name='one--12.12.12.12', - type=external, - target='12.12.12.12', - weight=1, - ), - ], - ))) - self.assertTrue(_profile_is_match(profiles[1], Profile( - name='foo--unit--tests-A-rule-one', - traffic_routing_method='Priority', - dns_config=DnsConfig( - relative_name='foo--unit--tests-a-rule-one', ttl=record.ttl), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='one', - type=nested, - target_resource_id=profiles[0].id, - priority=1, - ), - Endpoint( - name='two', - type=external, - target='2.2.2.2', - priority=2, - ), - Endpoint( - name='--default--', - type=external, - target='9.9.9.9', - priority=3, - ), - ], - ))) - self.assertTrue(_profile_is_match(profiles[2], Profile( - name='foo--unit--tests-A-rule-two', - traffic_routing_method='Priority', - dns_config=DnsConfig( - relative_name='foo--unit--tests-a-rule-two', ttl=record.ttl), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='two', - type=external, - target='2.2.2.2', - priority=1, - ), - Endpoint( - name='--default--', - type=external, - target='9.9.9.9', - priority=2, - ), - ], - ))) - self.assertTrue(_profile_is_match(profiles[3], Profile( - name='foo--unit--tests-A', - traffic_routing_method='Geographic', - dns_config=DnsConfig( - relative_name='foo--unit--tests-a', ttl=record.ttl), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='one', - type=nested, - target_resource_id=profiles[1].id, - geo_mapping=['GEO-AF'], - ), - Endpoint( - name='two', - type=nested, - target_resource_id=profiles[2].id, - geo_mapping=['WORLD'], - ), - ], - ))) - - # test that the record and ATM profile gets created - tm_sync = provider._tm_client.profiles.create_or_update - create = provider._dns_client.record_sets.create_or_update - provider._apply_Create(Create(record)) - self.assertEqual(tm_sync.call_count, len(profiles) + 1) - create.assert_called_once() - - # test broken alias - azrecord = RecordSet( - ttl=60, target_resource=SubResource(id=None)) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord, lenient=True) - self.assertEqual(record2.values, []) - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - provider._populate_traffic_managers() - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_dynamic_AAAA(self): - provider = self._get_provider() - external = 'Microsoft.Network/trafficManagerProfiles/externalEndpoints' - - record = Record.new(zone, 'foo', data={ - 'type': 'AAAA', - 'ttl': 60, - 'values': ['1::1', '2::2'], - 'dynamic': { - 'pools': { - 'one': { - 'values': [ - {'value': '1::1'}, - {'value': '2::2'}, - ], - }, - }, - 'rules': [ - {'pool': 'one'}, - ], - } - }) - profiles = provider._generate_traffic_managers(record) - - self.assertEqual(len(profiles), 1) - self.assertTrue(_profile_is_match(profiles[0], Profile( - name='foo--unit--tests-AAAA', - traffic_routing_method='Weighted', - dns_config=DnsConfig( - relative_name='foo--unit--tests-aaaa', ttl=record.ttl), - monitor_config=_get_monitor(record), - endpoints=[ - Endpoint( - name='one--1--1--default--', - type=external, - target='1::1', - weight=1, - ), - Endpoint( - name='one--2--2--default--', - type=external, - target='2::2', - weight=1, - ), - ], - ))) - - # test that the record and ATM profile gets created - tm_sync = provider._tm_client.profiles.create_or_update - create = provider._dns_client.record_sets.create_or_update - provider._apply_Create(Create(record)) - # A dynamic record can only have 1 profile - tm_sync.assert_called_once() - create.assert_called_once() - - # test broken alias - azrecord = RecordSet( - ttl=60, target_resource=SubResource(id=None)) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord, lenient=True) - self.assertEqual(record2.values, []) - - # test that same record gets populated back from traffic managers - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = profiles - azrecord = RecordSet( - ttl=60, - target_resource=SubResource(id=profiles[-1].id), - ) - azrecord.name = record.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record._type}' - record2 = provider._populate_record(zone, azrecord) - self.assertEqual(record2.dynamic._data(), record.dynamic._data()) - - # test that extra changes doesn't show any changes - desired = Zone(zone.name, sub_zones=[]) - desired.add_record(record) - changes = provider._extra_changes(zone, desired, []) - self.assertEqual(len(changes), 0) - - def test_sync_traffic_managers(self): - provider, zone, record = self._get_dynamic_package() - provider._populate_traffic_managers() - - tm_sync = provider._tm_client.profiles.create_or_update - - prefix = 'foo--unit--tests' - expected_seen = { - prefix, f'{prefix}-pool-two', f'{prefix}-rule-one', - f'{prefix}-rule-two', - } - - # test no change - profiles = provider._generate_traffic_managers(record) - seen = provider._sync_traffic_managers(profiles) - self.assertEqual(seen, expected_seen) - tm_sync.assert_not_called() - - # test that changing weight causes update API call - dynamic = record.dynamic._data() - dynamic['pools']['two']['values'][0]['weight'] = 14 - data = { - 'type': 'CNAME', - 'ttl': record.ttl, - 'value': record.value, - 'dynamic': dynamic, - 'octodns': record._octodns, - } - new_record = Record.new(zone, record.name, data) - tm_sync.reset_mock() - profiles = provider._generate_traffic_managers(new_record) - seen2 = provider._sync_traffic_managers(profiles) - self.assertEqual(seen2, expected_seen) - tm_sync.assert_called_once() - - # test that new profile was successfully inserted in cache - new_profile = provider._get_tm_profile_by_name(f'{prefix}-pool-two') - self.assertEqual(new_profile.endpoints[0].weight, 14) - - def test_sync_traffic_managers_duplicate(self): - provider, zone, record = self._get_dynamic_package() - tm_sync = provider._tm_client.profiles.create_or_update - - # change and duplicate profiles - profile = self._get_tm_profiles(provider)[0] - profile.name = 'changing_this_to_trigger_sync' - provider._sync_traffic_managers([profile, profile]) - - # it should only be called once for duplicate profiles - tm_sync.assert_called_once() - - def test_find_traffic_managers(self): - provider, zone, record = self._get_dynamic_package() - - # insert a non-matching profile - sample_profile = self._get_tm_profiles(provider)[0] - # dummy record for generating suffix - record2 = Record.new(zone, record.name + '2', data={ - 'type': record._type, - 'ttl': record.ttl, - 'value': record.value, - }) - prefix2 = _root_traffic_manager_name(record2) - tm_id = provider._profile_name_to_id - extra_profile = Profile( - id=tm_id(f'{prefix2}-pool-random'), - name=f'{prefix2}-pool-random', - traffic_routing_method='Weighted', - dns_config=sample_profile.dns_config, - monitor_config=sample_profile.monitor_config, - endpoints=sample_profile.endpoints, - ) - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value.append(extra_profile) - provider._populate_traffic_managers() - - # implicitly asserts that non-matching profile is not included - prefix = _root_traffic_manager_name(record) - self.assertEqual(provider._find_traffic_managers(record), { - prefix, f'{prefix}-pool-two', f'{prefix}-rule-one', - f'{prefix}-rule-two', - }) - - def test_traffic_manager_gc(self): - provider, zone, record = self._get_dynamic_package() - provider._populate_traffic_managers() - - profiles = provider._find_traffic_managers(record) - profile_delete_mock = provider._tm_client.profiles.delete - - provider._traffic_managers_gc(record, profiles) - profile_delete_mock.assert_not_called() - - profile_delete_mock.reset_mock() - remove = list(profiles)[3] - profiles.discard(remove) - - provider._traffic_managers_gc(record, profiles) - profile_delete_mock.assert_has_calls( - [call(provider._resource_group, remove)] - ) - - def test_apply(self): - provider = self._get_provider() - - expected_n = len(octo_records) - half = int(expected_n / 2) - changes = [Create(r) for r in octo_records[:half]] + \ - [Update(r, r) for r in octo_records[half:]] - deletes = [Delete(r) for r in octo_records] - - self.assertEquals(expected_n, provider.apply(Plan(None, zone, - changes, True))) - self.assertEquals(expected_n, provider.apply(Plan(zone, zone, - deletes, True))) - - def test_apply_create_dynamic(self): - provider = self._get_provider() - - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = [] - - tm_sync = provider._tm_client.profiles.create_or_update - - record = self._get_dynamic_record(zone) - - profiles = self._get_tm_profiles(provider) - - provider._apply_Create(Create(record)) - # create was called as many times as number of profiles required for - # the dynamic record - self.assertEqual(tm_sync.call_count, len(profiles)) - - create = provider._dns_client.record_sets.create_or_update - create.assert_called_once() - - def test_apply_update_dynamic(self): - # existing is simple, new is dynamic - provider = self._get_provider() - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value = [] - profiles = self._get_tm_profiles(provider) - dynamic_record = self._get_dynamic_record(zone) - simple_record = Record.new(zone, dynamic_record.name, data={ - 'type': 'CNAME', - 'ttl': 3600, - 'value': 'cname.unit.tests.', - }) - change = Update(simple_record, dynamic_record) - provider._apply_Update(change) - tm_sync, dns_update, tm_delete = ( - provider._tm_client.profiles.create_or_update, - provider._dns_client.record_sets.create_or_update, - provider._tm_client.profiles.delete - ) - self.assertEqual(tm_sync.call_count, len(profiles)) - dns_update.assert_called_once() - tm_delete.assert_not_called() - - # existing is dynamic, new is simple - provider, existing, dynamic_record = self._get_dynamic_package() - profiles = self._get_tm_profiles(provider) - change = Update(dynamic_record, simple_record) - provider._apply_Update(change) - tm_sync, dns_update, tm_delete = ( - provider._tm_client.profiles.create_or_update, - provider._dns_client.record_sets.create_or_update, - provider._tm_client.profiles.delete - ) - tm_sync.assert_not_called() - dns_update.assert_called_once() - self.assertEqual(tm_delete.call_count, len(profiles)) - - # both are dynamic, healthcheck port is changed - provider, existing, dynamic_record = self._get_dynamic_package() - profiles = self._get_tm_profiles(provider) - dynamic_record2 = self._get_dynamic_record(existing) - dynamic_record2._octodns['healthcheck']['port'] += 1 - change = Update(dynamic_record, dynamic_record2) - provider._apply_Update(change) - tm_sync, dns_update, tm_delete = ( - provider._tm_client.profiles.create_or_update, - provider._dns_client.record_sets.create_or_update, - provider._tm_client.profiles.delete - ) - self.assertEqual(tm_sync.call_count, len(profiles)) - dns_update.assert_not_called() - tm_delete.assert_not_called() - - # both are dynamic, extra profile should be deleted - provider, existing, dynamic_record = self._get_dynamic_package() - sample_profile = self._get_tm_profiles(provider)[0] - tm_id = provider._profile_name_to_id - root_profile_name = _root_traffic_manager_name(dynamic_record) - extra_profile = Profile( - id=tm_id(f'{root_profile_name}-pool-random'), - name=f'{root_profile_name}-pool-random', - traffic_routing_method='Weighted', - dns_config=sample_profile.dns_config, - monitor_config=sample_profile.monitor_config, - endpoints=sample_profile.endpoints, - ) - tm_list = provider._tm_client.profiles.list_by_resource_group - tm_list.return_value.append(extra_profile) - change = Update(dynamic_record, dynamic_record) - provider._apply_Update(change) - tm_sync, dns_update, tm_delete = ( - provider._tm_client.profiles.create_or_update, - provider._dns_client.record_sets.create_or_update, - provider._tm_client.profiles.delete - ) - tm_sync.assert_not_called() - dns_update.assert_not_called() - tm_delete.assert_called_once() - - # both are dynamic but alias is broken - provider, existing, record1 = self._get_dynamic_package() - azrecord = RecordSet( - ttl=record1.ttl, target_resource=SubResource(id=None)) - azrecord.name = record1.name or '@' - azrecord.type = f'Microsoft.Network/dnszones/{record1._type}' - - record2 = provider._populate_record(zone, azrecord, lenient=True) - self.assertIsNone(record2.value) - - change = Update(record2, record1) - provider._apply_Update(change) - tm_sync, dns_update, tm_delete = ( - provider._tm_client.profiles.create_or_update, - provider._dns_client.record_sets.create_or_update, - provider._tm_client.profiles.delete - ) - tm_sync.assert_not_called() - dns_update.assert_called_once() - tm_delete.assert_not_called() - - def test_apply_update_dynamic_A(self): - # existing is simple, new is dynamic - provider = self._get_provider() - simple_record = Record.new(zone, 'foo', data={ - 'type': 'A', - 'ttl': 3600, - 'values': ['1.1.1.1', '2.2.2.2'], - }) - dynamic_record = Record.new(zone, simple_record.name, data={ - 'type': 'A', - 'ttl': 60, - 'values': ['1.1.1.1'], - 'dynamic': { - 'pools': { - 'one': { - 'values': [ - {'value': '8.8.8.8'}, - {'value': '4.4.4.4'}, - ], - 'fallback': 'two', - }, - 'two': { - 'values': [{'value': '9.9.9.9'}], - }, - }, - 'rules': [ - {'geos': ['AF'], 'pool': 'two'}, - {'pool': 'one'}, - ], - } - }) - num_tms = len(provider._generate_traffic_managers(dynamic_record)) - change = Update(simple_record, dynamic_record) - provider._apply_Update(change) - tm_sync, dns_update, tm_delete = ( - provider._tm_client.profiles.create_or_update, - provider._dns_client.record_sets.create_or_update, - provider._tm_client.profiles.delete - ) - # sync is called once for each profile, plus 1 at the end for nested - # endpoints to workaround A/AAAA nesting limitation in Azure - self.assertEqual(tm_sync.call_count, num_tms + 1) - dns_update.assert_called_once() - tm_delete.assert_not_called() - - # both are dynamic, healthcheck port is changed to trigger sync on - # all profiles - provider = self._get_provider() - dynamic_record2 = Record.new(zone, dynamic_record.name, data={ - 'type': dynamic_record._type, - 'ttl': 300, - 'values': dynamic_record.values, - 'dynamic': dynamic_record.dynamic._data(), - 'octodns': { - 'healthcheck': {'port': 4433}, - } - }) - change = Update(dynamic_record, dynamic_record2) - provider._apply_Update(change) - tm_sync, dns_update, tm_delete = ( - provider._tm_client.profiles.create_or_update, - provider._dns_client.record_sets.create_or_update, - provider._tm_client.profiles.delete - ) - # sync is called once for each profile, extra call at the end is not - # needed when existing dynamic record is already aliased to its root - # profile - self.assertEqual(tm_sync.call_count, num_tms) - dns_update.assert_not_called() - tm_delete.assert_not_called() - - def test_apply_update_dynamic_A_singluar(self): - # existing is simple, new is dynamic that needs only one profile - provider = self._get_provider() - simple_record = Record.new(zone, 'foo', data={ - 'type': 'A', - 'ttl': 3600, - 'values': ['1.1.1.1', '2.2.2.2'], - }) - dynamic_record = Record.new(zone, simple_record.name, data={ - 'type': 'A', - 'ttl': 60, - 'values': ['1.1.1.1'], - 'dynamic': { - 'pools': { - 'one': { - 'values': [ - {'value': '8.8.8.8'}, - {'value': '1.1.1.1'}, - ], - }, - }, - 'rules': [ - {'pool': 'one'}, - ], - } - }) - num_tms = len(provider._generate_traffic_managers(dynamic_record)) - self.assertEqual(num_tms, 1) - change = Update(simple_record, dynamic_record) - provider._apply_Update(change) - tm_sync, dns_update, tm_delete = ( - provider._tm_client.profiles.create_or_update, - provider._dns_client.record_sets.create_or_update, - provider._tm_client.profiles.delete - ) - self.assertEqual(tm_sync.call_count, num_tms) - dns_update.assert_called_once() - tm_delete.assert_not_called() - - def test_apply_delete_dynamic(self): - provider, existing, record = self._get_dynamic_package() - provider._populate_traffic_managers() - profiles = self._get_tm_profiles(provider) - change = Delete(record) - provider._apply_Delete(change) - dns_delete, tm_delete = ( - provider._dns_client.record_sets.delete, - provider._tm_client.profiles.delete - ) - dns_delete.assert_called_once() - self.assertEqual(tm_delete.call_count, len(profiles)) - - def test_create_zone(self): - provider = self._get_provider() - - changes = [] - for i in octo_records: - changes.append(Create(i)) - desired = Zone('unit2.test.', []) - - err_msg = 'The Resource \'Microsoft.Network/dnszones/unit2.test\' ' - err_msg += 'under resource group \'mock_rg\' was not found.' - _get = provider._dns_client.zones.get - _get.side_effect = CloudError(Mock(status=404), err_msg) - - expected_n = len(octo_records) - self.assertEquals(expected_n, provider.apply(Plan(None, desired, - changes, True))) - - def test_check_zone_no_create(self): - provider = self._get_provider() - - rs = [] - recordSet = RecordSet(a_records=[ARecord(ipv4_address='1.1.1.1')]) - recordSet.name, recordSet.ttl, recordSet.type = 'a1', 0, 'A' - rs.append(recordSet) - recordSet = RecordSet(a_records=[ARecord(ipv4_address='1.1.1.1'), - ARecord(ipv4_address='2.2.2.2')]) - recordSet.name, recordSet.ttl, recordSet.type = 'a2', 1, 'A' - rs.append(recordSet) - - record_list = provider._dns_client.record_sets.list_by_dns_zone - record_list.return_value = rs - - err_msg = 'The Resource \'Microsoft.Network/dnszones/unit3.test\' ' - err_msg += 'under resource group \'mock_rg\' was not found.' - _get = provider._dns_client.zones.get - _get.side_effect = CloudError(Mock(status=404), err_msg) - - exists = provider.populate(Zone('unit3.test.', [])) - self.assertFalse(exists) - - self.assertEquals(len(zone.records), 0) + def test_missing(self): + with self.assertRaises(ModuleNotFoundError): + from octodns.provider.azuredns import AzureProvider + AzureProvider