mirror of
				https://github.com/github/octodns.git
				synced 2024-05-11 05:55:00 +00:00 
			
		
		
		
	Constellix customized healthcheck
This commit is contained in:
		@@ -151,3 +151,26 @@ Support matrix:
 | 
				
			|||||||
        measure_latency: false
 | 
					        measure_latency: false
 | 
				
			||||||
        request_interval: 30
 | 
					        request_interval: 30
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Constellix Health Check Options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Key  | Description | Default |
 | 
				
			||||||
 | 
					|--|--|--|
 | 
				
			||||||
 | 
					| sonar_interval | Sonar check interval [FIVESECONDS|THIRTYSECONDS|ONEMINUTE|TWOMINUTES|THREEMINUTES|FOURMINUTES|FIVEMINUTES|TENMINUTES|THIRTYMINUTES|HALFDAY|DAY] | ONEMINUTE |
 | 
				
			||||||
 | 
					| sonar_port | Sonar check port | 80 |
 | 
				
			||||||
 | 
					| sonar_regions | Sonar check regions for a check. WORLD or a list of [ASIAPAC|EUROPE|NACENTRAL|NAEAST|NAWEST|OCEANIA|SOUTHAMERICA] | WORLD |
 | 
				
			||||||
 | 
					| sonar_type | Sonar check type [TCP|HTTP] | TCP |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```yaml
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					  octodns:
 | 
				
			||||||
 | 
					    constellix:
 | 
				
			||||||
 | 
					      healthcheck:
 | 
				
			||||||
 | 
					        sonar_interval: DAY
 | 
				
			||||||
 | 
					        sonar_port: 80
 | 
				
			||||||
 | 
					        sonar_regions:
 | 
				
			||||||
 | 
					        - ASIAPAC
 | 
				
			||||||
 | 
					        - EUROPE
 | 
				
			||||||
 | 
					        sonar_type: TCP
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function, \
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from collections import defaultdict
 | 
					from collections import defaultdict
 | 
				
			||||||
from requests import Session
 | 
					from requests import Session
 | 
				
			||||||
from base64 import b64encode
 | 
					from base64 import b64encode, standard_b64encode
 | 
				
			||||||
from pycountry_convert import country_alpha2_to_continent_code
 | 
					from pycountry_convert import country_alpha2_to_continent_code
 | 
				
			||||||
import hashlib
 | 
					import hashlib
 | 
				
			||||||
import hmac
 | 
					import hmac
 | 
				
			||||||
@@ -191,6 +191,7 @@ class ConstellixClient(object):
 | 
				
			|||||||
        for pool in pools:
 | 
					        for pool in pools:
 | 
				
			||||||
            if pool['id'] == pool_id:
 | 
					            if pool['id'] == pool_id:
 | 
				
			||||||
                return pool
 | 
					                return pool
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def pool_create(self, data):
 | 
					    def pool_create(self, data):
 | 
				
			||||||
        path = f'/pools/{data.get("type")}'
 | 
					        path = f'/pools/{data.get("type")}'
 | 
				
			||||||
@@ -241,6 +242,7 @@ class ConstellixClient(object):
 | 
				
			|||||||
        for geofilter in geofilters:
 | 
					        for geofilter in geofilters:
 | 
				
			||||||
            if geofilter['id'] == geofilter_id:
 | 
					            if geofilter['id'] == geofilter_id:
 | 
				
			||||||
                return geofilter
 | 
					                return geofilter
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def geofilter_create(self, data):
 | 
					    def geofilter_create(self, data):
 | 
				
			||||||
        path = '/geoFilters'
 | 
					        path = '/geoFilters'
 | 
				
			||||||
@@ -270,6 +272,149 @@ class ConstellixClient(object):
 | 
				
			|||||||
            self._geofilters.pop(geofilter_id, None)
 | 
					            self._geofilters.pop(geofilter_id, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SonarClientException(ProviderException):
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SonarClientBadRequest(SonarClientException):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, resp):
 | 
				
			||||||
 | 
					        errors = resp.text
 | 
				
			||||||
 | 
					        super(SonarClientBadRequest, self).__init__(f'\n  - {errors}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SonarClientUnauthorized(SonarClientException):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        super(SonarClientUnauthorized, self).__init__('Unauthorized')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SonarClientNotFound(SonarClientException):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self):
 | 
				
			||||||
 | 
					        super(SonarClientNotFound, self).__init__('Not Found')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class SonarClient(object):
 | 
				
			||||||
 | 
					    BASE = 'https://api.sonar.constellix.com/rest/api'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, api_key, secret_key, ratelimit_delay=0.0):
 | 
				
			||||||
 | 
					        self.api_key = api_key
 | 
				
			||||||
 | 
					        self.secret_key = secret_key
 | 
				
			||||||
 | 
					        self.ratelimit_delay = ratelimit_delay
 | 
				
			||||||
 | 
					        self._sess = Session()
 | 
				
			||||||
 | 
					        self._agents = None
 | 
				
			||||||
 | 
					        self._checks = {'tcp': None, 'http': None}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _current_time(self):
 | 
				
			||||||
 | 
					        return str(int(time.time() * 1000))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _hmac_hash(self, now):
 | 
				
			||||||
 | 
					        digester = hmac.new(
 | 
				
			||||||
 | 
					            bytes(self.secret_key, "UTF-8"),
 | 
				
			||||||
 | 
					            bytes(now, "UTF-8"),
 | 
				
			||||||
 | 
					            hashlib.sha1)
 | 
				
			||||||
 | 
					        signature = digester.digest()
 | 
				
			||||||
 | 
					        hmac_text = str(standard_b64encode(signature), "UTF-8")
 | 
				
			||||||
 | 
					        return hmac_text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _request(self, method, path, params=None, data=None):
 | 
				
			||||||
 | 
					        now = self._current_time()
 | 
				
			||||||
 | 
					        hmac_text = self._hmac_hash(now)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        headers = {
 | 
				
			||||||
 | 
					            'x-cns-security-token': "{}:{}:{}".format(
 | 
				
			||||||
 | 
					                self.api_key,
 | 
				
			||||||
 | 
					                hmac_text,
 | 
				
			||||||
 | 
					                now),
 | 
				
			||||||
 | 
					            'Content-Type': "application/json"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        url = f'{self.BASE}{path}'
 | 
				
			||||||
 | 
					        resp = self._sess.request(method, url, headers=headers,
 | 
				
			||||||
 | 
					                                  params=params, json=data)
 | 
				
			||||||
 | 
					        if resp.status_code == 400:
 | 
				
			||||||
 | 
					            raise SonarClientBadRequest(resp)
 | 
				
			||||||
 | 
					        if resp.status_code == 401:
 | 
				
			||||||
 | 
					            raise SonarClientUnauthorized()
 | 
				
			||||||
 | 
					        if resp.status_code == 404:
 | 
				
			||||||
 | 
					            raise SonarClientNotFound()
 | 
				
			||||||
 | 
					        resp.raise_for_status()
 | 
				
			||||||
 | 
					        time.sleep(self.ratelimit_delay)
 | 
				
			||||||
 | 
					        return resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @property
 | 
				
			||||||
 | 
					    def agents(self):
 | 
				
			||||||
 | 
					        if self._agents is None:
 | 
				
			||||||
 | 
					            agents = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            resp = self._request('GET', '/system/sites').json()
 | 
				
			||||||
 | 
					            agents += resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self._agents = {f'{a["name"]}.': a for a in agents}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self._agents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def agents_for_regions(self, regions):
 | 
				
			||||||
 | 
					        if regions[0] == "WORLD":
 | 
				
			||||||
 | 
					            res_agents = []
 | 
				
			||||||
 | 
					            for agent in self.agents.values():
 | 
				
			||||||
 | 
					                res_agents.append(agent['id'])
 | 
				
			||||||
 | 
					            return res_agents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res_agents = []
 | 
				
			||||||
 | 
					        for agent in self.agents.values():
 | 
				
			||||||
 | 
					            if agent["region"] in regions:
 | 
				
			||||||
 | 
					                res_agents.append(agent['id'])
 | 
				
			||||||
 | 
					        return res_agents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def parse_uri_id(self, url):
 | 
				
			||||||
 | 
					        r = str(url).rfind("/")
 | 
				
			||||||
 | 
					        res = str(url)[r + 1:]
 | 
				
			||||||
 | 
					        return res
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def checks(self, check_type):
 | 
				
			||||||
 | 
					        if self._checks[check_type] is None:
 | 
				
			||||||
 | 
					            self._checks[check_type] = {}
 | 
				
			||||||
 | 
					            path = f'/{check_type}'
 | 
				
			||||||
 | 
					            response = self._request('GET', path).json()
 | 
				
			||||||
 | 
					            for check in response:
 | 
				
			||||||
 | 
					                self._checks[check_type][check['id']] = check
 | 
				
			||||||
 | 
					        return self._checks[check_type].values()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check(self, check_type, check_name):
 | 
				
			||||||
 | 
					        checks = self.checks(check_type)
 | 
				
			||||||
 | 
					        for check in checks:
 | 
				
			||||||
 | 
					            if check['name'] == check_name:
 | 
				
			||||||
 | 
					                return check
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_create(self, check_type, data):
 | 
				
			||||||
 | 
					        path = f'/{check_type}'
 | 
				
			||||||
 | 
					        response = self._request('POST', path, data=data)
 | 
				
			||||||
 | 
					        # Parse check ID from Location response header
 | 
				
			||||||
 | 
					        id = self.parse_uri_id(response.headers["Location"])
 | 
				
			||||||
 | 
					        # Get check details
 | 
				
			||||||
 | 
					        path = f'/{check_type}/{id}'
 | 
				
			||||||
 | 
					        response = self._request('GET', path, data=data).json()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Update our cache
 | 
				
			||||||
 | 
					        self._checks[check_type]['id'] = response
 | 
				
			||||||
 | 
					        return response
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def check_delete(self, check_id):
 | 
				
			||||||
 | 
					        # first get check type
 | 
				
			||||||
 | 
					        path = f'/check/type/{check_id}'
 | 
				
			||||||
 | 
					        response = self._request('GET', path).json()
 | 
				
			||||||
 | 
					        check_type = response['type'].lower()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        path = f'/{check_type}/{check_id}'
 | 
				
			||||||
 | 
					        self._request('DELETE', path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Update our cache
 | 
				
			||||||
 | 
					        self._checks[check_type].pop(check_id, None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConstellixProvider(BaseProvider):
 | 
					class ConstellixProvider(BaseProvider):
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    Constellix DNS provider
 | 
					    Constellix DNS provider
 | 
				
			||||||
@@ -295,6 +440,7 @@ class ConstellixProvider(BaseProvider):
 | 
				
			|||||||
        self.log.debug('__init__: id=%s, api_key=***, secret_key=***', id)
 | 
					        self.log.debug('__init__: id=%s, api_key=***, secret_key=***', id)
 | 
				
			||||||
        super(ConstellixProvider, self).__init__(id, *args, **kwargs)
 | 
					        super(ConstellixProvider, self).__init__(id, *args, **kwargs)
 | 
				
			||||||
        self._client = ConstellixClient(api_key, secret_key, ratelimit_delay)
 | 
					        self._client = ConstellixClient(api_key, secret_key, ratelimit_delay)
 | 
				
			||||||
 | 
					        self._sonar = SonarClient(api_key, secret_key, ratelimit_delay)
 | 
				
			||||||
        self._zone_records = {}
 | 
					        self._zone_records = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _data_for_multiple(self, _type, records):
 | 
					    def _data_for_multiple(self, _type, records):
 | 
				
			||||||
@@ -511,6 +657,27 @@ class ConstellixProvider(BaseProvider):
 | 
				
			|||||||
                      len(zone.records) - before, exists)
 | 
					                      len(zone.records) - before, exists)
 | 
				
			||||||
        return exists
 | 
					        return exists
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _healthcheck_config(self, record):
 | 
				
			||||||
 | 
					        sonar_healthcheck = record._octodns.get('constellix', {}) \
 | 
				
			||||||
 | 
					            .get('healthcheck', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if sonar_healthcheck is None:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        healthcheck = {}
 | 
				
			||||||
 | 
					        healthcheck["sonar_port"] = sonar_healthcheck.get('sonar_port', 80)
 | 
				
			||||||
 | 
					        healthcheck["sonar_type"] = sonar_healthcheck.get('sonar_type', "TCP")
 | 
				
			||||||
 | 
					        healthcheck["sonar_regions"] = sonar_healthcheck.get(
 | 
				
			||||||
 | 
					            'sonar_regions',
 | 
				
			||||||
 | 
					            ["WORLD"]
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        healthcheck["sonar_interval"] = sonar_healthcheck.get(
 | 
				
			||||||
 | 
					            'sonar_interval',
 | 
				
			||||||
 | 
					            "ONEMINUTE"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return healthcheck
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _params_for_multiple(self, record):
 | 
					    def _params_for_multiple(self, record):
 | 
				
			||||||
        yield {
 | 
					        yield {
 | 
				
			||||||
            'name': record.name,
 | 
					            'name': record.name,
 | 
				
			||||||
@@ -609,6 +776,8 @@ class ConstellixProvider(BaseProvider):
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _handle_pools(self, record):
 | 
					    def _handle_pools(self, record):
 | 
				
			||||||
 | 
					        healthcheck = self._healthcheck_config(record)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # If we don't have dynamic, then there's no pools
 | 
					        # If we don't have dynamic, then there's no pools
 | 
				
			||||||
        if not getattr(record, 'dynamic', False):
 | 
					        if not getattr(record, 'dynamic', False):
 | 
				
			||||||
            return []
 | 
					            return []
 | 
				
			||||||
@@ -629,6 +798,26 @@ class ConstellixProvider(BaseProvider):
 | 
				
			|||||||
            generated_pool_name = \
 | 
					            generated_pool_name = \
 | 
				
			||||||
                f'{record.zone.name}:{record.name}:{record._type}:{pool_name}'
 | 
					                f'{record.zone.name}:{record.name}:{record._type}:{pool_name}'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Create Sonar checks if needed
 | 
				
			||||||
 | 
					            if healthcheck is not None:
 | 
				
			||||||
 | 
					                check_sites = self._sonar.\
 | 
				
			||||||
 | 
					                    agents_for_regions(healthcheck["sonar_regions"])
 | 
				
			||||||
 | 
					                for value in values:
 | 
				
			||||||
 | 
					                    check_obj = self._create_update_check(
 | 
				
			||||||
 | 
					                        pool_type = record._type,
 | 
				
			||||||
 | 
					                        check_name = '{}-{}'.format(
 | 
				
			||||||
 | 
					                            generated_pool_name,
 | 
				
			||||||
 | 
					                            value['value']
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        check_type = healthcheck["sonar_type"].lower(),
 | 
				
			||||||
 | 
					                        value = value['value'],
 | 
				
			||||||
 | 
					                        port = healthcheck["sonar_port"],
 | 
				
			||||||
 | 
					                        interval = healthcheck["sonar_interval"],
 | 
				
			||||||
 | 
					                        sites = check_sites
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    value['checkId'] = check_obj['id']
 | 
				
			||||||
 | 
					                    value['policy'] = "followsonar"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # OK, pool is valid, let's create it or update it
 | 
					            # OK, pool is valid, let's create it or update it
 | 
				
			||||||
            self.log.debug("Creating pool %s", generated_pool_name)
 | 
					            self.log.debug("Creating pool %s", generated_pool_name)
 | 
				
			||||||
            pool_obj = self._create_update_pool(
 | 
					            pool_obj = self._create_update_pool(
 | 
				
			||||||
@@ -677,6 +866,37 @@ class ConstellixProvider(BaseProvider):
 | 
				
			|||||||
            res_pools.append(pool_obj)
 | 
					            res_pools.append(pool_obj)
 | 
				
			||||||
        return res_pools
 | 
					        return res_pools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _create_update_check(
 | 
				
			||||||
 | 
					            self,
 | 
				
			||||||
 | 
					            pool_type,
 | 
				
			||||||
 | 
					            check_name,
 | 
				
			||||||
 | 
					            check_type,
 | 
				
			||||||
 | 
					            value,
 | 
				
			||||||
 | 
					            port,
 | 
				
			||||||
 | 
					            interval,
 | 
				
			||||||
 | 
					            sites):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        check = {
 | 
				
			||||||
 | 
					            'name': check_name,
 | 
				
			||||||
 | 
					            'host': value,
 | 
				
			||||||
 | 
					            'port': port,
 | 
				
			||||||
 | 
					            'checkSites': sites,
 | 
				
			||||||
 | 
					            'interval': interval
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if pool_type == "AAAA":
 | 
				
			||||||
 | 
					            check['ipVersion'] = "IPV6"
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            check['ipVersion'] = "IPV4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if check_type == "http":
 | 
				
			||||||
 | 
					            check['protocolType'] = "HTTPS"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        existing_check = self._sonar.check(check_type, check_name)
 | 
				
			||||||
 | 
					        if existing_check:
 | 
				
			||||||
 | 
					            self._sonar.check_delete(existing_check['id'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return self._sonar.check_create(check_type, check)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _create_update_pool(self, pool_name, pool_type, ttl, values):
 | 
					    def _create_update_pool(self, pool_name, pool_type, ttl, values):
 | 
				
			||||||
        pool = {
 | 
					        pool = {
 | 
				
			||||||
            'name': pool_name,
 | 
					            'name': pool_name,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@
 | 
				
			|||||||
from __future__ import absolute_import, division, print_function, \
 | 
					from __future__ import absolute_import, division, print_function, \
 | 
				
			||||||
    unicode_literals
 | 
					    unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from mock import Mock, call
 | 
					from mock import Mock, PropertyMock, call
 | 
				
			||||||
from os.path import dirname, join
 | 
					from os.path import dirname, join
 | 
				
			||||||
from requests import HTTPError
 | 
					from requests import HTTPError
 | 
				
			||||||
from requests_mock import ANY, mock as requests_mock
 | 
					from requests_mock import ANY, mock as requests_mock
 | 
				
			||||||
@@ -72,6 +72,141 @@ class TestConstellixProvider(TestCase):
 | 
				
			|||||||
            expected._remove_record(record)
 | 
					            expected._remove_record(record)
 | 
				
			||||||
            break
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expected_healthcheck = Zone('unit.tests.', [])
 | 
				
			||||||
 | 
					    source = YamlProvider('test', join(dirname(__file__), 'config'))
 | 
				
			||||||
 | 
					    source.populate(expected_healthcheck)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Our test suite differs a bit, add our NS and remove the simple one
 | 
				
			||||||
 | 
					    expected_healthcheck.add_record(Record.new(expected_healthcheck, 'under', {
 | 
				
			||||||
 | 
					        'ttl': 3600,
 | 
				
			||||||
 | 
					        'type': 'NS',
 | 
				
			||||||
 | 
					        'values': [
 | 
				
			||||||
 | 
					            'ns1.unit.tests.',
 | 
				
			||||||
 | 
					            'ns2.unit.tests.',
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add some ALIAS records
 | 
				
			||||||
 | 
					    expected_healthcheck.add_record(Record.new(expected_healthcheck, '', {
 | 
				
			||||||
 | 
					        'ttl': 1800,
 | 
				
			||||||
 | 
					        'type': 'ALIAS',
 | 
				
			||||||
 | 
					        'value': 'aname.unit.tests.'
 | 
				
			||||||
 | 
					    }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add a dynamic record
 | 
				
			||||||
 | 
					    expected_healthcheck.add_record(
 | 
				
			||||||
 | 
					        Record.new(expected_healthcheck, 'www.dynamic', {
 | 
				
			||||||
 | 
					            'ttl': 300,
 | 
				
			||||||
 | 
					            'type': 'A',
 | 
				
			||||||
 | 
					            'values': [
 | 
				
			||||||
 | 
					                '1.2.3.4',
 | 
				
			||||||
 | 
					                '1.2.3.5'
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            'dynamic': {
 | 
				
			||||||
 | 
					                'pools': {
 | 
				
			||||||
 | 
					                    'two': {
 | 
				
			||||||
 | 
					                        'values': [{
 | 
				
			||||||
 | 
					                            'value': '1.2.3.4',
 | 
				
			||||||
 | 
					                            'weight': 1
 | 
				
			||||||
 | 
					                        }, {
 | 
				
			||||||
 | 
					                            'value': '1.2.3.5',
 | 
				
			||||||
 | 
					                            'weight': 1
 | 
				
			||||||
 | 
					                        }],
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                'rules': [{
 | 
				
			||||||
 | 
					                    'pool': 'two',
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            'octodns': {
 | 
				
			||||||
 | 
					                'constellix': {
 | 
				
			||||||
 | 
					                    'healthcheck': {
 | 
				
			||||||
 | 
					                        'sonar_port': 80,
 | 
				
			||||||
 | 
					                        'sonar_regions': [
 | 
				
			||||||
 | 
					                            'ASIAPAC',
 | 
				
			||||||
 | 
					                            'EUROPE'
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        'sonar_type': 'TCP'
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for record in list(expected_healthcheck.records):
 | 
				
			||||||
 | 
					        if record.name == 'sub' and record._type == 'NS':
 | 
				
			||||||
 | 
					            expected_healthcheck._remove_record(record)
 | 
				
			||||||
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expected_healthcheck_world = Zone('unit.tests.', [])
 | 
				
			||||||
 | 
					    source = YamlProvider('test', join(dirname(__file__), 'config'))
 | 
				
			||||||
 | 
					    source.populate(expected_healthcheck_world)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Our test suite differs a bit, add our NS and remove the simple one
 | 
				
			||||||
 | 
					    expected_healthcheck_world.add_record(
 | 
				
			||||||
 | 
					        Record.new(expected_healthcheck_world, 'under', {
 | 
				
			||||||
 | 
					            'ttl': 3600,
 | 
				
			||||||
 | 
					            'type': 'NS',
 | 
				
			||||||
 | 
					            'values': [
 | 
				
			||||||
 | 
					                'ns1.unit.tests.',
 | 
				
			||||||
 | 
					                'ns2.unit.tests.',
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add some ALIAS records
 | 
				
			||||||
 | 
					    expected_healthcheck_world.add_record(
 | 
				
			||||||
 | 
					        Record.new(expected_healthcheck_world, '', {
 | 
				
			||||||
 | 
					            'ttl': 1800,
 | 
				
			||||||
 | 
					            'type': 'ALIAS',
 | 
				
			||||||
 | 
					            'value': 'aname.unit.tests.'
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Add a dynamic record
 | 
				
			||||||
 | 
					    expected_healthcheck_world.add_record(
 | 
				
			||||||
 | 
					        Record.new(expected_healthcheck_world, 'www.dynamic', {
 | 
				
			||||||
 | 
					            'ttl': 300,
 | 
				
			||||||
 | 
					            'type': 'AAAA',
 | 
				
			||||||
 | 
					            'values': [
 | 
				
			||||||
 | 
					                '2601:644:500:e210:62f8:1dff:feb8:947a',
 | 
				
			||||||
 | 
					                '2601:642:500:e210:62f8:1dff:feb8:947a'
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            'dynamic': {
 | 
				
			||||||
 | 
					                'pools': {
 | 
				
			||||||
 | 
					                    'two': {
 | 
				
			||||||
 | 
					                        'values': [{
 | 
				
			||||||
 | 
					                            'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
 | 
				
			||||||
 | 
					                            'weight': 1
 | 
				
			||||||
 | 
					                        }, {
 | 
				
			||||||
 | 
					                            'value': '2601:642:500:e210:62f8:1dff:feb8:947a',
 | 
				
			||||||
 | 
					                            'weight': 1
 | 
				
			||||||
 | 
					                        }],
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                'rules': [{
 | 
				
			||||||
 | 
					                    'pool': 'two',
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            'octodns': {
 | 
				
			||||||
 | 
					                'constellix': {
 | 
				
			||||||
 | 
					                    'healthcheck': {
 | 
				
			||||||
 | 
					                        'sonar_port': 80,
 | 
				
			||||||
 | 
					                        'sonar_regions': [
 | 
				
			||||||
 | 
					                            'WORLD'
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                        'sonar_type': 'HTTP'
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for record in list(expected_healthcheck_world.records):
 | 
				
			||||||
 | 
					        if record.name == 'sub' and record._type == 'NS':
 | 
				
			||||||
 | 
					            expected_healthcheck_world._remove_record(record)
 | 
				
			||||||
 | 
					            break
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    expected_dynamic = Zone('unit.tests.', [])
 | 
					    expected_dynamic = Zone('unit.tests.', [])
 | 
				
			||||||
    source = YamlProvider('test', join(dirname(__file__), 'config'))
 | 
					    source = YamlProvider('test', join(dirname(__file__), 'config'))
 | 
				
			||||||
    source.populate(expected_dynamic)
 | 
					    source.populate(expected_dynamic)
 | 
				
			||||||
@@ -157,6 +292,14 @@ class TestConstellixProvider(TestCase):
 | 
				
			|||||||
                provider.populate(zone)
 | 
					                provider.populate(zone)
 | 
				
			||||||
            self.assertEquals('Unauthorized', str(ctx.exception))
 | 
					            self.assertEquals('Unauthorized', str(ctx.exception))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with requests_mock() as mock:
 | 
				
			||||||
 | 
					            mock.get(ANY, status_code=401,
 | 
				
			||||||
 | 
					                     text='{"errors": ["Unable to authenticate token"]}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            with self.assertRaises(Exception) as ctx:
 | 
				
			||||||
 | 
					                provider._sonar.agents
 | 
				
			||||||
 | 
					            self.assertEquals('Unauthorized', str(ctx.exception))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Bad request
 | 
					        # Bad request
 | 
				
			||||||
        with requests_mock() as mock:
 | 
					        with requests_mock() as mock:
 | 
				
			||||||
            mock.get(ANY, status_code=400,
 | 
					            mock.get(ANY, status_code=400,
 | 
				
			||||||
@@ -169,6 +312,15 @@ class TestConstellixProvider(TestCase):
 | 
				
			|||||||
            self.assertEquals('\n  - "unittests" is not a valid domain name',
 | 
					            self.assertEquals('\n  - "unittests" is not a valid domain name',
 | 
				
			||||||
                              str(ctx.exception))
 | 
					                              str(ctx.exception))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with requests_mock() as mock:
 | 
				
			||||||
 | 
					            mock.get(ANY, status_code=400,
 | 
				
			||||||
 | 
					                     text='error text')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            with self.assertRaises(Exception) as ctx:
 | 
				
			||||||
 | 
					                provider._sonar.agents
 | 
				
			||||||
 | 
					            self.assertEquals('\n  - error text',
 | 
				
			||||||
 | 
					                              str(ctx.exception))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # General error
 | 
					        # General error
 | 
				
			||||||
        with requests_mock() as mock:
 | 
					        with requests_mock() as mock:
 | 
				
			||||||
            mock.get(ANY, status_code=502, text='Things caught fire')
 | 
					            mock.get(ANY, status_code=502, text='Things caught fire')
 | 
				
			||||||
@@ -187,6 +339,19 @@ class TestConstellixProvider(TestCase):
 | 
				
			|||||||
            provider.populate(zone)
 | 
					            provider.populate(zone)
 | 
				
			||||||
            self.assertEquals(set(), zone.records)
 | 
					            self.assertEquals(set(), zone.records)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with requests_mock() as mock:
 | 
				
			||||||
 | 
					            mock.get(ANY, status_code=404, text='')
 | 
				
			||||||
 | 
					            with self.assertRaises(Exception) as ctx:
 | 
				
			||||||
 | 
					                provider._sonar.agents
 | 
				
			||||||
 | 
					            self.assertEquals('Not Found', str(ctx.exception))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Sonar Normal response
 | 
				
			||||||
 | 
					        with requests_mock() as mock:
 | 
				
			||||||
 | 
					            mock.get(ANY, status_code=200, text='[]')
 | 
				
			||||||
 | 
					            agents = provider._sonar.agents
 | 
				
			||||||
 | 
					            self.assertEquals({}, agents)
 | 
				
			||||||
 | 
					            agents = provider._sonar.agents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # No diffs == no changes
 | 
					        # No diffs == no changes
 | 
				
			||||||
        with requests_mock() as mock:
 | 
					        with requests_mock() as mock:
 | 
				
			||||||
            base = 'https://api.dns.constellix.com/v1'
 | 
					            base = 'https://api.dns.constellix.com/v1'
 | 
				
			||||||
@@ -426,7 +591,524 @@ class TestConstellixProvider(TestCase):
 | 
				
			|||||||
            call('DELETE', '/domains/123123/records/ANAME/11189899'),
 | 
					            call('DELETE', '/domains/123123/records/ANAME/11189899'),
 | 
				
			||||||
        ], any_order=True)
 | 
					        ], any_order=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_apply_dunamic(self):
 | 
					    def test_apply_healthcheck(self):
 | 
				
			||||||
 | 
					        provider = ConstellixProvider('test', 'api', 'secret')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        resp = Mock()
 | 
				
			||||||
 | 
					        resp.json = Mock()
 | 
				
			||||||
 | 
					        provider._client._request = Mock(return_value=resp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # non-existent domain, create everything
 | 
				
			||||||
 | 
					        resp.json.side_effect = [
 | 
				
			||||||
 | 
					            [],  # no domains returned during populate
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                'id': 123123,
 | 
				
			||||||
 | 
					                'name': 'unit.tests'
 | 
				
			||||||
 | 
					            }],  # domain created in apply
 | 
				
			||||||
 | 
					            [],  # No pools returned during populate
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                "id": 1808520,
 | 
				
			||||||
 | 
					                "name": "unit.tests.:www.dynamic:A:two",
 | 
				
			||||||
 | 
					            }]   # pool created in apply
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sonar_resp = Mock()
 | 
				
			||||||
 | 
					        sonar_resp.json = Mock()
 | 
				
			||||||
 | 
					        type(sonar_resp).headers = PropertyMock(return_value={
 | 
				
			||||||
 | 
					            "Location": "http://api.sonar.constellix.com/rest/api/tcp/52906"
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        sonar_resp.headers = Mock()
 | 
				
			||||||
 | 
					        provider._sonar._request = Mock(return_value=sonar_resp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sonar_resp.json.side_effect = [
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                "id": 1,
 | 
				
			||||||
 | 
					                "name": "USWAS01",
 | 
				
			||||||
 | 
					                "label": "Site 1",
 | 
				
			||||||
 | 
					                "location": "Washington, DC, U.S.A",
 | 
				
			||||||
 | 
					                "country": "U.S.A",
 | 
				
			||||||
 | 
					                "region": "ASIAPAC"
 | 
				
			||||||
 | 
					            }, {
 | 
				
			||||||
 | 
					                "id": 23,
 | 
				
			||||||
 | 
					                "name": "CATOR01",
 | 
				
			||||||
 | 
					                "label": "Site 1",
 | 
				
			||||||
 | 
					                "location": "Toronto,Canada",
 | 
				
			||||||
 | 
					                "country": "Canada",
 | 
				
			||||||
 | 
					                "region": "EUROPE"
 | 
				
			||||||
 | 
					            }, {
 | 
				
			||||||
 | 
					                "id": 25,
 | 
				
			||||||
 | 
					                "name": "CATOR01",
 | 
				
			||||||
 | 
					                "label": "Site 1",
 | 
				
			||||||
 | 
					                "location": "Toronto,Canada",
 | 
				
			||||||
 | 
					                "country": "Canada",
 | 
				
			||||||
 | 
					                "region": "OCEANIA"
 | 
				
			||||||
 | 
					            }],  # available agents
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                "id": 52,
 | 
				
			||||||
 | 
					                "name": "unit.tests.:www.dynamic:A:two-1.2.3.4"
 | 
				
			||||||
 | 
					            }],  # initial checks
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "type": 'TCP'
 | 
				
			||||||
 | 
					            },  # check type
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "id": 52906,
 | 
				
			||||||
 | 
					                "name": "unit.tests.:www.dynamic:A:two-1.2.3.4"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "id": 52907,
 | 
				
			||||||
 | 
					                "name": "unit.tests.:www.dynamic:A:two-1.2.3.5"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        plan = provider.plan(self.expected_healthcheck)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # No root NS, no ignored, no excluded, no unsupported
 | 
				
			||||||
 | 
					        n = len(self.expected_healthcheck.records) - 8
 | 
				
			||||||
 | 
					        self.assertEquals(n, len(plan.changes))
 | 
				
			||||||
 | 
					        self.assertEquals(n, provider.apply(plan))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        provider._client._request.assert_has_calls([
 | 
				
			||||||
 | 
					            # get all domains to build the cache
 | 
				
			||||||
 | 
					            call('GET', '/domains'),
 | 
				
			||||||
 | 
					            # created the domain
 | 
				
			||||||
 | 
					            call('POST', '/domains', data={'names': ['unit.tests']})
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check we tried to get our pool
 | 
				
			||||||
 | 
					        provider._client._request.assert_has_calls([
 | 
				
			||||||
 | 
					            # get all pools to build the cache
 | 
				
			||||||
 | 
					            call('GET', '/pools/A'),
 | 
				
			||||||
 | 
					            # created the pool
 | 
				
			||||||
 | 
					            call('POST', '/pools/A', data={
 | 
				
			||||||
 | 
					                'name': 'unit.tests.:www.dynamic:A:two',
 | 
				
			||||||
 | 
					                'type': 'A',
 | 
				
			||||||
 | 
					                'numReturn': 1,
 | 
				
			||||||
 | 
					                'minAvailableFailover': 1,
 | 
				
			||||||
 | 
					                'ttl': 300,
 | 
				
			||||||
 | 
					                'values': [{
 | 
				
			||||||
 | 
					                    "value": "1.2.3.4",
 | 
				
			||||||
 | 
					                    "weight": 1,
 | 
				
			||||||
 | 
					                    "checkId": 52906,
 | 
				
			||||||
 | 
					                    "policy": 'followsonar'
 | 
				
			||||||
 | 
					                }, {
 | 
				
			||||||
 | 
					                    "value": "1.2.3.5",
 | 
				
			||||||
 | 
					                    "weight": 1,
 | 
				
			||||||
 | 
					                    "checkId": 52907,
 | 
				
			||||||
 | 
					                    "policy": 'followsonar'
 | 
				
			||||||
 | 
					                }]
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # These two checks are broken up so that ordering doesn't break things.
 | 
				
			||||||
 | 
					        # Python3 doesn't make the calls in a consistent order so different
 | 
				
			||||||
 | 
					        # things follow the GET / on different runs
 | 
				
			||||||
 | 
					        provider._client._request.assert_has_calls([
 | 
				
			||||||
 | 
					            call('POST', '/domains/123123/records/SRV', data={
 | 
				
			||||||
 | 
					                'roundRobin': [{
 | 
				
			||||||
 | 
					                    'priority': 10,
 | 
				
			||||||
 | 
					                    'weight': 20,
 | 
				
			||||||
 | 
					                    'value': 'foo-1.unit.tests.',
 | 
				
			||||||
 | 
					                    'port': 30
 | 
				
			||||||
 | 
					                }, {
 | 
				
			||||||
 | 
					                    'priority': 12,
 | 
				
			||||||
 | 
					                    'weight': 20,
 | 
				
			||||||
 | 
					                    'value': 'foo-2.unit.tests.',
 | 
				
			||||||
 | 
					                    'port': 30
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					                'name': '_srv._tcp',
 | 
				
			||||||
 | 
					                'ttl': 600,
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEquals(22, provider._client._request.call_count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        provider._client._request.reset_mock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        provider._client.records = Mock(return_value=[
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'id': 11189897,
 | 
				
			||||||
 | 
					                'type': 'A',
 | 
				
			||||||
 | 
					                'name': 'www',
 | 
				
			||||||
 | 
					                'ttl': 300,
 | 
				
			||||||
 | 
					                'recordOption': 'roundRobin',
 | 
				
			||||||
 | 
					                'value': [
 | 
				
			||||||
 | 
					                    '1.2.3.4',
 | 
				
			||||||
 | 
					                    '2.2.3.4',
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            }, {
 | 
				
			||||||
 | 
					                'id': 11189898,
 | 
				
			||||||
 | 
					                'type': 'A',
 | 
				
			||||||
 | 
					                'name': 'ttl',
 | 
				
			||||||
 | 
					                'ttl': 600,
 | 
				
			||||||
 | 
					                'recordOption': 'roundRobin',
 | 
				
			||||||
 | 
					                'value': [
 | 
				
			||||||
 | 
					                    '3.2.3.4'
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            },  {
 | 
				
			||||||
 | 
					                'id': 11189899,
 | 
				
			||||||
 | 
					                'type': 'ALIAS',
 | 
				
			||||||
 | 
					                'name': 'alias',
 | 
				
			||||||
 | 
					                'ttl': 600,
 | 
				
			||||||
 | 
					                'recordOption': 'roundRobin',
 | 
				
			||||||
 | 
					                'value': [{
 | 
				
			||||||
 | 
					                    'value': 'aname.unit.tests.'
 | 
				
			||||||
 | 
					                }]
 | 
				
			||||||
 | 
					            }, {
 | 
				
			||||||
 | 
					                "id": 1808520,
 | 
				
			||||||
 | 
					                "type": "A",
 | 
				
			||||||
 | 
					                "name": "www.dynamic",
 | 
				
			||||||
 | 
					                "geolocation": None,
 | 
				
			||||||
 | 
					                "recordOption": "pools",
 | 
				
			||||||
 | 
					                "ttl": 300,
 | 
				
			||||||
 | 
					                "value": [],
 | 
				
			||||||
 | 
					                "pools": [
 | 
				
			||||||
 | 
					                    1808521
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        provider._client.pools = Mock(return_value=[{
 | 
				
			||||||
 | 
					            "id": 1808521,
 | 
				
			||||||
 | 
					            "name": "unit.tests.:www.dynamic:A:two",
 | 
				
			||||||
 | 
					            "type": "A",
 | 
				
			||||||
 | 
					            "values": [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "value": "1.2.3.4",
 | 
				
			||||||
 | 
					                    "weight": 1
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "value": "1.2.3.5",
 | 
				
			||||||
 | 
					                    "weight": 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Domain exists, we don't care about return
 | 
				
			||||||
 | 
					        resp.json.side_effect = [
 | 
				
			||||||
 | 
					            [],  # no domains returned during populate
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                'id': 123123,
 | 
				
			||||||
 | 
					                'name': 'unit.tests'
 | 
				
			||||||
 | 
					            }],  # domain created in apply
 | 
				
			||||||
 | 
					            [],  # No pools returned during populate
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                "id": 1808521,
 | 
				
			||||||
 | 
					                "name": "unit.tests.:www.dynamic:A:one"
 | 
				
			||||||
 | 
					            }]  # pool created in apply
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        wanted = Zone('unit.tests.', [])
 | 
				
			||||||
 | 
					        wanted.add_record(Record.new(wanted, 'ttl', {
 | 
				
			||||||
 | 
					            'ttl': 300,
 | 
				
			||||||
 | 
					            'type': 'A',
 | 
				
			||||||
 | 
					            'value': '3.2.3.4'
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        wanted.add_record(Record.new(wanted, 'www.dynamic', {
 | 
				
			||||||
 | 
					            'ttl': 300,
 | 
				
			||||||
 | 
					            'type': 'A',
 | 
				
			||||||
 | 
					            'values': [
 | 
				
			||||||
 | 
					                '1.2.3.4'
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            'dynamic': {
 | 
				
			||||||
 | 
					                'pools': {
 | 
				
			||||||
 | 
					                    'two': {
 | 
				
			||||||
 | 
					                        'values': [{
 | 
				
			||||||
 | 
					                            'value': '1.2.3.4',
 | 
				
			||||||
 | 
					                            'weight': 1
 | 
				
			||||||
 | 
					                        }],
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                'rules': [{
 | 
				
			||||||
 | 
					                    'pool': 'two',
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        plan = provider.plan(wanted)
 | 
				
			||||||
 | 
					        self.assertEquals(4, len(plan.changes))
 | 
				
			||||||
 | 
					        self.assertEquals(4, provider.apply(plan))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # recreate for update, and deletes for the 2 parts of the other
 | 
				
			||||||
 | 
					        provider._client._request.assert_has_calls([
 | 
				
			||||||
 | 
					            call('POST', '/domains/123123/records/A', data={
 | 
				
			||||||
 | 
					                'roundRobin': [{
 | 
				
			||||||
 | 
					                    'value': '3.2.3.4'
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					                'name': 'ttl',
 | 
				
			||||||
 | 
					                'ttl': 300
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            call('PUT', '/pools/A/1808521', data={
 | 
				
			||||||
 | 
					                'name': 'unit.tests.:www.dynamic:A:two',
 | 
				
			||||||
 | 
					                'type': 'A',
 | 
				
			||||||
 | 
					                'numReturn': 1,
 | 
				
			||||||
 | 
					                'minAvailableFailover': 1,
 | 
				
			||||||
 | 
					                'ttl': 300,
 | 
				
			||||||
 | 
					                'values': [{
 | 
				
			||||||
 | 
					                    "value": "1.2.3.4",
 | 
				
			||||||
 | 
					                    "weight": 1
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					                'id': 1808521,
 | 
				
			||||||
 | 
					                'geofilter': 1
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            call('DELETE', '/domains/123123/records/A/11189897'),
 | 
				
			||||||
 | 
					            call('DELETE', '/domains/123123/records/A/11189898'),
 | 
				
			||||||
 | 
					            call('DELETE', '/domains/123123/records/ANAME/11189899'),
 | 
				
			||||||
 | 
					        ], any_order=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_apply_healthcheck_world(self):
 | 
				
			||||||
 | 
					        provider = ConstellixProvider('test', 'api', 'secret')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        resp = Mock()
 | 
				
			||||||
 | 
					        resp.json = Mock()
 | 
				
			||||||
 | 
					        provider._client._request = Mock(return_value=resp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # non-existent domain, create everything
 | 
				
			||||||
 | 
					        resp.json.side_effect = [
 | 
				
			||||||
 | 
					            [],  # no domains returned during populate
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                'id': 123123,
 | 
				
			||||||
 | 
					                'name': 'unit.tests'
 | 
				
			||||||
 | 
					            }],  # domain created in apply
 | 
				
			||||||
 | 
					            [],  # No pools returned during populate
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                "id": 1808520,
 | 
				
			||||||
 | 
					                "name": "unit.tests.:www.dynamic:A:two",
 | 
				
			||||||
 | 
					            }]   # pool created in apply
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sonar_resp = Mock()
 | 
				
			||||||
 | 
					        sonar_resp.json = Mock()
 | 
				
			||||||
 | 
					        type(sonar_resp).headers = PropertyMock(return_value={
 | 
				
			||||||
 | 
					            "Location": "http://api.sonar.constellix.com/rest/api/tcp/52906"
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        sonar_resp.headers = Mock()
 | 
				
			||||||
 | 
					        provider._sonar._request = Mock(return_value=sonar_resp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sonar_resp.json.side_effect = [
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                "id": 1,
 | 
				
			||||||
 | 
					                "name": "USWAS01",
 | 
				
			||||||
 | 
					                "label": "Site 1",
 | 
				
			||||||
 | 
					                "location": "Washington, DC, U.S.A",
 | 
				
			||||||
 | 
					                "country": "U.S.A",
 | 
				
			||||||
 | 
					                "region": "ASIAPAC"
 | 
				
			||||||
 | 
					            }, {
 | 
				
			||||||
 | 
					                "id": 23,
 | 
				
			||||||
 | 
					                "name": "CATOR01",
 | 
				
			||||||
 | 
					                "label": "Site 1",
 | 
				
			||||||
 | 
					                "location": "Toronto,Canada",
 | 
				
			||||||
 | 
					                "country": "Canada",
 | 
				
			||||||
 | 
					                "region": "EUROPE"
 | 
				
			||||||
 | 
					            }],  # available agents
 | 
				
			||||||
 | 
					            [],  # no checks
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "id": 52906,
 | 
				
			||||||
 | 
					                "name": "check1"
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                "id": 52907,
 | 
				
			||||||
 | 
					                "name": "check2"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        plan = provider.plan(self.expected_healthcheck_world)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # No root NS, no ignored, no excluded, no unsupported
 | 
				
			||||||
 | 
					        n = len(self.expected_healthcheck.records) - 8
 | 
				
			||||||
 | 
					        self.assertEquals(n, len(plan.changes))
 | 
				
			||||||
 | 
					        self.assertEquals(n, provider.apply(plan))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        provider._client._request.assert_has_calls([
 | 
				
			||||||
 | 
					            # get all domains to build the cache
 | 
				
			||||||
 | 
					            call('GET', '/domains'),
 | 
				
			||||||
 | 
					            # created the domain
 | 
				
			||||||
 | 
					            call('POST', '/domains', data={'names': ['unit.tests']})
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Check we tried to get our pool
 | 
				
			||||||
 | 
					        provider._client._request.assert_has_calls([
 | 
				
			||||||
 | 
					            # get all pools to build the cache
 | 
				
			||||||
 | 
					            call('GET', '/pools/AAAA'),
 | 
				
			||||||
 | 
					            # created the pool
 | 
				
			||||||
 | 
					            call('POST', '/pools/AAAA', data={
 | 
				
			||||||
 | 
					                'name': 'unit.tests.:www.dynamic:AAAA:two',
 | 
				
			||||||
 | 
					                'type': 'AAAA',
 | 
				
			||||||
 | 
					                'numReturn': 1,
 | 
				
			||||||
 | 
					                'minAvailableFailover': 1,
 | 
				
			||||||
 | 
					                'ttl': 300,
 | 
				
			||||||
 | 
					                'values': [{
 | 
				
			||||||
 | 
					                    "value": "2601:642:500:e210:62f8:1dff:feb8:947a",
 | 
				
			||||||
 | 
					                    "weight": 1,
 | 
				
			||||||
 | 
					                    "checkId": 52906,
 | 
				
			||||||
 | 
					                    "policy": 'followsonar'
 | 
				
			||||||
 | 
					                }, {
 | 
				
			||||||
 | 
					                    "value": "2601:644:500:e210:62f8:1dff:feb8:947a",
 | 
				
			||||||
 | 
					                    "weight": 1,
 | 
				
			||||||
 | 
					                    "checkId": 52907,
 | 
				
			||||||
 | 
					                    "policy": 'followsonar'
 | 
				
			||||||
 | 
					                }]
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # These two checks are broken up so that ordering doesn't break things.
 | 
				
			||||||
 | 
					        # Python3 doesn't make the calls in a consistent order so different
 | 
				
			||||||
 | 
					        # things follow the GET / on different runs
 | 
				
			||||||
 | 
					        provider._client._request.assert_has_calls([
 | 
				
			||||||
 | 
					            call('POST', '/domains/123123/records/SRV', data={
 | 
				
			||||||
 | 
					                'roundRobin': [{
 | 
				
			||||||
 | 
					                    'priority': 10,
 | 
				
			||||||
 | 
					                    'weight': 20,
 | 
				
			||||||
 | 
					                    'value': 'foo-1.unit.tests.',
 | 
				
			||||||
 | 
					                    'port': 30
 | 
				
			||||||
 | 
					                }, {
 | 
				
			||||||
 | 
					                    'priority': 12,
 | 
				
			||||||
 | 
					                    'weight': 20,
 | 
				
			||||||
 | 
					                    'value': 'foo-2.unit.tests.',
 | 
				
			||||||
 | 
					                    'port': 30
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					                'name': '_srv._tcp',
 | 
				
			||||||
 | 
					                'ttl': 600,
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertEquals(22, provider._client._request.call_count)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        provider._client._request.reset_mock()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        provider._client.records = Mock(return_value=[
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                'id': 11189897,
 | 
				
			||||||
 | 
					                'type': 'A',
 | 
				
			||||||
 | 
					                'name': 'www',
 | 
				
			||||||
 | 
					                'ttl': 300,
 | 
				
			||||||
 | 
					                'recordOption': 'roundRobin',
 | 
				
			||||||
 | 
					                'value': [
 | 
				
			||||||
 | 
					                    '1.2.3.4',
 | 
				
			||||||
 | 
					                    '2.2.3.4',
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            }, {
 | 
				
			||||||
 | 
					                'id': 11189898,
 | 
				
			||||||
 | 
					                'type': 'A',
 | 
				
			||||||
 | 
					                'name': 'ttl',
 | 
				
			||||||
 | 
					                'ttl': 600,
 | 
				
			||||||
 | 
					                'recordOption': 'roundRobin',
 | 
				
			||||||
 | 
					                'value': [
 | 
				
			||||||
 | 
					                    '3.2.3.4'
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            },  {
 | 
				
			||||||
 | 
					                'id': 11189899,
 | 
				
			||||||
 | 
					                'type': 'ALIAS',
 | 
				
			||||||
 | 
					                'name': 'alias',
 | 
				
			||||||
 | 
					                'ttl': 600,
 | 
				
			||||||
 | 
					                'recordOption': 'roundRobin',
 | 
				
			||||||
 | 
					                'value': [{
 | 
				
			||||||
 | 
					                    'value': 'aname.unit.tests.'
 | 
				
			||||||
 | 
					                }]
 | 
				
			||||||
 | 
					            }, {
 | 
				
			||||||
 | 
					                "id": 1808520,
 | 
				
			||||||
 | 
					                "type": "A",
 | 
				
			||||||
 | 
					                "name": "www.dynamic",
 | 
				
			||||||
 | 
					                "geolocation": None,
 | 
				
			||||||
 | 
					                "recordOption": "pools",
 | 
				
			||||||
 | 
					                "ttl": 300,
 | 
				
			||||||
 | 
					                "value": [],
 | 
				
			||||||
 | 
					                "pools": [
 | 
				
			||||||
 | 
					                    1808521
 | 
				
			||||||
 | 
					                ]
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        provider._client.pools = Mock(return_value=[{
 | 
				
			||||||
 | 
					            "id": 1808521,
 | 
				
			||||||
 | 
					            "name": "unit.tests.:www.dynamic:A:two",
 | 
				
			||||||
 | 
					            "type": "A",
 | 
				
			||||||
 | 
					            "values": [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "value": "1.2.3.4",
 | 
				
			||||||
 | 
					                    "weight": 1
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "value": "1.2.3.5",
 | 
				
			||||||
 | 
					                    "weight": 1
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        }])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Domain exists, we don't care about return
 | 
				
			||||||
 | 
					        resp.json.side_effect = [
 | 
				
			||||||
 | 
					            [],  # no domains returned during populate
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                'id': 123123,
 | 
				
			||||||
 | 
					                'name': 'unit.tests'
 | 
				
			||||||
 | 
					            }],  # domain created in apply
 | 
				
			||||||
 | 
					            [],  # No pools returned during populate
 | 
				
			||||||
 | 
					            [{
 | 
				
			||||||
 | 
					                "id": 1808521,
 | 
				
			||||||
 | 
					                "name": "unit.tests.:www.dynamic:A:one"
 | 
				
			||||||
 | 
					            }]  # pool created in apply
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        wanted = Zone('unit.tests.', [])
 | 
				
			||||||
 | 
					        wanted.add_record(Record.new(wanted, 'ttl', {
 | 
				
			||||||
 | 
					            'ttl': 300,
 | 
				
			||||||
 | 
					            'type': 'A',
 | 
				
			||||||
 | 
					            'value': '3.2.3.4'
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        wanted.add_record(Record.new(wanted, 'www.dynamic', {
 | 
				
			||||||
 | 
					            'ttl': 300,
 | 
				
			||||||
 | 
					            'type': 'A',
 | 
				
			||||||
 | 
					            'values': [
 | 
				
			||||||
 | 
					                '1.2.3.4'
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            'dynamic': {
 | 
				
			||||||
 | 
					                'pools': {
 | 
				
			||||||
 | 
					                    'two': {
 | 
				
			||||||
 | 
					                        'values': [{
 | 
				
			||||||
 | 
					                            'value': '1.2.3.4',
 | 
				
			||||||
 | 
					                            'weight': 1
 | 
				
			||||||
 | 
					                        }],
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                'rules': [{
 | 
				
			||||||
 | 
					                    'pool': 'two',
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        }))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        plan = provider.plan(wanted)
 | 
				
			||||||
 | 
					        self.assertEquals(4, len(plan.changes))
 | 
				
			||||||
 | 
					        self.assertEquals(4, provider.apply(plan))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # recreate for update, and deletes for the 2 parts of the other
 | 
				
			||||||
 | 
					        provider._client._request.assert_has_calls([
 | 
				
			||||||
 | 
					            call('POST', '/domains/123123/records/A', data={
 | 
				
			||||||
 | 
					                'roundRobin': [{
 | 
				
			||||||
 | 
					                    'value': '3.2.3.4'
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					                'name': 'ttl',
 | 
				
			||||||
 | 
					                'ttl': 300
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            call('PUT', '/pools/A/1808521', data={
 | 
				
			||||||
 | 
					                'name': 'unit.tests.:www.dynamic:A:two',
 | 
				
			||||||
 | 
					                'type': 'A',
 | 
				
			||||||
 | 
					                'numReturn': 1,
 | 
				
			||||||
 | 
					                'minAvailableFailover': 1,
 | 
				
			||||||
 | 
					                'ttl': 300,
 | 
				
			||||||
 | 
					                'values': [{
 | 
				
			||||||
 | 
					                    "value": "1.2.3.4",
 | 
				
			||||||
 | 
					                    "weight": 1
 | 
				
			||||||
 | 
					                }],
 | 
				
			||||||
 | 
					                'id': 1808521,
 | 
				
			||||||
 | 
					                'geofilter': 1
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            call('DELETE', '/domains/123123/records/A/11189897'),
 | 
				
			||||||
 | 
					            call('DELETE', '/domains/123123/records/A/11189898'),
 | 
				
			||||||
 | 
					            call('DELETE', '/domains/123123/records/ANAME/11189899'),
 | 
				
			||||||
 | 
					        ], any_order=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_apply_dynamic(self):
 | 
				
			||||||
        provider = ConstellixProvider('test', 'api', 'secret')
 | 
					        provider = ConstellixProvider('test', 'api', 'secret')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        resp = Mock()
 | 
					        resp = Mock()
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user