1
0
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:
apatserkovskyi
2021-10-25 21:11:01 +03:00
parent 29186da021
commit f9f37d61bf
3 changed files with 928 additions and 3 deletions

View File

@@ -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
```

View File

@@ -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,

View File

@@ -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()