mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge branch 'gcore-provider' of github.com:G-Core/octodns into gcore-provider
This commit is contained in:
+18
-1
@@ -1,4 +1,11 @@
|
||||
## v0.9.13 - 2021-..-.. -
|
||||
## v0.9.14 - 2021-??-?? - ...
|
||||
|
||||
#### Noteworthy changes
|
||||
|
||||
* NS1 NA target now includes `SX` and `UM`. If `NA` continent is in use in
|
||||
dynamic records care must be taken to upgrade/downgrade to v0.9.13.
|
||||
|
||||
## v0.9.13 - 2021-07-18 - Processors Alpha
|
||||
|
||||
#### Noteworthy changes
|
||||
|
||||
@@ -14,6 +21,16 @@
|
||||
America list for backwards compatibility reasons. They will be added in the
|
||||
next releaser.
|
||||
|
||||
#### Stuff
|
||||
|
||||
* Lots of progress on the partial/beta support for dynamic records in Azure,
|
||||
still not production ready.
|
||||
* NS1 fix for when a pool only exists as a fallback
|
||||
* Zone level lenient flag
|
||||
* Validate weight makes sense for pools with a single record
|
||||
* UltraDNS support for aliases and general fixes/improvements
|
||||
* Misc doc fixes and improvements
|
||||
|
||||
## v0.9.12 - 2021-04-30 - Enough time has passed
|
||||
|
||||
#### Noteworthy changes
|
||||
|
||||
@@ -53,7 +53,7 @@ $ mkdir config
|
||||
If you'd like to install a version that has not yet been released in a repetable/safe manner you can do the following. In general octoDNS is fairly stable inbetween releases thanks to the plan and apply process, but care should be taken regardless.
|
||||
|
||||
```shell
|
||||
$ pip install -e git+https://git@github.com/github/octodns.git@<SHA>#egg=octodns
|
||||
$ pip install -e git+https://git@github.com/octodns/octodns.git@<SHA>#egg=octodns
|
||||
```
|
||||
|
||||
### Config
|
||||
@@ -89,7 +89,7 @@ zones:
|
||||
- dyn
|
||||
- route53
|
||||
|
||||
example.net:
|
||||
example.net.:
|
||||
alias: example.com.
|
||||
```
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ OctoDNS supports the following record types:
|
||||
* `SRV`
|
||||
* `SSHFP`
|
||||
* `TXT`
|
||||
* `URLFWD`
|
||||
|
||||
Underlying provider support for each of these varies and some providers have extra requirements or limitations. In cases where a record type is not supported by a provider OctoDNS will ignore it there and continue to manage the record elsewhere. For example `SSHFP` is supported by Dyn, but not Route53. If your source data includes an SSHFP record OctoDNS will keep it in sync on Dyn, but not consider it when evaluating the state of Route53. The best way to find out what types are supported by a provider is to look for its `supports` method. If that method exists the logic will drive which records are supported and which are ignored. If the provider does not implement the method it will fall back to `BaseProvider.supports` which indicates full support.
|
||||
|
||||
|
||||
+1
-1
@@ -3,4 +3,4 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
__VERSION__ = '0.9.12'
|
||||
__VERSION__ = '0.9.13'
|
||||
|
||||
+167
-39
@@ -10,6 +10,7 @@ from copy import deepcopy
|
||||
from logging import getLogger
|
||||
from requests import Session
|
||||
from time import sleep
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from ..record import Record, Update
|
||||
from .base import BaseProvider
|
||||
@@ -76,7 +77,7 @@ class CloudflareProvider(BaseProvider):
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'LOC', 'MX', 'NS',
|
||||
'PTR', 'SRV', 'SPF', 'TXT'))
|
||||
'PTR', 'SRV', 'SPF', 'TXT', 'URLFWD'))
|
||||
|
||||
MIN_TTL = 120
|
||||
TIMEOUT = 15
|
||||
@@ -170,6 +171,9 @@ class CloudflareProvider(BaseProvider):
|
||||
|
||||
return self._zones
|
||||
|
||||
def _ttl_data(self, ttl):
|
||||
return 300 if ttl == 1 else ttl
|
||||
|
||||
def _data_for_cdn(self, name, _type, records):
|
||||
self.log.info('CDN rewrite for %s', records[0]['name'])
|
||||
_type = "CNAME"
|
||||
@@ -177,14 +181,14 @@ class CloudflareProvider(BaseProvider):
|
||||
_type = "ALIAS"
|
||||
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'ttl': self._ttl_data(records[0]['ttl']),
|
||||
'type': _type,
|
||||
'value': '{}.cdn.cloudflare.net.'.format(records[0]['name']),
|
||||
}
|
||||
|
||||
def _data_for_multiple(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'ttl': self._ttl_data(records[0]['ttl']),
|
||||
'type': _type,
|
||||
'values': [r['content'] for r in records],
|
||||
}
|
||||
@@ -195,7 +199,7 @@ class CloudflareProvider(BaseProvider):
|
||||
|
||||
def _data_for_TXT(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'ttl': self._ttl_data(records[0]['ttl']),
|
||||
'type': _type,
|
||||
'values': [r['content'].replace(';', '\\;') for r in records],
|
||||
}
|
||||
@@ -206,7 +210,7 @@ class CloudflareProvider(BaseProvider):
|
||||
data = r['data']
|
||||
values.append(data)
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'ttl': self._ttl_data(records[0]['ttl']),
|
||||
'type': _type,
|
||||
'values': values,
|
||||
}
|
||||
@@ -214,7 +218,7 @@ class CloudflareProvider(BaseProvider):
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
only = records[0]
|
||||
return {
|
||||
'ttl': only['ttl'],
|
||||
'ttl': self._ttl_data(only['ttl']),
|
||||
'type': _type,
|
||||
'value': '{}.'.format(only['content'])
|
||||
}
|
||||
@@ -241,7 +245,7 @@ class CloudflareProvider(BaseProvider):
|
||||
'precision_vert': float(r['precision_vert']),
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'ttl': self._ttl_data(records[0]['ttl']),
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
@@ -254,14 +258,14 @@ class CloudflareProvider(BaseProvider):
|
||||
'exchange': '{}.'.format(r['content']),
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'ttl': self._ttl_data(records[0]['ttl']),
|
||||
'type': _type,
|
||||
'values': values,
|
||||
}
|
||||
|
||||
def _data_for_NS(self, _type, records):
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'ttl': self._ttl_data(records[0]['ttl']),
|
||||
'type': _type,
|
||||
'values': ['{}.'.format(r['content']) for r in records],
|
||||
}
|
||||
@@ -279,7 +283,23 @@ class CloudflareProvider(BaseProvider):
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records[0]['ttl'],
|
||||
'ttl': self._ttl_data(records[0]['ttl']),
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_URLFWD(self, _type, records):
|
||||
values = []
|
||||
for r in records:
|
||||
values.append({
|
||||
'path': r['path'],
|
||||
'target': r['url'],
|
||||
'code': r['status_code'],
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': 300, # ttl does not exist for this type, forcing a setting
|
||||
'values': values
|
||||
}
|
||||
|
||||
@@ -302,6 +322,13 @@ class CloudflareProvider(BaseProvider):
|
||||
else:
|
||||
page = None
|
||||
|
||||
path = '/zones/{}/pagerules'.format(zone_id)
|
||||
resp = self._try_request('GET', path, params={'status': 'active'})
|
||||
for r in resp['result']:
|
||||
# assumption, base on API guide, will only contain 1 action
|
||||
if r['actions'][0]['id'] == 'forwarding_url':
|
||||
records += [r]
|
||||
|
||||
self._zone_records[zone.name] = records
|
||||
|
||||
return self._zone_records[zone.name]
|
||||
@@ -338,10 +365,29 @@ class CloudflareProvider(BaseProvider):
|
||||
exists = True
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in records:
|
||||
name = zone.hostname_from_fqdn(record['name'])
|
||||
_type = record['type']
|
||||
if _type in self.SUPPORTS:
|
||||
values[name][record['type']].append(record)
|
||||
if 'targets' in record:
|
||||
# assumption, targets will always contain 1 target
|
||||
# API documentation only indicates 'url' as the only target
|
||||
# if record['targets'][0]['target'] == 'url':
|
||||
uri = record['targets'][0]['constraint']['value']
|
||||
uri = '//' + uri if not uri.startswith('http') else uri
|
||||
parsed_uri = urlsplit(uri)
|
||||
name = zone.hostname_from_fqdn(parsed_uri.netloc)
|
||||
path = parsed_uri.path
|
||||
_type = 'URLFWD'
|
||||
# assumption, actions will always contain 1 action
|
||||
_values = record['actions'][0]['value']
|
||||
_values['path'] = path
|
||||
# no ttl set by pagerule, creating one
|
||||
_values['ttl'] = 300
|
||||
values[name][_type].append(_values)
|
||||
# the dns_records branch
|
||||
# elif 'name' in record:
|
||||
else:
|
||||
name = zone.hostname_from_fqdn(record['name'])
|
||||
_type = record['type']
|
||||
if _type in self.SUPPORTS:
|
||||
values[name][record['type']].append(record)
|
||||
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
@@ -373,6 +419,11 @@ class CloudflareProvider(BaseProvider):
|
||||
existing.update({
|
||||
'ttl': new['ttl']
|
||||
})
|
||||
elif change.new._type == 'URLFWD':
|
||||
existing = deepcopy(change.existing.data)
|
||||
existing.update({
|
||||
'ttl': new['ttl']
|
||||
})
|
||||
else:
|
||||
existing = change.existing.data
|
||||
|
||||
@@ -470,6 +521,31 @@ class CloudflareProvider(BaseProvider):
|
||||
}
|
||||
}
|
||||
|
||||
def _contents_for_URLFWD(self, record):
|
||||
name = record.fqdn[:-1]
|
||||
for value in record.values:
|
||||
yield {
|
||||
'targets': [
|
||||
{
|
||||
'target': 'url',
|
||||
'constraint': {
|
||||
'operator': 'matches',
|
||||
'value': name + value.path
|
||||
}
|
||||
}
|
||||
],
|
||||
'actions': [
|
||||
{
|
||||
'id': 'forwarding_url',
|
||||
'value': {
|
||||
'url': value.target,
|
||||
'status_code': value.code,
|
||||
}
|
||||
}
|
||||
],
|
||||
'status': 'active',
|
||||
}
|
||||
|
||||
def _record_is_proxied(self, record):
|
||||
return (
|
||||
not self.cdn and
|
||||
@@ -485,20 +561,25 @@ class CloudflareProvider(BaseProvider):
|
||||
if _type == 'ALIAS':
|
||||
_type = 'CNAME'
|
||||
|
||||
contents_for = getattr(self, '_contents_for_{}'.format(_type))
|
||||
for content in contents_for(record):
|
||||
content.update({
|
||||
'name': name,
|
||||
'type': _type,
|
||||
'ttl': ttl,
|
||||
})
|
||||
|
||||
if _type in _PROXIABLE_RECORD_TYPES:
|
||||
if _type == 'URLFWD':
|
||||
contents_for = getattr(self, '_contents_for_{}'.format(_type))
|
||||
for content in contents_for(record):
|
||||
yield content
|
||||
else:
|
||||
contents_for = getattr(self, '_contents_for_{}'.format(_type))
|
||||
for content in contents_for(record):
|
||||
content.update({
|
||||
'proxied': self._record_is_proxied(record)
|
||||
'name': name,
|
||||
'type': _type,
|
||||
'ttl': ttl,
|
||||
})
|
||||
|
||||
yield content
|
||||
if _type in _PROXIABLE_RECORD_TYPES:
|
||||
content.update({
|
||||
'proxied': self._record_is_proxied(record)
|
||||
})
|
||||
|
||||
yield content
|
||||
|
||||
def _gen_key(self, data):
|
||||
# Note that most CF record data has a `content` field the value of
|
||||
@@ -512,7 +593,8 @@ class CloudflareProvider(BaseProvider):
|
||||
# BUT... there are exceptions. MX, CAA, LOC and SRV don't have a simple
|
||||
# content as things are currently implemented so we need to handle
|
||||
# those explicitly and create unique/hashable strings for them.
|
||||
_type = data['type']
|
||||
# AND... for URLFWD/Redirects additional adventures are created.
|
||||
_type = data.get('type', 'URLFWD')
|
||||
if _type == 'MX':
|
||||
return '{priority} {content}'.format(**data)
|
||||
elif _type == 'CAA':
|
||||
@@ -537,12 +619,23 @@ class CloudflareProvider(BaseProvider):
|
||||
'{precision_horz}',
|
||||
'{precision_vert}')
|
||||
return ' '.join(loc).format(**data)
|
||||
elif _type == 'URLFWD':
|
||||
uri = data['targets'][0]['constraint']['value']
|
||||
uri = '//' + uri if not uri.startswith('http') else uri
|
||||
parsed_uri = urlsplit(uri)
|
||||
return '{name} {path} {url} {status_code}' \
|
||||
.format(name=parsed_uri.netloc,
|
||||
path=parsed_uri.path,
|
||||
**data['actions'][0]['value'])
|
||||
return data['content']
|
||||
|
||||
def _apply_Create(self, change):
|
||||
new = change.new
|
||||
zone_id = self.zones[new.zone.name]
|
||||
path = '/zones/{}/dns_records'.format(zone_id)
|
||||
if new._type == 'URLFWD':
|
||||
path = '/zones/{}/pagerules'.format(zone_id)
|
||||
else:
|
||||
path = '/zones/{}/dns_records'.format(zone_id)
|
||||
for content in self._gen_data(new):
|
||||
self._try_request('POST', path, data=content)
|
||||
|
||||
@@ -555,14 +648,27 @@ class CloudflareProvider(BaseProvider):
|
||||
existing = {}
|
||||
# Find all of the existing CF records for this name & type
|
||||
for record in self.zone_records(zone):
|
||||
name = zone.hostname_from_fqdn(record['name'])
|
||||
if 'targets' in record:
|
||||
uri = record['targets'][0]['constraint']['value']
|
||||
uri = '//' + uri if not uri.startswith('http') else uri
|
||||
parsed_uri = urlsplit(uri)
|
||||
name = zone.hostname_from_fqdn(parsed_uri.netloc)
|
||||
path = parsed_uri.path
|
||||
# assumption, actions will always contain 1 action
|
||||
_values = record['actions'][0]['value']
|
||||
_values['path'] = path
|
||||
_values['ttl'] = 300
|
||||
_values['type'] = 'URLFWD'
|
||||
record.update(_values)
|
||||
else:
|
||||
name = zone.hostname_from_fqdn(record['name'])
|
||||
# Use the _record_for so that we include all of standard
|
||||
# conversion logic
|
||||
r = self._record_for(zone, name, record['type'], [record], True)
|
||||
if hostname == r.name and _type == r._type:
|
||||
# Round trip the single value through a record to contents flow
|
||||
# to get a consistent _gen_data result that matches what
|
||||
# went in to new_contents
|
||||
# Round trip the single value through a record to contents
|
||||
# flow to get a consistent _gen_data result that matches
|
||||
# what went in to new_contents
|
||||
data = next(self._gen_data(r))
|
||||
|
||||
# Record the record_id and data for this existing record
|
||||
@@ -630,7 +736,10 @@ class CloudflareProvider(BaseProvider):
|
||||
# otherwise required, just makes things deterministic
|
||||
|
||||
# Creates
|
||||
path = '/zones/{}/dns_records'.format(zone_id)
|
||||
if _type == 'URLFWD':
|
||||
path = '/zones/{}/pagerules'.format(zone_id)
|
||||
else:
|
||||
path = '/zones/{}/dns_records'.format(zone_id)
|
||||
for _, data in sorted(creates.items()):
|
||||
self.log.debug('_apply_Update: creating %s', data)
|
||||
self._try_request('POST', path, data=data)
|
||||
@@ -640,7 +749,10 @@ class CloudflareProvider(BaseProvider):
|
||||
record_id = info['record_id']
|
||||
data = info['data']
|
||||
old_data = info['old_data']
|
||||
path = '/zones/{}/dns_records/{}'.format(zone_id, record_id)
|
||||
if _type == 'URLFWD':
|
||||
path = '/zones/{}/pagerules/{}'.format(zone_id, record_id)
|
||||
else:
|
||||
path = '/zones/{}/dns_records/{}'.format(zone_id, record_id)
|
||||
self.log.debug('_apply_Update: updating %s, %s -> %s',
|
||||
record_id, data, old_data)
|
||||
self._try_request('PUT', path, data=data)
|
||||
@@ -649,7 +761,10 @@ class CloudflareProvider(BaseProvider):
|
||||
for _, info in sorted(deletes.items()):
|
||||
record_id = info['record_id']
|
||||
old_data = info['data']
|
||||
path = '/zones/{}/dns_records/{}'.format(zone_id, record_id)
|
||||
if _type == 'URLFWD':
|
||||
path = '/zones/{}/pagerules/{}'.format(zone_id, record_id)
|
||||
else:
|
||||
path = '/zones/{}/dns_records/{}'.format(zone_id, record_id)
|
||||
self.log.debug('_apply_Update: removing %s, %s', record_id,
|
||||
old_data)
|
||||
self._try_request('DELETE', path)
|
||||
@@ -661,11 +776,24 @@ class CloudflareProvider(BaseProvider):
|
||||
existing_type = 'CNAME' if existing._type == 'ALIAS' \
|
||||
else existing._type
|
||||
for record in self.zone_records(existing.zone):
|
||||
if existing_name == record['name'] and \
|
||||
existing_type == record['type']:
|
||||
path = '/zones/{}/dns_records/{}'.format(record['zone_id'],
|
||||
record['id'])
|
||||
self._try_request('DELETE', path)
|
||||
if 'targets' in record:
|
||||
uri = record['targets'][0]['constraint']['value']
|
||||
uri = '//' + uri if not uri.startswith('http') else uri
|
||||
parsed_uri = urlsplit(uri)
|
||||
record_name = parsed_uri.netloc
|
||||
record_type = 'URLFWD'
|
||||
zone_id = self.zones.get(existing.zone.name, False)
|
||||
if existing_name == record_name and \
|
||||
existing_type == record_type:
|
||||
path = '/zones/{}/pagerules/{}' \
|
||||
.format(zone_id, record['id'])
|
||||
self._try_request('DELETE', path)
|
||||
else:
|
||||
if existing_name == record['name'] and \
|
||||
existing_type == record['type']:
|
||||
path = '/zones/{}/dns_records/{}' \
|
||||
.format(record['zone_id'], record['id'])
|
||||
self._try_request('DELETE', path)
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
|
||||
+25
-2
@@ -234,7 +234,7 @@ class Ns1Provider(BaseProvider):
|
||||
SUPPORTS_GEO = True
|
||||
SUPPORTS_DYNAMIC = True
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR',
|
||||
'NS', 'PTR', 'SPF', 'SRV', 'TXT'))
|
||||
'NS', 'PTR', 'SPF', 'SRV', 'TXT', 'URLFWD'))
|
||||
|
||||
ZONE_NOT_FOUND_MESSAGE = 'server error: zone not found'
|
||||
|
||||
@@ -363,7 +363,8 @@ class Ns1Provider(BaseProvider):
|
||||
'NA': {'DO', 'DM', 'BB', 'BL', 'BM', 'HT', 'KN', 'JM', 'VC', 'HN',
|
||||
'BS', 'BZ', 'PR', 'NI', 'LC', 'TT', 'VG', 'PA', 'TC', 'PM',
|
||||
'GT', 'AG', 'GP', 'AI', 'VI', 'CA', 'GD', 'AW', 'CR', 'GL',
|
||||
'CU', 'MF', 'SV', 'US', 'MQ', 'MS', 'KY', 'MX', 'CW', 'BQ'}
|
||||
'CU', 'MF', 'SV', 'US', 'MQ', 'MS', 'KY', 'MX', 'CW', 'BQ',
|
||||
'SX', 'UM'}
|
||||
}
|
||||
|
||||
def __init__(self, id, api_key, retry_count=4, monitor_regions=None,
|
||||
@@ -748,6 +749,23 @@ class Ns1Provider(BaseProvider):
|
||||
'values': values,
|
||||
}
|
||||
|
||||
def _data_for_URLFWD(self, _type, record):
|
||||
values = []
|
||||
for answer in record['short_answers']:
|
||||
path, target, code, masking, query = answer.split(' ', 4)
|
||||
values.append({
|
||||
'path': path,
|
||||
'target': target,
|
||||
'code': code,
|
||||
'masking': masking,
|
||||
'query': query,
|
||||
})
|
||||
return {
|
||||
'ttl': record['ttl'],
|
||||
'type': _type,
|
||||
'values': values,
|
||||
}
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s',
|
||||
zone.name,
|
||||
@@ -1243,6 +1261,11 @@ class Ns1Provider(BaseProvider):
|
||||
for v in record.values]
|
||||
return {'answers': values, 'ttl': record.ttl}, None
|
||||
|
||||
def _params_for_URLFWD(self, record):
|
||||
values = [(v.path, v.target, v.code, v.masking, v.query)
|
||||
for v in record.values]
|
||||
return {'answers': values, 'ttl': record.ttl}, None
|
||||
|
||||
def _get_ns1_filters(self, ns1_zone_name):
|
||||
ns1_filters = {}
|
||||
ns1_zone = {}
|
||||
|
||||
@@ -105,7 +105,8 @@ class YamlProvider(BaseProvider):
|
||||
SUPPORTS_GEO = True
|
||||
SUPPORTS_DYNAMIC = True
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'DNAME', 'LOC', 'MX',
|
||||
'NAPTR', 'NS', 'PTR', 'SSHFP', 'SPF', 'SRV', 'TXT'))
|
||||
'NAPTR', 'NS', 'PTR', 'SSHFP', 'SPF', 'SRV', 'TXT',
|
||||
'URLFWD'))
|
||||
|
||||
def __init__(self, id, directory, default_ttl=3600, enforce_order=True,
|
||||
populate_should_replace=False, *args, **kwargs):
|
||||
|
||||
@@ -106,6 +106,7 @@ class Record(EqualityTupleMixin):
|
||||
'SRV': SrvRecord,
|
||||
'SSHFP': SshfpRecord,
|
||||
'TXT': TxtRecord,
|
||||
'URLFWD': UrlfwdRecord,
|
||||
}[_type]
|
||||
except KeyError:
|
||||
raise Exception('Unknown record type: "{}"'.format(_type))
|
||||
@@ -617,7 +618,6 @@ class _DynamicMixin(object):
|
||||
else:
|
||||
seen_default = False
|
||||
|
||||
# TODO: don't allow 'default' as a pool name, reserved
|
||||
for i, rule in enumerate(rules):
|
||||
rule_num = i + 1
|
||||
try:
|
||||
@@ -1467,3 +1467,86 @@ class _TxtValue(_ChunkedValue):
|
||||
class TxtRecord(_ChunkedValuesMixin, Record):
|
||||
_type = 'TXT'
|
||||
_value_type = _TxtValue
|
||||
|
||||
|
||||
class UrlfwdValue(EqualityTupleMixin):
|
||||
VALID_CODES = (301, 302)
|
||||
VALID_MASKS = (0, 1, 2)
|
||||
VALID_QUERY = (0, 1)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data, _type):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
for value in data:
|
||||
try:
|
||||
code = int(value['code'])
|
||||
if code not in cls.VALID_CODES:
|
||||
reasons.append('unrecognized return code "{}"'
|
||||
.format(code))
|
||||
except KeyError:
|
||||
reasons.append('missing code')
|
||||
except ValueError:
|
||||
reasons.append('invalid return code "{}"'
|
||||
.format(value['code']))
|
||||
try:
|
||||
masking = int(value['masking'])
|
||||
if masking not in cls.VALID_MASKS:
|
||||
reasons.append('unrecognized masking setting "{}"'
|
||||
.format(masking))
|
||||
except KeyError:
|
||||
reasons.append('missing masking')
|
||||
except ValueError:
|
||||
reasons.append('invalid masking setting "{}"'
|
||||
.format(value['masking']))
|
||||
try:
|
||||
query = int(value['query'])
|
||||
if query not in cls.VALID_QUERY:
|
||||
reasons.append('unrecognized query setting "{}"'
|
||||
.format(query))
|
||||
except KeyError:
|
||||
reasons.append('missing query')
|
||||
except ValueError:
|
||||
reasons.append('invalid query setting "{}"'
|
||||
.format(value['query']))
|
||||
for k in ('path', 'target'):
|
||||
if k not in value:
|
||||
reasons.append('missing {}'.format(k))
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return [UrlfwdValue(v) for v in values]
|
||||
|
||||
def __init__(self, value):
|
||||
self.path = value['path']
|
||||
self.target = value['target']
|
||||
self.code = int(value['code'])
|
||||
self.masking = int(value['masking'])
|
||||
self.query = int(value['query'])
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return {
|
||||
'path': self.path,
|
||||
'target': self.target,
|
||||
'code': self.code,
|
||||
'masking': self.masking,
|
||||
'query': self.query,
|
||||
}
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
def _equality_tuple(self):
|
||||
return (self.path, self.target, self.code, self.masking, self.query)
|
||||
|
||||
def __repr__(self):
|
||||
return '"{}" "{}" {} {} {}'.format(self.path, self.target, self.code,
|
||||
self.masking, self.query)
|
||||
|
||||
|
||||
class UrlfwdRecord(_ValuesMixin, Record):
|
||||
_type = 'URLFWD'
|
||||
_value_type = UrlfwdValue
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
urlfwd:
|
||||
ttl: 300
|
||||
type: URLFWD
|
||||
values:
|
||||
- code: 302
|
||||
masking: 2
|
||||
path: '/'
|
||||
query: 0
|
||||
target: 'http://www.unit.tests'
|
||||
- code: 301
|
||||
masking: 2
|
||||
path: '/target'
|
||||
query: 0
|
||||
target: 'http://target.unit.tests'
|
||||
@@ -169,6 +169,20 @@ txt:
|
||||
- Bah bah black sheep
|
||||
- have you any wool.
|
||||
- 'v=DKIM1\;k=rsa\;s=email\;h=sha256\;p=A/kinda+of/long/string+with+numb3rs'
|
||||
urlfwd:
|
||||
ttl: 300
|
||||
type: URLFWD
|
||||
values:
|
||||
- code: 302
|
||||
masking: 2
|
||||
path: '/'
|
||||
query: 0
|
||||
target: 'http://www.unit.tests'
|
||||
- code: 301
|
||||
masking: 2
|
||||
path: '/target'
|
||||
query: 0
|
||||
target: 'http://target.unit.tests'
|
||||
www:
|
||||
ttl: 300
|
||||
type: A
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"id": "2b1ec1793185213139f22059a165376e",
|
||||
"targets": [
|
||||
{
|
||||
"target": "url",
|
||||
"constraint": {
|
||||
"operator": "matches",
|
||||
"value": "urlfwd0.unit.tests/"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "always_use_https"
|
||||
}
|
||||
],
|
||||
"priority": 4,
|
||||
"status": "active",
|
||||
"created_on": "2021-06-29T17:14:28.000000Z",
|
||||
"modified_on": "2021-06-29T17:15:33.000000Z"
|
||||
},
|
||||
{
|
||||
"id": "2b1ec1793185213139f22059a165376f",
|
||||
"targets": [
|
||||
{
|
||||
"target": "url",
|
||||
"constraint": {
|
||||
"operator": "matches",
|
||||
"value": "urlfwd0.unit.tests/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "forwarding_url",
|
||||
"value": {
|
||||
"url": "https://www.unit.tests/",
|
||||
"status_code": 301
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 3,
|
||||
"status": "active",
|
||||
"created_on": "2021-06-29T17:07:12.000000Z",
|
||||
"modified_on": "2021-06-29T17:15:12.000000Z"
|
||||
},
|
||||
{
|
||||
"id": "2b1ec1793185213139f22059a165377e",
|
||||
"targets": [
|
||||
{
|
||||
"target": "url",
|
||||
"constraint": {
|
||||
"operator": "matches",
|
||||
"value": "urlfwd1.unit.tests/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "forwarding_url",
|
||||
"value": {
|
||||
"url": "https://www.unit.tests/",
|
||||
"status_code": 302
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 2,
|
||||
"status": "active",
|
||||
"created_on": "2021-06-28T22:42:27.000000Z",
|
||||
"modified_on": "2021-06-28T22:43:13.000000Z"
|
||||
},
|
||||
{
|
||||
"id": "2a9140b17ffb0e6aed826049eec970b8",
|
||||
"targets": [
|
||||
{
|
||||
"target": "url",
|
||||
"constraint": {
|
||||
"operator": "matches",
|
||||
"value": "urlfwd2.unit.tests/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "forwarding_url",
|
||||
"value": {
|
||||
"url": "https://www.unit.tests/",
|
||||
"status_code": 301
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 1,
|
||||
"status": "active",
|
||||
"created_on": "2021-06-25T20:10:50.000000Z",
|
||||
"modified_on": "2021-06-28T22:38:10.000000Z"
|
||||
}
|
||||
],
|
||||
"success": true,
|
||||
"errors": [],
|
||||
"messages": []
|
||||
}
|
||||
@@ -121,12 +121,12 @@ class TestManager(TestCase):
|
||||
environ['YAML_TMP_DIR'] = tmpdir.dirname
|
||||
tc = Manager(get_config_filename('simple.yaml')) \
|
||||
.sync(dry_run=False)
|
||||
self.assertEquals(25, tc)
|
||||
self.assertEquals(26, tc)
|
||||
|
||||
# try with just one of the zones
|
||||
tc = Manager(get_config_filename('simple.yaml')) \
|
||||
.sync(dry_run=False, eligible_zones=['unit.tests.'])
|
||||
self.assertEquals(19, tc)
|
||||
self.assertEquals(20, tc)
|
||||
|
||||
# the subzone, with 2 targets
|
||||
tc = Manager(get_config_filename('simple.yaml')) \
|
||||
@@ -141,18 +141,18 @@ class TestManager(TestCase):
|
||||
# Again with force
|
||||
tc = Manager(get_config_filename('simple.yaml')) \
|
||||
.sync(dry_run=False, force=True)
|
||||
self.assertEquals(25, tc)
|
||||
self.assertEquals(26, tc)
|
||||
|
||||
# Again with max_workers = 1
|
||||
tc = Manager(get_config_filename('simple.yaml'), max_workers=1) \
|
||||
.sync(dry_run=False, force=True)
|
||||
self.assertEquals(25, tc)
|
||||
self.assertEquals(26, tc)
|
||||
|
||||
# Include meta
|
||||
tc = Manager(get_config_filename('simple.yaml'), max_workers=1,
|
||||
include_meta=True) \
|
||||
.sync(dry_run=False, force=True)
|
||||
self.assertEquals(29, tc)
|
||||
self.assertEquals(30, tc)
|
||||
|
||||
def test_eligible_sources(self):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
@@ -218,13 +218,13 @@ class TestManager(TestCase):
|
||||
fh.write('---\n{}')
|
||||
|
||||
changes = manager.compare(['in'], ['dump'], 'unit.tests.')
|
||||
self.assertEquals(19, len(changes))
|
||||
self.assertEquals(20, len(changes))
|
||||
|
||||
# Compound sources with varying support
|
||||
changes = manager.compare(['in', 'nosshfp'],
|
||||
['dump'],
|
||||
'unit.tests.')
|
||||
self.assertEquals(18, len(changes))
|
||||
self.assertEquals(19, len(changes))
|
||||
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
manager.compare(['nope'], ['dump'], 'unit.tests.')
|
||||
|
||||
@@ -166,9 +166,15 @@ class TestCloudflareProvider(TestCase):
|
||||
json={'result': [], 'result_info': {'count': 0,
|
||||
'per_page': 0}})
|
||||
|
||||
base = '{}/234234243423aaabb334342aaa343435'.format(base)
|
||||
|
||||
# pagerules/URLFWD
|
||||
with open('tests/fixtures/cloudflare-pagerules.json') as fh:
|
||||
mock.get('{}/pagerules?status=active'.format(base),
|
||||
status_code=200, text=fh.read())
|
||||
|
||||
# records
|
||||
base = '{}/234234243423aaabb334342aaa343435/dns_records' \
|
||||
.format(base)
|
||||
base = '{}/dns_records'.format(base)
|
||||
with open('tests/fixtures/cloudflare-dns_records-'
|
||||
'page-1.json') as fh:
|
||||
mock.get('{}?page=1'.format(base), status_code=200,
|
||||
@@ -184,16 +190,16 @@ class TestCloudflareProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(16, len(zone.records))
|
||||
self.assertEquals(19, len(zone.records))
|
||||
|
||||
changes = self.expected.changes(zone, provider)
|
||||
|
||||
self.assertEquals(0, len(changes))
|
||||
self.assertEquals(4, len(changes))
|
||||
|
||||
# re-populating the same zone/records comes out of cache, no calls
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(16, len(again.records))
|
||||
self.assertEquals(19, len(again.records))
|
||||
|
||||
def test_apply(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token', retry_period=0)
|
||||
@@ -207,12 +213,12 @@ class TestCloudflareProvider(TestCase):
|
||||
'id': 42,
|
||||
}
|
||||
}, # zone create
|
||||
] + [None] * 25 # individual record creates
|
||||
] + [None] * 27 # individual record creates
|
||||
|
||||
# non-existent zone, create everything
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(16, len(plan.changes))
|
||||
self.assertEquals(16, provider.apply(plan))
|
||||
self.assertEquals(17, len(plan.changes))
|
||||
self.assertEquals(17, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
@@ -236,9 +242,31 @@ class TestCloudflareProvider(TestCase):
|
||||
'name': 'txt.unit.tests',
|
||||
'ttl': 600
|
||||
}),
|
||||
# create at least one pagerules
|
||||
call('POST', '/zones/42/pagerules', data={
|
||||
'targets': [
|
||||
{
|
||||
'target': 'url',
|
||||
'constraint': {
|
||||
'operator': 'matches',
|
||||
'value': 'urlfwd.unit.tests/'
|
||||
}
|
||||
}
|
||||
],
|
||||
'actions': [
|
||||
{
|
||||
'id': 'forwarding_url',
|
||||
'value': {
|
||||
'url': 'http://www.unit.tests',
|
||||
'status_code': 302
|
||||
}
|
||||
}
|
||||
],
|
||||
'status': 'active'
|
||||
}),
|
||||
], True)
|
||||
# expected number of total calls
|
||||
self.assertEquals(27, provider._request.call_count)
|
||||
self.assertEquals(29, provider._request.call_count)
|
||||
|
||||
provider._request.reset_mock()
|
||||
|
||||
@@ -311,6 +339,56 @@ class TestCloudflareProvider(TestCase):
|
||||
"auto_added": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2a9140b17ffb0e6aed826049eec970b7",
|
||||
"targets": [
|
||||
{
|
||||
"target": "url",
|
||||
"constraint": {
|
||||
"operator": "matches",
|
||||
"value": "urlfwd.unit.tests/"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "forwarding_url",
|
||||
"value": {
|
||||
"url": "https://www.unit.tests",
|
||||
"status_code": 302
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 1,
|
||||
"status": "active",
|
||||
"created_on": "2021-06-25T20:10:50.000000Z",
|
||||
"modified_on": "2021-06-28T22:38:10.000000Z"
|
||||
},
|
||||
{
|
||||
"id": "2a9141b18ffb0e6aed826050eec970b8",
|
||||
"targets": [
|
||||
{
|
||||
"target": "url",
|
||||
"constraint": {
|
||||
"operator": "matches",
|
||||
"value": "urlfwdother.unit.tests/target"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "forwarding_url",
|
||||
"value": {
|
||||
"url": "https://target.unit.tests",
|
||||
"status_code": 301
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 2,
|
||||
"status": "active",
|
||||
"created_on": "2021-06-25T20:10:50.000000Z",
|
||||
"modified_on": "2021-06-28T22:38:10.000000Z"
|
||||
},
|
||||
])
|
||||
|
||||
# we don't care about the POST/create return values
|
||||
@@ -319,7 +397,7 @@ class TestCloudflareProvider(TestCase):
|
||||
# Test out the create rate-limit handling, then 9 successes
|
||||
provider._request.side_effect = [
|
||||
CloudflareRateLimitError('{}'),
|
||||
] + ([None] * 3)
|
||||
] + ([None] * 5)
|
||||
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, 'nc', {
|
||||
@@ -332,14 +410,27 @@ class TestCloudflareProvider(TestCase):
|
||||
'type': 'A',
|
||||
'value': '3.2.3.4'
|
||||
}))
|
||||
wanted.add_record(Record.new(wanted, 'urlfwd', {
|
||||
'ttl': 300,
|
||||
'type': 'URLFWD',
|
||||
'value': {
|
||||
'path': '/*', # path change
|
||||
'target': 'https://www.unit.tests/', # target change
|
||||
'code': 301, # status_code change
|
||||
'masking': '2',
|
||||
'query': 0,
|
||||
}
|
||||
}))
|
||||
|
||||
plan = provider.plan(wanted)
|
||||
# only see the delete & ttl update, below min-ttl is filtered out
|
||||
self.assertEquals(2, len(plan.changes))
|
||||
self.assertEquals(2, provider.apply(plan))
|
||||
self.assertEquals(4, len(plan.changes))
|
||||
self.assertEquals(4, provider.apply(plan))
|
||||
self.assertTrue(plan.exists)
|
||||
# creates a the new value and then deletes all the old
|
||||
provider._request.assert_has_calls([
|
||||
call('DELETE', '/zones/42/'
|
||||
'pagerules/2a9141b18ffb0e6aed826050eec970b8'),
|
||||
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
|
||||
'dns_records/fc12ab34cd5611334422ab3322997653'),
|
||||
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
|
||||
@@ -351,7 +442,29 @@ class TestCloudflareProvider(TestCase):
|
||||
'name': 'ttl.unit.tests',
|
||||
'proxied': False,
|
||||
'ttl': 300
|
||||
})
|
||||
}),
|
||||
call('PUT', '/zones/42/pagerules/'
|
||||
'2a9140b17ffb0e6aed826049eec970b7', data={
|
||||
'targets': [
|
||||
{
|
||||
'target': 'url',
|
||||
'constraint': {
|
||||
'operator': 'matches',
|
||||
'value': 'urlfwd.unit.tests/*'
|
||||
}
|
||||
}
|
||||
],
|
||||
'actions': [
|
||||
{
|
||||
'id': 'forwarding_url',
|
||||
'value': {
|
||||
'url': 'https://www.unit.tests/',
|
||||
'status_code': 301
|
||||
}
|
||||
}
|
||||
],
|
||||
'status': 'active',
|
||||
}),
|
||||
])
|
||||
|
||||
def test_update_add_swap(self):
|
||||
@@ -500,6 +613,56 @@ class TestCloudflareProvider(TestCase):
|
||||
"auto_added": False
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2a9140b17ffb0e6aed826049eec974b7",
|
||||
"targets": [
|
||||
{
|
||||
"target": "url",
|
||||
"constraint": {
|
||||
"operator": "matches",
|
||||
"value": "urlfwd1.unit.tests/"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "forwarding_url",
|
||||
"value": {
|
||||
"url": "https://www.unit.tests",
|
||||
"status_code": 302
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 1,
|
||||
"status": "active",
|
||||
"created_on": "2021-06-25T20:10:50.000000Z",
|
||||
"modified_on": "2021-06-28T22:38:10.000000Z"
|
||||
},
|
||||
{
|
||||
"id": "2a9141b18ffb0e6aed826054eec970b8",
|
||||
"targets": [
|
||||
{
|
||||
"target": "url",
|
||||
"constraint": {
|
||||
"operator": "matches",
|
||||
"value": "urlfwd1.unit.tests/target"
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"id": "forwarding_url",
|
||||
"value": {
|
||||
"url": "https://target.unit.tests",
|
||||
"status_code": 301
|
||||
}
|
||||
}
|
||||
],
|
||||
"priority": 2,
|
||||
"status": "active",
|
||||
"created_on": "2021-06-25T20:10:50.000000Z",
|
||||
"modified_on": "2021-06-28T22:38:10.000000Z"
|
||||
},
|
||||
])
|
||||
|
||||
provider._request = Mock()
|
||||
@@ -513,6 +676,8 @@ class TestCloudflareProvider(TestCase):
|
||||
}, # zone create
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
]
|
||||
|
||||
# Add something and delete something
|
||||
@@ -523,14 +688,46 @@ class TestCloudflareProvider(TestCase):
|
||||
# This matches the zone data above, one to delete, one to leave
|
||||
'values': ['ns1.foo.bar.', 'ns2.foo.bar.'],
|
||||
})
|
||||
exstingurlfwd = Record.new(zone, 'urlfwd1', {
|
||||
'ttl': 300,
|
||||
'type': 'URLFWD',
|
||||
'values': [
|
||||
{
|
||||
'path': '/',
|
||||
'target': 'https://www.unit.tests',
|
||||
'code': 302,
|
||||
'masking': '2',
|
||||
'query': 0,
|
||||
},
|
||||
{
|
||||
'path': '/target',
|
||||
'target': 'https://target.unit.tests',
|
||||
'code': 301,
|
||||
'masking': '2',
|
||||
'query': 0,
|
||||
}
|
||||
]
|
||||
})
|
||||
new = Record.new(zone, '', {
|
||||
'ttl': 300,
|
||||
'type': 'NS',
|
||||
# This leaves one and deletes one
|
||||
'value': 'ns2.foo.bar.',
|
||||
})
|
||||
newurlfwd = Record.new(zone, 'urlfwd1', {
|
||||
'ttl': 300,
|
||||
'type': 'URLFWD',
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'https://www.unit.tests',
|
||||
'code': 302,
|
||||
'masking': '2',
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
change = Update(existing, new)
|
||||
plan = Plan(zone, zone, [change], True)
|
||||
changeurlfwd = Update(exstingurlfwd, newurlfwd)
|
||||
plan = Plan(zone, zone, [change, changeurlfwd], True)
|
||||
provider._apply(plan)
|
||||
|
||||
# Get zones, create zone, create a record, delete a record
|
||||
@@ -548,7 +745,31 @@ class TestCloudflareProvider(TestCase):
|
||||
'ttl': 300
|
||||
}),
|
||||
call('DELETE', '/zones/42/dns_records/'
|
||||
'fc12ab34cd5611334422ab3322997653')
|
||||
'fc12ab34cd5611334422ab3322997653'),
|
||||
call('PUT', '/zones/42/pagerules/'
|
||||
'2a9140b17ffb0e6aed826049eec974b7', data={
|
||||
'targets': [
|
||||
{
|
||||
'target': 'url',
|
||||
'constraint': {
|
||||
'operator': 'matches',
|
||||
'value': 'urlfwd1.unit.tests/'
|
||||
}
|
||||
}
|
||||
],
|
||||
'actions': [
|
||||
{
|
||||
'id': 'forwarding_url',
|
||||
'value': {
|
||||
'url': 'https://www.unit.tests',
|
||||
'status_code': 302
|
||||
}
|
||||
}
|
||||
],
|
||||
'status': 'active'
|
||||
}),
|
||||
call('DELETE', '/zones/42/pagerules/'
|
||||
'2a9141b18ffb0e6aed826054eec970b8'),
|
||||
])
|
||||
|
||||
def test_ptr(self):
|
||||
@@ -1410,3 +1631,11 @@ class TestCloudflareProvider(TestCase):
|
||||
with self.assertRaises(CloudflareRateLimitError) as ctx:
|
||||
provider.zone_records(zone)
|
||||
self.assertEquals('last', text_type(ctx.exception))
|
||||
|
||||
def test_ttl_mapping(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
|
||||
self.assertEquals(120, provider._ttl_data(120))
|
||||
self.assertEquals(120, provider._ttl_data(120))
|
||||
self.assertEquals(3600, provider._ttl_data(3600))
|
||||
self.assertEquals(300, provider._ttl_data(1))
|
||||
|
||||
@@ -132,7 +132,7 @@ class TestConstellixProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded, no unsupported
|
||||
n = len(self.expected.records) - 7
|
||||
n = len(self.expected.records) - 8
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ class TestDigitalOceanProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded, no unsupported
|
||||
n = len(self.expected.records) - 9
|
||||
n = len(self.expected.records) - 10
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestDnsimpleProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded
|
||||
n = len(self.expected.records) - 7
|
||||
n = len(self.expected.records) - 8
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
@@ -134,7 +134,7 @@ class TestDnsMadeEasyProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded, no unsupported
|
||||
n = len(self.expected.records) - 9
|
||||
n = len(self.expected.records) - 10
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ class TestEasyDNSProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded, no unsupported
|
||||
n = len(self.expected.records) - 8
|
||||
n = len(self.expected.records) - 9
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
@@ -193,7 +193,7 @@ class TestGandiProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded, no LOC
|
||||
n = len(self.expected.records) - 5
|
||||
n = len(self.expected.records) - 6
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestHetznerProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded, no unsupported
|
||||
n = len(self.expected.records) - 9
|
||||
n = len(self.expected.records) - 10
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
@@ -109,6 +109,17 @@ class TestNs1Provider(TestCase):
|
||||
'value': 'ca.unit.tests',
|
||||
},
|
||||
}))
|
||||
expected.add(Record.new(zone, 'urlfwd', {
|
||||
'ttl': 41,
|
||||
'type': 'URLFWD',
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo.unit.tests',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
},
|
||||
}))
|
||||
|
||||
ns1_records = [{
|
||||
'type': 'A',
|
||||
@@ -164,6 +175,11 @@ class TestNs1Provider(TestCase):
|
||||
'ttl': 40,
|
||||
'short_answers': ['0 issue ca.unit.tests'],
|
||||
'domain': 'unit.tests.',
|
||||
}, {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 41,
|
||||
'short_answers': ['/ http://foo.unit.tests 301 2 0'],
|
||||
'domain': 'urlfwd.unit.tests.',
|
||||
}]
|
||||
|
||||
@patch('ns1.rest.records.Records.retrieve')
|
||||
@@ -345,7 +361,7 @@ class TestNs1Provider(TestCase):
|
||||
# Test out the create rate-limit handling, then 9 successes
|
||||
record_create_mock.side_effect = [
|
||||
RateLimitException('boo', period=0),
|
||||
] + ([None] * 9)
|
||||
] + ([None] * 10)
|
||||
|
||||
got_n = provider.apply(plan)
|
||||
self.assertEquals(expected_n, got_n)
|
||||
|
||||
@@ -185,7 +185,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
expected_n = len(expected.records) - 3
|
||||
expected_n = len(expected.records) - 4
|
||||
self.assertEquals(19, expected_n)
|
||||
|
||||
# No diffs == no changes
|
||||
@@ -291,7 +291,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
self.assertEquals(22, len(expected.records))
|
||||
self.assertEquals(23, len(expected.records))
|
||||
|
||||
# A small change to a single record
|
||||
with requests_mock() as mock:
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestYamlProvider(TestCase):
|
||||
|
||||
# without it we see everything
|
||||
source.populate(zone)
|
||||
self.assertEquals(22, len(zone.records))
|
||||
self.assertEquals(23, len(zone.records))
|
||||
|
||||
source.populate(dynamic_zone)
|
||||
self.assertEquals(6, len(dynamic_zone.records))
|
||||
@@ -58,12 +58,12 @@ class TestYamlProvider(TestCase):
|
||||
|
||||
# We add everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(19, len([c for c in plan.changes
|
||||
self.assertEquals(20, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
self.assertFalse(isfile(yaml_file))
|
||||
|
||||
# Now actually do it
|
||||
self.assertEquals(19, target.apply(plan))
|
||||
self.assertEquals(20, target.apply(plan))
|
||||
self.assertTrue(isfile(yaml_file))
|
||||
|
||||
# Dynamic plan
|
||||
@@ -87,7 +87,7 @@ class TestYamlProvider(TestCase):
|
||||
|
||||
# A 2nd sync should still create everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(19, len([c for c in plan.changes
|
||||
self.assertEquals(20, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
|
||||
with open(yaml_file) as fh:
|
||||
@@ -107,6 +107,7 @@ class TestYamlProvider(TestCase):
|
||||
self.assertTrue('values' in data.pop('sub'))
|
||||
self.assertTrue('values' in data.pop('txt'))
|
||||
self.assertTrue('values' in data.pop('loc'))
|
||||
self.assertTrue('values' in data.pop('urlfwd'))
|
||||
# these are stored as singular 'value'
|
||||
self.assertTrue('value' in data.pop('_imap._tcp'))
|
||||
self.assertTrue('value' in data.pop('_pop3._tcp'))
|
||||
@@ -248,7 +249,7 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
# without it we see everything
|
||||
source.populate(zone)
|
||||
self.assertEquals(19, len(zone.records))
|
||||
self.assertEquals(20, len(zone.records))
|
||||
|
||||
source.populate(dynamic_zone)
|
||||
self.assertEquals(5, len(dynamic_zone.records))
|
||||
@@ -263,12 +264,12 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
# We add everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(16, len([c for c in plan.changes
|
||||
self.assertEquals(17, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
self.assertFalse(isdir(zone_dir))
|
||||
|
||||
# Now actually do it
|
||||
self.assertEquals(16, target.apply(plan))
|
||||
self.assertEquals(17, target.apply(plan))
|
||||
|
||||
# Dynamic plan
|
||||
plan = target.plan(dynamic_zone)
|
||||
@@ -291,7 +292,7 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
# A 2nd sync should still create everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(16, len([c for c in plan.changes
|
||||
self.assertEquals(17, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
|
||||
yaml_file = join(zone_dir, '$unit.tests.yaml')
|
||||
@@ -306,7 +307,8 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
# These records are stored as plural "values." Check each file to
|
||||
# ensure correctness.
|
||||
for record_name in ('_srv._tcp', 'mx', 'naptr', 'sub', 'txt'):
|
||||
for record_name in ('_srv._tcp', 'mx', 'naptr', 'sub', 'txt',
|
||||
'urlfwd'):
|
||||
yaml_file = join(zone_dir, '{}.yaml'.format(record_name))
|
||||
self.assertTrue(isfile(yaml_file))
|
||||
with open(yaml_file) as fh:
|
||||
|
||||
@@ -12,8 +12,8 @@ from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \
|
||||
CaaValue, CnameRecord, DnameRecord, Create, Delete, GeoValue, LocRecord, \
|
||||
LocValue, MxRecord, MxValue, NaptrRecord, NaptrValue, NsRecord, \
|
||||
PtrRecord, Record, SshfpRecord, SshfpValue, SpfRecord, SrvRecord, \
|
||||
SrvValue, TxtRecord, Update, ValidationError, _Dynamic, _DynamicPool, \
|
||||
_DynamicRule
|
||||
SrvValue, TxtRecord, Update, UrlfwdRecord, UrlfwdValue, ValidationError, \
|
||||
_Dynamic, _DynamicPool, _DynamicRule
|
||||
from octodns.zone import Zone
|
||||
|
||||
from helpers import DynamicProvider, GeoProvider, SimpleProvider
|
||||
@@ -884,6 +884,112 @@ class TestRecord(TestCase):
|
||||
b_value = 'b other'
|
||||
self.assertMultipleValues(TxtRecord, a_values, b_value)
|
||||
|
||||
def test_urlfwd(self):
|
||||
a_values = [{
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}, {
|
||||
'path': '/target',
|
||||
'target': 'http://target',
|
||||
'code': 302,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}]
|
||||
a_data = {'ttl': 30, 'values': a_values}
|
||||
a = UrlfwdRecord(self.zone, 'a', a_data)
|
||||
self.assertEquals('a', a.name)
|
||||
self.assertEquals('a.unit.tests.', a.fqdn)
|
||||
self.assertEquals(30, a.ttl)
|
||||
self.assertEquals(a_values[0]['path'], a.values[0].path)
|
||||
self.assertEquals(a_values[0]['target'], a.values[0].target)
|
||||
self.assertEquals(a_values[0]['code'], a.values[0].code)
|
||||
self.assertEquals(a_values[0]['masking'], a.values[0].masking)
|
||||
self.assertEquals(a_values[0]['query'], a.values[0].query)
|
||||
self.assertEquals(a_values[1]['path'], a.values[1].path)
|
||||
self.assertEquals(a_values[1]['target'], a.values[1].target)
|
||||
self.assertEquals(a_values[1]['code'], a.values[1].code)
|
||||
self.assertEquals(a_values[1]['masking'], a.values[1].masking)
|
||||
self.assertEquals(a_values[1]['query'], a.values[1].query)
|
||||
self.assertEquals(a_data, a.data)
|
||||
|
||||
b_value = {
|
||||
'path': '/',
|
||||
'target': 'http://location',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}
|
||||
b_data = {'ttl': 30, 'value': b_value}
|
||||
b = UrlfwdRecord(self.zone, 'b', b_data)
|
||||
self.assertEquals(b_value['path'], b.values[0].path)
|
||||
self.assertEquals(b_value['target'], b.values[0].target)
|
||||
self.assertEquals(b_value['code'], b.values[0].code)
|
||||
self.assertEquals(b_value['masking'], b.values[0].masking)
|
||||
self.assertEquals(b_value['query'], b.values[0].query)
|
||||
self.assertEquals(b_data, b.data)
|
||||
|
||||
target = SimpleProvider()
|
||||
# No changes with self
|
||||
self.assertFalse(a.changes(a, target))
|
||||
# Diff in path causes change
|
||||
other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values})
|
||||
other.values[0].path = '/change'
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
# Diff in target causes change
|
||||
other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values})
|
||||
other.values[0].target = 'http://target'
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
# Diff in code causes change
|
||||
other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values})
|
||||
other.values[0].code = 302
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
# Diff in masking causes change
|
||||
other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values})
|
||||
other.values[0].masking = 0
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
# Diff in query causes change
|
||||
other = UrlfwdRecord(self.zone, 'a', {'ttl': 30, 'values': a_values})
|
||||
other.values[0].query = 1
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
|
||||
# hash
|
||||
v = UrlfwdValue({
|
||||
'path': '/',
|
||||
'target': 'http://place',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
})
|
||||
o = UrlfwdValue({
|
||||
'path': '/location',
|
||||
'target': 'http://redirect',
|
||||
'code': 302,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
})
|
||||
values = set()
|
||||
values.add(v)
|
||||
self.assertTrue(v in values)
|
||||
self.assertFalse(o in values)
|
||||
values.add(o)
|
||||
self.assertTrue(o in values)
|
||||
|
||||
# __repr__ doesn't blow up
|
||||
a.__repr__()
|
||||
|
||||
def test_record_new(self):
|
||||
txt = Record.new(self.zone, 'txt', {
|
||||
'ttl': 44,
|
||||
@@ -3019,6 +3125,203 @@ class TestRecordValidation(TestCase):
|
||||
# should be chunked values, with quoting
|
||||
self.assertEquals(single.chunked_values, chunked.chunked_values)
|
||||
|
||||
def test_URLFWD(self):
|
||||
# doesn't blow up
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'values': [{
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}, {
|
||||
'path': '/target',
|
||||
'target': 'http://target',
|
||||
'code': 302,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}]
|
||||
})
|
||||
|
||||
# missing path
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['missing path'], ctx.exception.reasons)
|
||||
|
||||
# missing target
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['missing target'], ctx.exception.reasons)
|
||||
|
||||
# missing code
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['missing code'], ctx.exception.reasons)
|
||||
|
||||
# invalid code
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 'nope',
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['invalid return code "nope"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# unrecognized code
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 3,
|
||||
'masking': 2,
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['unrecognized return code "3"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# missing masking
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['missing masking'], ctx.exception.reasons)
|
||||
|
||||
# invalid masking
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 'nope',
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['invalid masking setting "nope"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# unrecognized masking
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 3,
|
||||
'query': 0,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['unrecognized masking setting "3"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# missing query
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['missing query'], ctx.exception.reasons)
|
||||
|
||||
# invalid query
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 'nope',
|
||||
}
|
||||
})
|
||||
self.assertEquals(['invalid query setting "nope"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# unrecognized query
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'URLFWD',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'path': '/',
|
||||
'target': 'http://foo',
|
||||
'code': 301,
|
||||
'masking': 2,
|
||||
'query': 3,
|
||||
}
|
||||
})
|
||||
self.assertEquals(['unrecognized query setting "3"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
|
||||
class TestDynamicRecords(TestCase):
|
||||
zone = Zone('unit.tests.', [])
|
||||
|
||||
Reference in New Issue
Block a user