mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge branch 'master' into gcore-provider
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
name: OctoDNS
|
||||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
@@ -7,8 +10,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
# Tested versions based on dates in https://devguide.python.org/devcycle/#end-of-life-branches,
|
||||
# with the addition of 2.7 b/c it's still if pretty wide active use.
|
||||
python-version: [2.7, 3.6, 3.7, 3.8, 3.9]
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
steps:
|
||||
- uses: actions/checkout@master
|
||||
- name: Setup python
|
||||
|
||||
+2
-1
@@ -5,8 +5,8 @@
|
||||
*.pyc
|
||||
.coverage
|
||||
.env
|
||||
/config/
|
||||
/build/
|
||||
/config/
|
||||
coverage.xml
|
||||
dist/
|
||||
env/
|
||||
@@ -14,4 +14,5 @@ htmlcov/
|
||||
nosetests.xml
|
||||
octodns.egg-info/
|
||||
output/
|
||||
tests/zones/unit.tests.
|
||||
tmp/
|
||||
|
||||
@@ -28,6 +28,7 @@ It is similar to [Netflix/denominator](https://github.com/Netflix/denominator).
|
||||
- [Dynamic sources](#dynamic-sources)
|
||||
- [Contributing](#contributing)
|
||||
- [Getting help](#getting-help)
|
||||
- [Related Projects & Resources](#related-projects--resources)
|
||||
- [License](#license)
|
||||
- [Authors](#authors)
|
||||
|
||||
@@ -185,7 +186,7 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
|--|--|--|--|--|
|
||||
| [AzureProvider](/octodns/provider/azuredns.py) | azure-mgmt-dns | A, AAAA, CAA, CNAME, MX, NS, PTR, SRV, TXT | No | |
|
||||
| [Akamai](/octodns/provider/edgedns.py) | edgegrid-python | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TXT | No | |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | | A, AAAA, ALIAS, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | | A, AAAA, ALIAS, CAA, CNAME, LOC, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [ConstellixProvider](/octodns/provider/constellix.py) | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [DigitalOceanProvider](/octodns/provider/digitalocean.py) | | A, AAAA, CAA, CNAME, MX, NS, TXT, SRV | No | CAA tags restricted |
|
||||
| [DnsMadeEasyProvider](/octodns/provider/dnsmadeeasy.py) | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
@@ -206,7 +207,7 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| [Selectel](/octodns/provider/selectel.py) | | A, AAAA, CNAME, MX, NS, SPF, SRV, TXT | No | |
|
||||
| [Transip](/octodns/provider/transip.py) | transip | A, AAAA, CNAME, MX, SRV, SPF, TXT, SSHFP, CAA | No | |
|
||||
| [UltraDns](/octodns/provider/ultra.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | |
|
||||
| [AxfrSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
|
||||
| [AxfrSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, LOC, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
|
||||
| [ZoneFileSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only |
|
||||
| [TinyDnsFileSource](/octodns/source/tinydns.py) | | A, CNAME, MX, NS, PTR | No | read-only |
|
||||
| [YamlProvider](/octodns/provider/yaml.py) | | All | Yes | config |
|
||||
@@ -226,6 +227,8 @@ Most of the things included in OctoDNS are providers, the obvious difference bei
|
||||
|
||||
The `class` key in the providers config section can be used to point to arbitrary classes in the python path so internal or 3rd party providers can easily be included with no coordination beyond getting them into PYTHONPATH, most likely installed into the virtualenv with OctoDNS.
|
||||
|
||||
For examples of building third-party sources and providers, see [Related Projects & Resources](#related-projects--resources).
|
||||
|
||||
## Other Uses
|
||||
|
||||
### Syncing between providers
|
||||
@@ -287,6 +290,29 @@ Please see our [contributing document](/CONTRIBUTING.md) if you would like to pa
|
||||
|
||||
If you have a problem or suggestion, please [open an issue](https://github.com/octodns/octodns/issues/new) in this repository, and we will do our best to help. Please note that this project adheres to the [Contributor Covenant Code of Conduct](/CODE_OF_CONDUCT.md).
|
||||
|
||||
## Related Projects & Resources
|
||||
|
||||
- **GitHub Action:** [OctoDNS-Sync](https://github.com/marketplace/actions/octodns-sync)
|
||||
- **Sample Implementations.** See how others are using it
|
||||
- [`hackclub/dns`](https://github.com/hackclub/dns)
|
||||
- [`kubernetes/k8s.io:/dns`](https://github.com/kubernetes/k8s.io/tree/main/dns)
|
||||
- [`g0v-network/domains`](https://github.com/g0v-network/domains)
|
||||
- [`jekyll/dns`](https://github.com/jekyll/dns)
|
||||
- **Custom Sources & Providers.**
|
||||
- [`octodns/octodns-ddns`](https://github.com/octodns/octodns-ddns): A simple Dynamic DNS source.
|
||||
- [`doddo/octodns-lexicon`](https://github.com/doddo/octodns-lexicon): Use [Lexicon](https://github.com/AnalogJ/lexicon) providers as octoDNS providers.
|
||||
- [`asyncon/octoblox`](https://github.com/asyncon/octoblox): [Infoblox](https://www.infoblox.com/) provider.
|
||||
- [`sukiyaki/octodns-netbox`](https://github.com/sukiyaki/octodns-netbox): [NetBox](https://github.com/netbox-community/netbox) source.
|
||||
- [`kompetenzbolzen/octodns-custom-provider`](https://github.com/kompetenzbolzen/octodns-custom-provider): zonefile provider & phpIPAM source.
|
||||
- **Resources.**
|
||||
- Article: [Visualising DNS records with Neo4j](https://medium.com/@costask/querying-and-visualising-octodns-records-with-neo4j-f4f72ab2d474) + code
|
||||
- Video: [FOSDEM 2019 - DNS as code with octodns](https://archive.fosdem.org/2019/schedule/event/dns_octodns/)
|
||||
- GitHub Blog: [Enabling DNS split authority with OctoDNS](https://github.blog/2017-04-27-enabling-split-authority-dns-with-octodns/)
|
||||
- Tutorial: [How To Deploy and Manage Your DNS using OctoDNS on Ubuntu 18.04](https://www.digitalocean.com/community/tutorials/how-to-deploy-and-manage-your-dns-using-octodns-on-ubuntu-18-04)
|
||||
- Cloudflare Blog: [Improving the Resiliency of Our Infrastructure DNS Zone](https://blog.cloudflare.com/improving-the-resiliency-of-our-infrastructure-dns-zone/)
|
||||
|
||||
If you know of any other resources, please do let us know!
|
||||
|
||||
## License
|
||||
|
||||
OctoDNS is licensed under the [MIT license](LICENSE).
|
||||
|
||||
@@ -10,6 +10,7 @@ OctoDNS supports the following record types:
|
||||
* `CAA`
|
||||
* `CNAME`
|
||||
* `DNAME`
|
||||
* `LOC`
|
||||
* `MX`
|
||||
* `NAPTR`
|
||||
* `NS`
|
||||
|
||||
@@ -91,7 +91,10 @@ class BaseProvider(BaseSource):
|
||||
self.log.info('apply: disabled')
|
||||
return 0
|
||||
|
||||
self.log.info('apply: making changes')
|
||||
zone_name = plan.desired.name
|
||||
num_changes = len(plan.changes)
|
||||
self.log.info('apply: making %d changes to %s', num_changes,
|
||||
zone_name)
|
||||
self._apply(plan)
|
||||
return len(plan.changes)
|
||||
|
||||
|
||||
@@ -75,8 +75,8 @@ class CloudflareProvider(BaseProvider):
|
||||
'''
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR',
|
||||
'SRV', 'SPF', 'TXT'))
|
||||
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'LOC', 'MX', 'NS',
|
||||
'PTR', 'SRV', 'SPF', 'TXT'))
|
||||
|
||||
MIN_TTL = 120
|
||||
TIMEOUT = 15
|
||||
@@ -133,6 +133,7 @@ class CloudflareProvider(BaseProvider):
|
||||
timeout=self.TIMEOUT)
|
||||
self.log.debug('_request: status=%d', resp.status_code)
|
||||
if resp.status_code == 400:
|
||||
self.log.debug('_request: data=%s', data)
|
||||
raise CloudflareError(resp.json())
|
||||
if resp.status_code == 403:
|
||||
raise CloudflareAuthenticationError(resp.json())
|
||||
@@ -142,6 +143,11 @@ class CloudflareProvider(BaseProvider):
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def _change_keyer(self, change):
|
||||
key = change.__class__.__name__
|
||||
order = {'Delete': 0, 'Create': 1, 'Update': 2}
|
||||
return order[key]
|
||||
|
||||
@property
|
||||
def zones(self):
|
||||
if self._zones is None:
|
||||
@@ -216,6 +222,30 @@ class CloudflareProvider(BaseProvider):
|
||||
_data_for_ALIAS = _data_for_CNAME
|
||||
_data_for_PTR = _data_for_CNAME
|
||||
|
||||
def _data_for_LOC(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
r = record['data']
|
||||
values.append({
|
||||
'lat_degrees': int(r['lat_degrees']),
|
||||
'lat_minutes': int(r['lat_minutes']),
|
||||
'lat_seconds': float(r['lat_seconds']),
|
||||
'lat_direction': r['lat_direction'],
|
||||
'long_degrees': int(r['long_degrees']),
|
||||
'long_minutes': int(r['long_minutes']),
|
||||
'long_seconds': float(r['long_seconds']),
|
||||
'long_direction': r['long_direction'],
|
||||
'altitude': float(r['altitude']),
|
||||
'size': float(r['size']),
|
||||
'precision_horz': float(r['precision_horz']),
|
||||
'precision_vert': float(r['precision_vert']),
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
for r in records:
|
||||
@@ -239,11 +269,13 @@ class CloudflareProvider(BaseProvider):
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
for r in records:
|
||||
target = ('{}.'.format(r['data']['target'])
|
||||
if r['data']['target'] != "." else ".")
|
||||
values.append({
|
||||
'priority': r['data']['priority'],
|
||||
'weight': r['data']['weight'],
|
||||
'port': r['data']['port'],
|
||||
'target': '{}.'.format(r['data']['target']),
|
||||
'target': target,
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
@@ -384,6 +416,25 @@ class CloudflareProvider(BaseProvider):
|
||||
|
||||
_contents_for_PTR = _contents_for_CNAME
|
||||
|
||||
def _contents_for_LOC(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
'data': {
|
||||
'lat_degrees': value.lat_degrees,
|
||||
'lat_minutes': value.lat_minutes,
|
||||
'lat_seconds': value.lat_seconds,
|
||||
'lat_direction': value.lat_direction,
|
||||
'long_degrees': value.long_degrees,
|
||||
'long_minutes': value.long_minutes,
|
||||
'long_seconds': value.long_seconds,
|
||||
'long_direction': value.long_direction,
|
||||
'altitude': value.altitude,
|
||||
'size': value.size,
|
||||
'precision_horz': value.precision_horz,
|
||||
'precision_vert': value.precision_vert,
|
||||
}
|
||||
}
|
||||
|
||||
def _contents_for_MX(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
@@ -405,6 +456,8 @@ class CloudflareProvider(BaseProvider):
|
||||
name = subdomain
|
||||
|
||||
for value in record.values:
|
||||
target = value.target[:-1] if value.target != "." else "."
|
||||
|
||||
yield {
|
||||
'data': {
|
||||
'service': service,
|
||||
@@ -413,7 +466,7 @@ class CloudflareProvider(BaseProvider):
|
||||
'priority': value.priority,
|
||||
'weight': value.weight,
|
||||
'port': value.port,
|
||||
'target': value.target[:-1],
|
||||
'target': target,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -456,7 +509,7 @@ class CloudflareProvider(BaseProvider):
|
||||
# new records cleanly. In general when there are multiple records for a
|
||||
# name & type each will have a distinct/consistent `content` that can
|
||||
# serve as a unique identifier.
|
||||
# BUT... there are exceptions. MX, CAA, and SRV don't have a simple
|
||||
# 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']
|
||||
@@ -468,6 +521,22 @@ class CloudflareProvider(BaseProvider):
|
||||
elif _type == 'SRV':
|
||||
data = data['data']
|
||||
return '{port} {priority} {target} {weight}'.format(**data)
|
||||
elif _type == 'LOC':
|
||||
data = data['data']
|
||||
loc = (
|
||||
'{lat_degrees}',
|
||||
'{lat_minutes}',
|
||||
'{lat_seconds}',
|
||||
'{lat_direction}',
|
||||
'{long_degrees}',
|
||||
'{long_minutes}',
|
||||
'{long_seconds}',
|
||||
'{long_direction}',
|
||||
'{altitude}',
|
||||
'{size}',
|
||||
'{precision_horz}',
|
||||
'{precision_vert}')
|
||||
return ' '.join(loc).format(**data)
|
||||
return data['content']
|
||||
|
||||
def _apply_Create(self, change):
|
||||
@@ -616,6 +685,11 @@ class CloudflareProvider(BaseProvider):
|
||||
self.zones[name] = zone_id
|
||||
self._zone_records[name] = {}
|
||||
|
||||
# Force the operation order to be Delete() -> Create() -> Update()
|
||||
# This will help avoid problems in updating a CNAME record into an
|
||||
# A record and vice-versa
|
||||
changes.sort(key=self._change_keyer)
|
||||
|
||||
for change in changes:
|
||||
class_name = change.__class__.__name__
|
||||
getattr(self, '_apply_{}'.format(class_name))(change)
|
||||
|
||||
@@ -186,10 +186,14 @@ class DigitalOceanProvider(BaseProvider):
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
target = (
|
||||
'{}.'.format(record['data'])
|
||||
if record['data'] != "." else "."
|
||||
)
|
||||
values.append({
|
||||
'port': record['port'],
|
||||
'priority': record['priority'],
|
||||
'target': '{}.'.format(record['data']),
|
||||
'target': target,
|
||||
'weight': record['weight']
|
||||
})
|
||||
return {
|
||||
|
||||
@@ -218,12 +218,23 @@ class DnsimpleProvider(BaseProvider):
|
||||
try:
|
||||
weight, port, target = record['content'].split(' ', 2)
|
||||
except ValueError:
|
||||
# see _data_for_NAPTR's continue
|
||||
# their api/website will let you create invalid records, this
|
||||
# essentially handles that by ignoring them for values
|
||||
# purposes. That will cause updates to happen to delete them if
|
||||
# they shouldn't exist or update them if they're wrong
|
||||
self.log.warning(
|
||||
'_data_for_SRV: unsupported %s record (%s)',
|
||||
_type,
|
||||
record['content']
|
||||
)
|
||||
continue
|
||||
|
||||
target = '{}.'.format(target) if target != "." else "."
|
||||
|
||||
values.append({
|
||||
'port': port,
|
||||
'priority': record['priority'],
|
||||
'target': '{}.'.format(target),
|
||||
'target': target,
|
||||
'weight': weight
|
||||
})
|
||||
return {
|
||||
@@ -270,6 +281,10 @@ class DnsimpleProvider(BaseProvider):
|
||||
for record in self.zone_records(zone):
|
||||
_type = record['type']
|
||||
if _type not in self.SUPPORTS:
|
||||
self.log.warning(
|
||||
'populate: skipping unsupported %s record',
|
||||
_type
|
||||
)
|
||||
continue
|
||||
elif _type == 'TXT' and record['content'].startswith('ALIAS for'):
|
||||
# ALIAS has a "ride along" TXT record with 'ALIAS for XXXX',
|
||||
@@ -290,6 +305,27 @@ class DnsimpleProvider(BaseProvider):
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def supports(self, record):
|
||||
# DNSimple does not support empty/NULL SRV records
|
||||
#
|
||||
# Fails silently and leaves a corrupt record
|
||||
#
|
||||
# Skip the record and continue
|
||||
if record._type == "SRV":
|
||||
if 'value' in record.data:
|
||||
targets = (record.data['value']['target'],)
|
||||
else:
|
||||
targets = [value['target'] for value in record.data['values']]
|
||||
|
||||
if "." in targets:
|
||||
self.log.warning(
|
||||
'supports: unsupported %s record with target (%s)',
|
||||
record._type, targets
|
||||
)
|
||||
return False
|
||||
|
||||
return super(DnsimpleProvider, self).supports(record)
|
||||
|
||||
def _params_for_multiple(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
|
||||
@@ -284,6 +284,30 @@ class DnsMadeEasyProvider(BaseProvider):
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def supports(self, record):
|
||||
# DNS Made Easy does not support empty/NULL SRV records
|
||||
#
|
||||
# Attempting to sync such a record would generate the following error
|
||||
#
|
||||
# octodns.provider.dnsmadeeasy.DnsMadeEasyClientBadRequest:
|
||||
# - Record value may not be a standalone dot.
|
||||
#
|
||||
# Skip the record and continue
|
||||
if record._type == "SRV":
|
||||
if 'value' in record.data:
|
||||
targets = (record.data['value']['target'],)
|
||||
else:
|
||||
targets = [value['target'] for value in record.data['values']]
|
||||
|
||||
if "." in targets:
|
||||
self.log.warning(
|
||||
'supports: unsupported %s record with target (%s)',
|
||||
record._type, targets
|
||||
)
|
||||
return False
|
||||
|
||||
return super(DnsMadeEasyProvider, self).supports(record)
|
||||
|
||||
def _params_for_multiple(self, record):
|
||||
for value in record.values:
|
||||
yield {
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from requests import HTTPError, Session
|
||||
from operator import itemgetter
|
||||
import logging
|
||||
|
||||
from ..record import Create, Record
|
||||
@@ -15,8 +16,8 @@ from .base import BaseProvider
|
||||
class PowerDnsBaseProvider(BaseProvider):
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
|
||||
'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT'))
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'LOC', 'MX', 'NAPTR',
|
||||
'NS', 'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT'))
|
||||
TIMEOUT = 5
|
||||
|
||||
def __init__(self, id, host, api_key, port=8081,
|
||||
@@ -102,6 +103,33 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
_data_for_SPF = _data_for_quoted
|
||||
_data_for_TXT = _data_for_quoted
|
||||
|
||||
def _data_for_LOC(self, rrset):
|
||||
values = []
|
||||
for record in rrset['records']:
|
||||
lat_degrees, lat_minutes, lat_seconds, lat_direction, \
|
||||
long_degrees, long_minutes, long_seconds, long_direction, \
|
||||
altitude, size, precision_horz, precision_vert = \
|
||||
record['content'].replace('m', '').split(' ', 11)
|
||||
values.append({
|
||||
'lat_degrees': int(lat_degrees),
|
||||
'lat_minutes': int(lat_minutes),
|
||||
'lat_seconds': float(lat_seconds),
|
||||
'lat_direction': lat_direction,
|
||||
'long_degrees': int(long_degrees),
|
||||
'long_minutes': int(long_minutes),
|
||||
'long_seconds': float(long_seconds),
|
||||
'long_direction': long_direction,
|
||||
'altitude': float(altitude),
|
||||
'size': float(size),
|
||||
'precision_horz': float(precision_horz),
|
||||
'precision_vert': float(precision_vert),
|
||||
})
|
||||
return {
|
||||
'ttl': rrset['ttl'],
|
||||
'type': rrset['type'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_MX(self, rrset):
|
||||
values = []
|
||||
for record in rrset['records']:
|
||||
@@ -285,6 +313,27 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
_records_for_SPF = _records_for_quoted
|
||||
_records_for_TXT = _records_for_quoted
|
||||
|
||||
def _records_for_LOC(self, record):
|
||||
return [{
|
||||
'content':
|
||||
'%d %d %0.3f %s %d %d %.3f %s %0.2fm %0.2fm %0.2fm %0.2fm' %
|
||||
(
|
||||
int(v.lat_degrees),
|
||||
int(v.lat_minutes),
|
||||
float(v.lat_seconds),
|
||||
v.lat_direction,
|
||||
int(v.long_degrees),
|
||||
int(v.long_minutes),
|
||||
float(v.long_seconds),
|
||||
v.long_direction,
|
||||
float(v.altitude),
|
||||
float(v.size),
|
||||
float(v.precision_horz),
|
||||
float(v.precision_vert)
|
||||
),
|
||||
'disabled': False
|
||||
} for v in record.values]
|
||||
|
||||
def _records_for_MX(self, record):
|
||||
return [{
|
||||
'content': '{} {}'.format(v.preference, v.exchange),
|
||||
@@ -381,6 +430,12 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
for change in changes:
|
||||
class_name = change.__class__.__name__
|
||||
mods.append(getattr(self, '_mod_{}'.format(class_name))(change))
|
||||
|
||||
# Ensure that any DELETE modifications always occur before any REPLACE
|
||||
# modifications. This ensures that an A record can be replaced by a
|
||||
# CNAME record and vice-versa.
|
||||
mods.sort(key=itemgetter('changetype'))
|
||||
|
||||
self.log.debug('_apply: sending change request')
|
||||
|
||||
try:
|
||||
|
||||
@@ -104,7 +104,7 @@ class YamlProvider(BaseProvider):
|
||||
'''
|
||||
SUPPORTS_GEO = True
|
||||
SUPPORTS_DYNAMIC = True
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'DNAME', 'MX',
|
||||
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'DNAME', 'LOC', 'MX',
|
||||
'NAPTR', 'NS', 'PTR', 'SSHFP', 'SPF', 'SRV', 'TXT'))
|
||||
|
||||
def __init__(self, id, directory, default_ttl=3600, enforce_order=True,
|
||||
@@ -239,11 +239,13 @@ class SplitYamlProvider(YamlProvider):
|
||||
# instead of a file matching the record name.
|
||||
CATCHALL_RECORD_NAMES = ('*', '')
|
||||
|
||||
def __init__(self, id, directory, *args, **kwargs):
|
||||
def __init__(self, id, directory, extension='.', *args, **kwargs):
|
||||
super(SplitYamlProvider, self).__init__(id, directory, *args, **kwargs)
|
||||
self.extension = extension
|
||||
|
||||
def _zone_directory(self, zone):
|
||||
return join(self.directory, zone.name)
|
||||
filename = '{}{}'.format(zone.name[:-1], self.extension)
|
||||
return join(self.directory, filename)
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
|
||||
+193
-1
@@ -97,6 +97,7 @@ class Record(EqualityTupleMixin):
|
||||
'CAA': CaaRecord,
|
||||
'CNAME': CnameRecord,
|
||||
'DNAME': DnameRecord,
|
||||
'LOC': LocRecord,
|
||||
'MX': MxRecord,
|
||||
'NAPTR': NaptrRecord,
|
||||
'NS': NsRecord,
|
||||
@@ -758,7 +759,9 @@ class _TargetValue(object):
|
||||
reasons.append('empty value')
|
||||
elif not data:
|
||||
reasons.append('missing value')
|
||||
elif not FQDN(data, allow_underscores=True).is_valid:
|
||||
# NOTE: FQDN complains if the data it receives isn't a str, it doesn't
|
||||
# allow unicode... This is likely specific to 2.7
|
||||
elif not FQDN(str(data), allow_underscores=True).is_valid:
|
||||
reasons.append('{} value "{}" is not a valid FQDN'
|
||||
.format(_type, data))
|
||||
elif not data.endswith('.'):
|
||||
@@ -877,6 +880,195 @@ class DnameRecord(_DynamicMixin, _ValueMixin, Record):
|
||||
_value_type = DnameValue
|
||||
|
||||
|
||||
class LocValue(EqualityTupleMixin):
|
||||
# TODO: work out how to do defaults per RFC
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data, _type):
|
||||
int_keys = [
|
||||
'lat_degrees',
|
||||
'lat_minutes',
|
||||
'long_degrees',
|
||||
'long_minutes',
|
||||
]
|
||||
|
||||
float_keys = [
|
||||
'lat_seconds',
|
||||
'long_seconds',
|
||||
'altitude',
|
||||
'size',
|
||||
'precision_horz',
|
||||
'precision_vert',
|
||||
]
|
||||
|
||||
direction_keys = [
|
||||
'lat_direction',
|
||||
'long_direction',
|
||||
]
|
||||
|
||||
if not isinstance(data, (list, tuple)):
|
||||
data = (data,)
|
||||
reasons = []
|
||||
for value in data:
|
||||
for key in int_keys:
|
||||
try:
|
||||
int(value[key])
|
||||
if (
|
||||
(
|
||||
key == 'lat_degrees' and
|
||||
not 0 <= int(value[key]) <= 90
|
||||
) or (
|
||||
key == 'long_degrees' and
|
||||
not 0 <= int(value[key]) <= 180
|
||||
) or (
|
||||
key in ['lat_minutes', 'long_minutes'] and
|
||||
not 0 <= int(value[key]) <= 59
|
||||
)
|
||||
):
|
||||
reasons.append('invalid value for {} "{}"'
|
||||
.format(key, value[key]))
|
||||
except KeyError:
|
||||
reasons.append('missing {}'.format(key))
|
||||
except ValueError:
|
||||
reasons.append('invalid {} "{}"'
|
||||
.format(key, value[key]))
|
||||
|
||||
for key in float_keys:
|
||||
try:
|
||||
float(value[key])
|
||||
if (
|
||||
(
|
||||
key in ['lat_seconds', 'long_seconds'] and
|
||||
not 0 <= float(value[key]) <= 59.999
|
||||
) or (
|
||||
key == 'altitude' and
|
||||
not -100000.00 <= float(value[key]) <= 42849672.95
|
||||
) or (
|
||||
key in ['size',
|
||||
'precision_horz',
|
||||
'precision_vert'] and
|
||||
not 0 <= float(value[key]) <= 90000000.00
|
||||
)
|
||||
):
|
||||
reasons.append('invalid value for {} "{}"'
|
||||
.format(key, value[key]))
|
||||
except KeyError:
|
||||
reasons.append('missing {}'.format(key))
|
||||
except ValueError:
|
||||
reasons.append('invalid {} "{}"'
|
||||
.format(key, value[key]))
|
||||
|
||||
for key in direction_keys:
|
||||
try:
|
||||
str(value[key])
|
||||
if (
|
||||
key == 'lat_direction' and
|
||||
value[key] not in ['N', 'S']
|
||||
):
|
||||
reasons.append('invalid direction for {} "{}"'
|
||||
.format(key, value[key]))
|
||||
if (
|
||||
key == 'long_direction' and
|
||||
value[key] not in ['E', 'W']
|
||||
):
|
||||
reasons.append('invalid direction for {} "{}"'
|
||||
.format(key, value[key]))
|
||||
except KeyError:
|
||||
reasons.append('missing {}'.format(key))
|
||||
return reasons
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return [LocValue(v) for v in values]
|
||||
|
||||
def __init__(self, value):
|
||||
self.lat_degrees = int(value['lat_degrees'])
|
||||
self.lat_minutes = int(value['lat_minutes'])
|
||||
self.lat_seconds = float(value['lat_seconds'])
|
||||
self.lat_direction = value['lat_direction'].upper()
|
||||
self.long_degrees = int(value['long_degrees'])
|
||||
self.long_minutes = int(value['long_minutes'])
|
||||
self.long_seconds = float(value['long_seconds'])
|
||||
self.long_direction = value['long_direction'].upper()
|
||||
self.altitude = float(value['altitude'])
|
||||
self.size = float(value['size'])
|
||||
self.precision_horz = float(value['precision_horz'])
|
||||
self.precision_vert = float(value['precision_vert'])
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return {
|
||||
'lat_degrees': self.lat_degrees,
|
||||
'lat_minutes': self.lat_minutes,
|
||||
'lat_seconds': self.lat_seconds,
|
||||
'lat_direction': self.lat_direction,
|
||||
'long_degrees': self.long_degrees,
|
||||
'long_minutes': self.long_minutes,
|
||||
'long_seconds': self.long_seconds,
|
||||
'long_direction': self.long_direction,
|
||||
'altitude': self.altitude,
|
||||
'size': self.size,
|
||||
'precision_horz': self.precision_horz,
|
||||
'precision_vert': self.precision_vert,
|
||||
}
|
||||
|
||||
def __hash__(self):
|
||||
return hash((
|
||||
self.lat_degrees,
|
||||
self.lat_minutes,
|
||||
self.lat_seconds,
|
||||
self.lat_direction,
|
||||
self.long_degrees,
|
||||
self.long_minutes,
|
||||
self.long_seconds,
|
||||
self.long_direction,
|
||||
self.altitude,
|
||||
self.size,
|
||||
self.precision_horz,
|
||||
self.precision_vert,
|
||||
))
|
||||
|
||||
def _equality_tuple(self):
|
||||
return (
|
||||
self.lat_degrees,
|
||||
self.lat_minutes,
|
||||
self.lat_seconds,
|
||||
self.lat_direction,
|
||||
self.long_degrees,
|
||||
self.long_minutes,
|
||||
self.long_seconds,
|
||||
self.long_direction,
|
||||
self.altitude,
|
||||
self.size,
|
||||
self.precision_horz,
|
||||
self.precision_vert,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
loc_format = "'{0} {1} {2:.3f} {3} " + \
|
||||
"{4} {5} {6:.3f} {7} " + \
|
||||
"{8:.2f}m {9:.2f}m {10:.2f}m {11:.2f}m'"
|
||||
return loc_format.format(
|
||||
self.lat_degrees,
|
||||
self.lat_minutes,
|
||||
self.lat_seconds,
|
||||
self.lat_direction,
|
||||
self.long_degrees,
|
||||
self.long_minutes,
|
||||
self.long_seconds,
|
||||
self.long_direction,
|
||||
self.altitude,
|
||||
self.size,
|
||||
self.precision_horz,
|
||||
self.precision_vert,
|
||||
)
|
||||
|
||||
|
||||
class LocRecord(_ValuesMixin, Record):
|
||||
_type = 'LOC'
|
||||
_value_type = LocValue
|
||||
|
||||
|
||||
class MxValue(EqualityTupleMixin):
|
||||
|
||||
@classmethod
|
||||
|
||||
+31
-9
@@ -26,8 +26,8 @@ class AxfrBaseSource(BaseSource):
|
||||
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR', 'SPF',
|
||||
'SRV', 'TXT'))
|
||||
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'LOC', 'MX', 'NS', 'PTR',
|
||||
'SPF', 'SRV', 'TXT'))
|
||||
|
||||
def __init__(self, id):
|
||||
super(AxfrBaseSource, self).__init__(id)
|
||||
@@ -58,6 +58,33 @@ class AxfrBaseSource(BaseSource):
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_LOC(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
lat_degrees, lat_minutes, lat_seconds, lat_direction, \
|
||||
long_degrees, long_minutes, long_seconds, long_direction, \
|
||||
altitude, size, precision_horz, precision_vert = \
|
||||
record['value'].replace('m', '').split(' ', 11)
|
||||
values.append({
|
||||
'lat_degrees': lat_degrees,
|
||||
'lat_minutes': lat_minutes,
|
||||
'lat_seconds': lat_seconds,
|
||||
'lat_direction': lat_direction,
|
||||
'long_degrees': long_degrees,
|
||||
'long_minutes': long_minutes,
|
||||
'long_seconds': long_seconds,
|
||||
'long_direction': long_direction,
|
||||
'altitude': altitude,
|
||||
'size': size,
|
||||
'precision_horz': precision_horz,
|
||||
'precision_vert': precision_vert,
|
||||
})
|
||||
return {
|
||||
'ttl': records[0]['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
for record in records:
|
||||
@@ -216,7 +243,7 @@ class ZoneFileSource(AxfrBaseSource):
|
||||
# (optional, default true)
|
||||
check_origin: false
|
||||
'''
|
||||
def __init__(self, id, directory, file_extension=None, check_origin=True):
|
||||
def __init__(self, id, directory, file_extension='.', check_origin=True):
|
||||
self.log = logging.getLogger('ZoneFileSource[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, directory=%s, file_extension=%s, '
|
||||
'check_origin=%s', id,
|
||||
@@ -229,12 +256,7 @@ class ZoneFileSource(AxfrBaseSource):
|
||||
self._zone_records = {}
|
||||
|
||||
def _load_zone_file(self, zone_name):
|
||||
|
||||
zone_filename = zone_name
|
||||
if self.file_extension:
|
||||
zone_filename = '{}{}'.format(zone_name,
|
||||
self.file_extension.lstrip('.'))
|
||||
|
||||
zone_filename = '{}{}'.format(zone_name[:-1], self.file_extension)
|
||||
zonefiles = listdir(self.directory)
|
||||
if zone_filename in zonefiles:
|
||||
try:
|
||||
|
||||
@@ -5,4 +5,4 @@ pycodestyle==2.6.0
|
||||
pyflakes==2.2.0
|
||||
readme_renderer[md]==26.0
|
||||
requests_mock
|
||||
twine==3.2.0
|
||||
twine==3.2.0; python_version >= '3.2'
|
||||
|
||||
@@ -4,14 +4,17 @@ providers:
|
||||
in:
|
||||
class: octodns.provider.yaml.SplitYamlProvider
|
||||
directory: tests/config/split
|
||||
extension: .tst
|
||||
dump:
|
||||
class: octodns.provider.yaml.SplitYamlProvider
|
||||
directory: env/YAML_TMP_DIR
|
||||
extension: .tst
|
||||
# This is sort of ugly, but it shouldn't hurt anything. It'll just write out
|
||||
# the target file twice where it and dump are both used
|
||||
dump2:
|
||||
class: octodns.provider.yaml.SplitYamlProvider
|
||||
directory: env/YAML_TMP_DIR
|
||||
extension: .tst
|
||||
simple:
|
||||
class: helpers.SimpleProvider
|
||||
geo:
|
||||
|
||||
@@ -36,6 +36,22 @@
|
||||
- flags: 0
|
||||
tag: issue
|
||||
value: ca.unit.tests
|
||||
_imap._tcp:
|
||||
ttl: 600
|
||||
type: SRV
|
||||
values:
|
||||
- port: 0
|
||||
priority: 0
|
||||
target: .
|
||||
weight: 0
|
||||
_pop3._tcp:
|
||||
ttl: 600
|
||||
type: SRV
|
||||
values:
|
||||
- port: 0
|
||||
priority: 0
|
||||
target: .
|
||||
weight: 0
|
||||
_srv._tcp:
|
||||
ttl: 600
|
||||
type: SRV
|
||||
@@ -77,6 +93,34 @@ included:
|
||||
- test
|
||||
type: CNAME
|
||||
value: unit.tests.
|
||||
loc:
|
||||
ttl: 300
|
||||
type: LOC
|
||||
values:
|
||||
- altitude: 20
|
||||
lat_degrees: 31
|
||||
lat_direction: S
|
||||
lat_minutes: 58
|
||||
lat_seconds: 52.1
|
||||
long_degrees: 115
|
||||
long_direction: E
|
||||
long_minutes: 49
|
||||
long_seconds: 11.7
|
||||
precision_horz: 10
|
||||
precision_vert: 2
|
||||
size: 10
|
||||
- altitude: 20
|
||||
lat_degrees: 53
|
||||
lat_direction: N
|
||||
lat_minutes: 13
|
||||
lat_seconds: 10
|
||||
long_degrees: 2
|
||||
long_direction: W
|
||||
long_minutes: 18
|
||||
long_seconds: 26
|
||||
precision_horz: 1000
|
||||
precision_vert: 2
|
||||
size: 10
|
||||
mx:
|
||||
ttl: 300
|
||||
type: MX
|
||||
|
||||
+16
-16
@@ -177,15 +177,15 @@
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997656",
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.unit.tests",
|
||||
"name": "_imap._tcp.unit.tests",
|
||||
"data": {
|
||||
"service": "_srv",
|
||||
"service": "_imap",
|
||||
"proto": "_tcp",
|
||||
"name": "unit.tests",
|
||||
"priority": 12,
|
||||
"weight": 20,
|
||||
"port": 30,
|
||||
"target": "foo-2.unit.tests"
|
||||
"priority": 0,
|
||||
"weight": 0,
|
||||
"port": 0,
|
||||
"target": "."
|
||||
},
|
||||
"proxiable": true,
|
||||
"proxied": false,
|
||||
@@ -202,15 +202,15 @@
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997656",
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.unit.tests",
|
||||
"name": "_pop3._tcp.unit.tests",
|
||||
"data": {
|
||||
"service": "_srv",
|
||||
"proto": "_tcp",
|
||||
"service": "_imap",
|
||||
"proto": "_pop3",
|
||||
"name": "unit.tests",
|
||||
"priority": 10,
|
||||
"weight": 20,
|
||||
"port": 30,
|
||||
"target": "foo-1.unit.tests"
|
||||
"priority": 0,
|
||||
"weight": 0,
|
||||
"port": 0,
|
||||
"target": "."
|
||||
},
|
||||
"proxiable": true,
|
||||
"proxied": false,
|
||||
@@ -227,10 +227,10 @@
|
||||
],
|
||||
"result_info": {
|
||||
"page": 2,
|
||||
"per_page": 11,
|
||||
"total_pages": 2,
|
||||
"per_page": 10,
|
||||
"total_pages": 3,
|
||||
"count": 10,
|
||||
"total_count": 20
|
||||
"total_count": 24
|
||||
},
|
||||
"success": true,
|
||||
"errors": [],
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"result": [
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997656",
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.unit.tests",
|
||||
"data": {
|
||||
"service": "_srv",
|
||||
"proto": "_tcp",
|
||||
"name": "unit.tests",
|
||||
"priority": 12,
|
||||
"weight": 20,
|
||||
"port": 30,
|
||||
"target": "foo-2.unit.tests"
|
||||
},
|
||||
"proxiable": true,
|
||||
"proxied": false,
|
||||
"ttl": 600,
|
||||
"locked": false,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.940682Z",
|
||||
"created_on": "2017-03-11T18:01:43.940682Z",
|
||||
"meta": {
|
||||
"auto_added": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "fc12ab34cd5611334422ab3322997656",
|
||||
"type": "SRV",
|
||||
"name": "_srv._tcp.unit.tests",
|
||||
"data": {
|
||||
"service": "_srv",
|
||||
"proto": "_tcp",
|
||||
"name": "unit.tests",
|
||||
"priority": 10,
|
||||
"weight": 20,
|
||||
"port": 30,
|
||||
"target": "foo-1.unit.tests"
|
||||
},
|
||||
"proxiable": true,
|
||||
"proxied": false,
|
||||
"ttl": 600,
|
||||
"locked": false,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"modified_on": "2017-03-11T18:01:43.940682Z",
|
||||
"created_on": "2017-03-11T18:01:43.940682Z",
|
||||
"meta": {
|
||||
"auto_added": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "372e67954025e0ba6aaa6d586b9e0b59",
|
||||
"type": "LOC",
|
||||
"name": "loc.unit.tests",
|
||||
"content": "IN LOC 31 58 52.1 S 115 49 11.7 E 20m 10m 10m 2m",
|
||||
"proxiable": true,
|
||||
"proxied": false,
|
||||
"ttl": 300,
|
||||
"locked": false,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"created_on": "2020-01-28T05:20:00.12345Z",
|
||||
"modified_on": "2020-01-28T05:20:00.12345Z",
|
||||
"data": {
|
||||
"lat_degrees": 31,
|
||||
"lat_minutes": 58,
|
||||
"lat_seconds": 52.1,
|
||||
"lat_direction": "S",
|
||||
"long_degrees": 115,
|
||||
"long_minutes": 49,
|
||||
"long_seconds": 11.7,
|
||||
"long_direction": "E",
|
||||
"altitude": 20,
|
||||
"size": 10,
|
||||
"precision_horz": 10,
|
||||
"precision_vert": 2
|
||||
},
|
||||
"meta": {
|
||||
"auto_added": true,
|
||||
"source": "primary"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "372e67954025e0ba6aaa6d586b9e0b59",
|
||||
"type": "LOC",
|
||||
"name": "loc.unit.tests",
|
||||
"content": "IN LOC 53 14 10 N 2 18 26 W 20m 10m 1000m 2m",
|
||||
"proxiable": true,
|
||||
"proxied": false,
|
||||
"ttl": 300,
|
||||
"locked": false,
|
||||
"zone_id": "ff12ab34cd5611334422ab3322997650",
|
||||
"zone_name": "unit.tests",
|
||||
"created_on": "2020-01-28T05:20:00.12345Z",
|
||||
"modified_on": "2020-01-28T05:20:00.12345Z",
|
||||
"data": {
|
||||
"lat_degrees": 53,
|
||||
"lat_minutes": 13,
|
||||
"lat_seconds": 10,
|
||||
"lat_direction": "N",
|
||||
"long_degrees": 2,
|
||||
"long_minutes": 18,
|
||||
"long_seconds": 26,
|
||||
"long_direction": "W",
|
||||
"altitude": 20,
|
||||
"size": 10,
|
||||
"precision_horz": 1000,
|
||||
"precision_vert": 2
|
||||
},
|
||||
"meta": {
|
||||
"auto_added": true,
|
||||
"source": "primary"
|
||||
}
|
||||
}
|
||||
],
|
||||
"result_info": {
|
||||
"page": 3,
|
||||
"per_page": 10,
|
||||
"total_pages": 3,
|
||||
"count": 4,
|
||||
"total_count": 24
|
||||
},
|
||||
"success": true,
|
||||
"errors": [],
|
||||
"messages": []
|
||||
}
|
||||
+56
@@ -64,6 +64,62 @@
|
||||
"roundRobinFailover": [],
|
||||
"pools": [],
|
||||
"poolsDetail": []
|
||||
}, {
|
||||
"id": 1898527,
|
||||
"type": "SRV",
|
||||
"recordType": "srv",
|
||||
"name": "_imap._tcp",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149714387,
|
||||
"value": [{
|
||||
"value": ".",
|
||||
"priority": 0,
|
||||
"weight": 0,
|
||||
"port": 0,
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": ".",
|
||||
"priority": 0,
|
||||
"weight": 0,
|
||||
"port": 0,
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1898528,
|
||||
"type": "SRV",
|
||||
"recordType": "srv",
|
||||
"name": "_pop3._tcp",
|
||||
"recordOption": "roundRobin",
|
||||
"noAnswer": false,
|
||||
"note": "",
|
||||
"ttl": 600,
|
||||
"gtdRegion": 1,
|
||||
"parentId": 123123,
|
||||
"parent": "domain",
|
||||
"source": "Domain",
|
||||
"modifiedTs": 1565149714387,
|
||||
"value": [{
|
||||
"value": ".",
|
||||
"priority": 0,
|
||||
"weight": 0,
|
||||
"port": 0,
|
||||
"disableFlag": false
|
||||
}],
|
||||
"roundRobin": [{
|
||||
"value": ".",
|
||||
"priority": 0,
|
||||
"weight": 0,
|
||||
"port": 0,
|
||||
"disableFlag": false
|
||||
}]
|
||||
}, {
|
||||
"id": 1808527,
|
||||
"type": "SRV",
|
||||
|
||||
+22
@@ -76,6 +76,28 @@
|
||||
"weight": null,
|
||||
"flags": null,
|
||||
"tag": null
|
||||
}, {
|
||||
"id": 11189896,
|
||||
"type": "SRV",
|
||||
"name": "_imap._tcp",
|
||||
"data": ".",
|
||||
"priority": 0,
|
||||
"port": 0,
|
||||
"ttl": 600,
|
||||
"weight": 0,
|
||||
"flags": null,
|
||||
"tag": null
|
||||
}, {
|
||||
"id": 11189897,
|
||||
"type": "SRV",
|
||||
"name": "_pop3._tcp",
|
||||
"data": ".",
|
||||
"priority": 0,
|
||||
"port": 0,
|
||||
"ttl": 600,
|
||||
"weight": 0,
|
||||
"flags": null,
|
||||
"tag": null
|
||||
}],
|
||||
"links": {
|
||||
"pages": {
|
||||
|
||||
Vendored
+24
-2
@@ -264,10 +264,32 @@
|
||||
"rdata": "v=DKIM1;k=rsa;s=email;h=sha256;p=A\/kinda+of\/long\/string+with+numb3rs",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340025",
|
||||
"domain": "unit.tests",
|
||||
"host": "_imap._tcp",
|
||||
"ttl": "600",
|
||||
"prio": "0",
|
||||
"type": "SRV",
|
||||
"rdata": "0 0 0 .",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
},
|
||||
{
|
||||
"id": "12340026",
|
||||
"domain": "unit.tests",
|
||||
"host": "_pop3._tcp",
|
||||
"ttl": "600",
|
||||
"prio": "0",
|
||||
"type": "SRV",
|
||||
"rdata": "0 0 0 .",
|
||||
"geozone_id": "0",
|
||||
"last_mod": "2020-01-01 01:01:01"
|
||||
}
|
||||
],
|
||||
"count": 24,
|
||||
"total": 24,
|
||||
"count": 26,
|
||||
"total": 26,
|
||||
"start": 0,
|
||||
"max": 1000,
|
||||
"status": 200
|
||||
|
||||
Vendored
+18
-2
@@ -9,6 +9,22 @@
|
||||
"name": "_srv._tcp.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"0 0 0 ."
|
||||
],
|
||||
"type": "SRV",
|
||||
"name": "_imap._tcp.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"0 0 0 ."
|
||||
],
|
||||
"type": "SRV",
|
||||
"name": "_pop3._tcp.unit.tests",
|
||||
"ttl": 600
|
||||
},
|
||||
{
|
||||
"rdata": [
|
||||
"2601:644:500:e210:62f8:1dff:feb8:947a"
|
||||
@@ -151,7 +167,7 @@
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"totalElements": 16,
|
||||
"totalElements": 18,
|
||||
"showAll": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+18
@@ -123,6 +123,24 @@
|
||||
"2.2.3.6"
|
||||
]
|
||||
},
|
||||
{
|
||||
"rrset_type": "SRV",
|
||||
"rrset_ttl": 600,
|
||||
"rrset_name": "_imap._tcp",
|
||||
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_imap._tcp/SRV",
|
||||
"rrset_values": [
|
||||
"0 0 0 ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"rrset_type": "SRV",
|
||||
"rrset_ttl": 600,
|
||||
"rrset_name": "_pop3._tcp",
|
||||
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_pop3._tcp/SRV",
|
||||
"rrset_values": [
|
||||
"0 0 0 ."
|
||||
]
|
||||
},
|
||||
{
|
||||
"rrset_type": "SRV",
|
||||
"rrset_ttl": 600,
|
||||
|
||||
Vendored
+2
@@ -5,6 +5,8 @@
|
||||
@ 3600 SSHFP 1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73
|
||||
@ 3600 SSHFP 1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49
|
||||
@ 3600 CAA 0 issue ca.unit.tests
|
||||
_imap._tcp 600 SRV 0 0 0 .
|
||||
_pop3._tcp 600 SRV 0 0 0 .
|
||||
_srv._tcp 600 SRV 10 20 30 foo-1.unit.tests.
|
||||
_srv._tcp 600 SRV 12 20 30 foo-2.unit.tests.
|
||||
aaaa 600 AAAA 2601:644:500:e210:62f8:1dff:feb8:947a
|
||||
|
||||
+40
@@ -32,6 +32,22 @@
|
||||
"ttl": 300,
|
||||
"type": "MX"
|
||||
},
|
||||
{
|
||||
"comments": [],
|
||||
"name": "loc.unit.tests.",
|
||||
"records": [
|
||||
{
|
||||
"content": "31 58 52.100 S 115 49 11.700 E 20.00m 10.00m 10.00m 2.00m",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"content": "53 13 10.000 N 2 18 26.000 W 20.00m 10.00m 1000.00m 2.00m",
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"ttl": 300,
|
||||
"type": "LOC"
|
||||
},
|
||||
{
|
||||
"comments": [],
|
||||
"name": "sub.unit.tests.",
|
||||
@@ -59,6 +75,30 @@
|
||||
"ttl": 300,
|
||||
"type": "A"
|
||||
},
|
||||
{
|
||||
"comments": [],
|
||||
"name": "_imap._tcp.unit.tests.",
|
||||
"records": [
|
||||
{
|
||||
"content": "0 0 0 .",
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"ttl": 600,
|
||||
"type": "SRV"
|
||||
},
|
||||
{
|
||||
"comments": [],
|
||||
"name": "_pop3._tcp.unit.tests.",
|
||||
"records": [
|
||||
{
|
||||
"content": "0 0 0 .",
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"ttl": 600,
|
||||
"type": "SRV"
|
||||
},
|
||||
{
|
||||
"comments": [],
|
||||
"name": "_srv._tcp.unit.tests.",
|
||||
|
||||
@@ -118,12 +118,12 @@ class TestManager(TestCase):
|
||||
environ['YAML_TMP_DIR'] = tmpdir.dirname
|
||||
tc = Manager(get_config_filename('simple.yaml')) \
|
||||
.sync(dry_run=False)
|
||||
self.assertEquals(22, tc)
|
||||
self.assertEquals(25, 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(16, tc)
|
||||
self.assertEquals(19, tc)
|
||||
|
||||
# the subzone, with 2 targets
|
||||
tc = Manager(get_config_filename('simple.yaml')) \
|
||||
@@ -138,18 +138,18 @@ class TestManager(TestCase):
|
||||
# Again with force
|
||||
tc = Manager(get_config_filename('simple.yaml')) \
|
||||
.sync(dry_run=False, force=True)
|
||||
self.assertEquals(22, tc)
|
||||
self.assertEquals(25, tc)
|
||||
|
||||
# Again with max_workers = 1
|
||||
tc = Manager(get_config_filename('simple.yaml'), max_workers=1) \
|
||||
.sync(dry_run=False, force=True)
|
||||
self.assertEquals(22, tc)
|
||||
self.assertEquals(25, tc)
|
||||
|
||||
# Include meta
|
||||
tc = Manager(get_config_filename('simple.yaml'), max_workers=1,
|
||||
include_meta=True) \
|
||||
.sync(dry_run=False, force=True)
|
||||
self.assertEquals(26, tc)
|
||||
self.assertEquals(29, tc)
|
||||
|
||||
def test_eligible_sources(self):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
@@ -215,13 +215,13 @@ class TestManager(TestCase):
|
||||
fh.write('---\n{}')
|
||||
|
||||
changes = manager.compare(['in'], ['dump'], 'unit.tests.')
|
||||
self.assertEquals(16, len(changes))
|
||||
self.assertEquals(19, len(changes))
|
||||
|
||||
# Compound sources with varying support
|
||||
changes = manager.compare(['in', 'nosshfp'],
|
||||
['dump'],
|
||||
'unit.tests.')
|
||||
self.assertEquals(15, len(changes))
|
||||
self.assertEquals(18, len(changes))
|
||||
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
manager.compare(['nope'], ['dump'], 'unit.tests.')
|
||||
|
||||
@@ -177,10 +177,14 @@ class TestCloudflareProvider(TestCase):
|
||||
'page-2.json') as fh:
|
||||
mock.get('{}?page=2'.format(base), status_code=200,
|
||||
text=fh.read())
|
||||
with open('tests/fixtures/cloudflare-dns_records-'
|
||||
'page-3.json') as fh:
|
||||
mock.get('{}?page=3'.format(base), status_code=200,
|
||||
text=fh.read())
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(13, len(zone.records))
|
||||
self.assertEquals(16, len(zone.records))
|
||||
|
||||
changes = self.expected.changes(zone, provider)
|
||||
|
||||
@@ -189,7 +193,7 @@ class TestCloudflareProvider(TestCase):
|
||||
# re-populating the same zone/records comes out of cache, no calls
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(13, len(again.records))
|
||||
self.assertEquals(16, len(again.records))
|
||||
|
||||
def test_apply(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token', retry_period=0)
|
||||
@@ -203,12 +207,12 @@ class TestCloudflareProvider(TestCase):
|
||||
'id': 42,
|
||||
}
|
||||
}, # zone create
|
||||
] + [None] * 22 # individual record creates
|
||||
] + [None] * 25 # individual record creates
|
||||
|
||||
# non-existent zone, create everything
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(13, len(plan.changes))
|
||||
self.assertEquals(13, provider.apply(plan))
|
||||
self.assertEquals(16, len(plan.changes))
|
||||
self.assertEquals(16, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
@@ -234,7 +238,7 @@ class TestCloudflareProvider(TestCase):
|
||||
}),
|
||||
], True)
|
||||
# expected number of total calls
|
||||
self.assertEquals(23, provider._request.call_count)
|
||||
self.assertEquals(27, provider._request.call_count)
|
||||
|
||||
provider._request.reset_mock()
|
||||
|
||||
@@ -336,6 +340,10 @@ class TestCloudflareProvider(TestCase):
|
||||
self.assertTrue(plan.exists)
|
||||
# creates a the new value and then deletes all the old
|
||||
provider._request.assert_has_calls([
|
||||
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
|
||||
'dns_records/fc12ab34cd5611334422ab3322997653'),
|
||||
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
|
||||
'dns_records/fc12ab34cd5611334422ab3322997654'),
|
||||
call('PUT', '/zones/42/dns_records/'
|
||||
'fc12ab34cd5611334422ab3322997655', data={
|
||||
'content': '3.2.3.4',
|
||||
@@ -343,11 +351,7 @@ class TestCloudflareProvider(TestCase):
|
||||
'name': 'ttl.unit.tests',
|
||||
'proxied': False,
|
||||
'ttl': 300
|
||||
}),
|
||||
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
|
||||
'dns_records/fc12ab34cd5611334422ab3322997653'),
|
||||
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
|
||||
'dns_records/fc12ab34cd5611334422ab3322997654')
|
||||
})
|
||||
])
|
||||
|
||||
def test_update_add_swap(self):
|
||||
@@ -566,6 +570,52 @@ class TestCloudflareProvider(TestCase):
|
||||
'content': 'foo.bar.com.'
|
||||
}, list(ptr_record_contents)[0])
|
||||
|
||||
def test_loc(self):
|
||||
self.maxDiff = None
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
# LOC record
|
||||
loc_record = Record.new(zone, 'example', {
|
||||
'ttl': 300,
|
||||
'type': 'LOC',
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
loc_record_contents = provider._gen_data(loc_record)
|
||||
self.assertEquals({
|
||||
'name': 'example.unit.tests',
|
||||
'ttl': 300,
|
||||
'type': 'LOC',
|
||||
'data': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
}, list(loc_record_contents)[0])
|
||||
|
||||
def test_srv(self):
|
||||
provider = CloudflareProvider('test', 'email', 'token')
|
||||
|
||||
@@ -697,6 +747,23 @@ class TestCloudflareProvider(TestCase):
|
||||
},
|
||||
'type': 'SRV',
|
||||
}),
|
||||
('31 58 52.1 S 115 49 11.7 E 20 10 10 2', {
|
||||
'data': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
},
|
||||
'type': 'LOC',
|
||||
}),
|
||||
):
|
||||
self.assertEqual(expected, provider._gen_key(data))
|
||||
|
||||
|
||||
@@ -101,14 +101,14 @@ class TestConstellixProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(14, len(zone.records))
|
||||
self.assertEquals(16, len(zone.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
# 2nd populate makes no network calls/all from cache
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(14, len(again.records))
|
||||
self.assertEquals(16, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del provider._zone_records[zone.name]
|
||||
@@ -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) - 6
|
||||
n = len(self.expected.records) - 7
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
|
||||
@@ -163,7 +163,7 @@ class TestConstellixProvider(TestCase):
|
||||
}),
|
||||
])
|
||||
|
||||
self.assertEquals(17, provider._client._request.call_count)
|
||||
self.assertEquals(19, provider._client._request.call_count)
|
||||
|
||||
provider._client._request.reset_mock()
|
||||
|
||||
|
||||
@@ -83,14 +83,14 @@ class TestDigitalOceanProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(12, len(zone.records))
|
||||
self.assertEquals(14, len(zone.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
# 2nd populate makes no network calls/all from cache
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(12, len(again.records))
|
||||
self.assertEquals(14, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del provider._zone_records[zone.name]
|
||||
@@ -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) - 8
|
||||
n = len(self.expected.records) - 9
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
@@ -190,6 +190,24 @@ class TestDigitalOceanProvider(TestCase):
|
||||
'flags': 0, 'name': '@',
|
||||
'tag': 'issue',
|
||||
'ttl': 3600, 'type': 'CAA'}),
|
||||
call('POST', '/domains/unit.tests/records', data={
|
||||
'name': '_imap._tcp',
|
||||
'weight': 0,
|
||||
'data': '.',
|
||||
'priority': 0,
|
||||
'ttl': 600,
|
||||
'type': 'SRV',
|
||||
'port': 0
|
||||
}),
|
||||
call('POST', '/domains/unit.tests/records', data={
|
||||
'name': '_pop3._tcp',
|
||||
'weight': 0,
|
||||
'data': '.',
|
||||
'priority': 0,
|
||||
'ttl': 600,
|
||||
'type': 'SRV',
|
||||
'port': 0
|
||||
}),
|
||||
call('POST', '/domains/unit.tests/records', data={
|
||||
'name': '_srv._tcp',
|
||||
'weight': 20,
|
||||
@@ -200,7 +218,7 @@ class TestDigitalOceanProvider(TestCase):
|
||||
'port': 30
|
||||
}),
|
||||
])
|
||||
self.assertEquals(24, provider._client._request.call_count)
|
||||
self.assertEquals(26, provider._client._request.call_count)
|
||||
|
||||
provider._client._request.reset_mock()
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestDnsimpleProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded
|
||||
n = len(self.expected.records) - 4
|
||||
n = len(self.expected.records) - 7
|
||||
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) - 6
|
||||
n = len(self.expected.records) - 9
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
|
||||
|
||||
@@ -80,14 +80,14 @@ class TestEasyDNSProvider(TestCase):
|
||||
text=fh.read())
|
||||
|
||||
provider.populate(zone)
|
||||
self.assertEquals(13, len(zone.records))
|
||||
self.assertEquals(15, len(zone.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
# 2nd populate makes no network calls/all from cache
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(13, len(again.records))
|
||||
self.assertEquals(15, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del provider._zone_records[zone.name]
|
||||
@@ -374,12 +374,12 @@ class TestEasyDNSProvider(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))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
self.assertEquals(23, provider._client._request.call_count)
|
||||
self.assertEquals(25, provider._client._request.call_count)
|
||||
|
||||
provider._client._request.reset_mock()
|
||||
|
||||
|
||||
@@ -77,14 +77,14 @@ class TestEdgeDnsProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(16, len(zone.records))
|
||||
self.assertEquals(18, len(zone.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
# 2nd populate makes no network calls/all from cache
|
||||
again = Zone('unit.tests.', [])
|
||||
provider.populate(again)
|
||||
self.assertEquals(16, len(again.records))
|
||||
self.assertEquals(18, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del provider._zone_records[zone.name]
|
||||
@@ -105,7 +105,7 @@ class TestEdgeDnsProvider(TestCase):
|
||||
mock.delete(ANY, status_code=204)
|
||||
|
||||
changes = provider.apply(plan)
|
||||
self.assertEquals(29, changes)
|
||||
self.assertEquals(31, changes)
|
||||
|
||||
# Test against a zone that doesn't exist yet
|
||||
with requests_mock() as mock:
|
||||
@@ -118,7 +118,7 @@ class TestEdgeDnsProvider(TestCase):
|
||||
mock.delete(ANY, status_code=204)
|
||||
|
||||
changes = provider.apply(plan)
|
||||
self.assertEquals(14, changes)
|
||||
self.assertEquals(16, changes)
|
||||
|
||||
# Test against a zone that doesn't exist yet, but gid not provided
|
||||
with requests_mock() as mock:
|
||||
@@ -132,7 +132,7 @@ class TestEdgeDnsProvider(TestCase):
|
||||
mock.delete(ANY, status_code=204)
|
||||
|
||||
changes = provider.apply(plan)
|
||||
self.assertEquals(14, changes)
|
||||
self.assertEquals(16, changes)
|
||||
|
||||
# Test against a zone that doesn't exist, but cid not provided
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ class TestGandiProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(14, len(zone.records))
|
||||
self.assertEquals(16, len(zone.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
@@ -192,8 +192,8 @@ class TestGandiProvider(TestCase):
|
||||
]
|
||||
plan = provider.plan(self.expected)
|
||||
|
||||
# No root NS, no ignored, no excluded
|
||||
n = len(self.expected.records) - 4
|
||||
# No root NS, no ignored, no excluded, no LOC
|
||||
n = len(self.expected.records) - 5
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
@@ -284,6 +284,22 @@ class TestGandiProvider(TestCase):
|
||||
'12 20 30 foo-2.unit.tests.'
|
||||
]
|
||||
}),
|
||||
call('POST', '/livedns/domains/unit.tests/records', data={
|
||||
'rrset_name': '_pop3._tcp',
|
||||
'rrset_ttl': 600,
|
||||
'rrset_type': 'SRV',
|
||||
'rrset_values': [
|
||||
'0 0 0 .',
|
||||
]
|
||||
}),
|
||||
call('POST', '/livedns/domains/unit.tests/records', data={
|
||||
'rrset_name': '_imap._tcp',
|
||||
'rrset_ttl': 600,
|
||||
'rrset_type': 'SRV',
|
||||
'rrset_values': [
|
||||
'0 0 0 .',
|
||||
]
|
||||
}),
|
||||
call('POST', '/livedns/domains/unit.tests/records', data={
|
||||
'rrset_name': '@',
|
||||
'rrset_ttl': 3600,
|
||||
@@ -307,7 +323,7 @@ class TestGandiProvider(TestCase):
|
||||
})
|
||||
])
|
||||
# expected number of total calls
|
||||
self.assertEquals(17, provider._client._request.call_count)
|
||||
self.assertEquals(19, provider._client._request.call_count)
|
||||
|
||||
provider._client._request.reset_mock()
|
||||
|
||||
|
||||
@@ -378,8 +378,8 @@ class TestMythicBeastsProvider(TestCase):
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
|
||||
self.assertEquals(15, len(zone.records))
|
||||
self.assertEquals(15, len(self.expected.records))
|
||||
self.assertEquals(17, len(zone.records))
|
||||
self.assertEquals(17, len(self.expected.records))
|
||||
changes = self.expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
@@ -445,7 +445,7 @@ class TestMythicBeastsProvider(TestCase):
|
||||
if isinstance(c, Update)]))
|
||||
self.assertEquals(1, len([c for c in plan.changes
|
||||
if isinstance(c, Delete)]))
|
||||
self.assertEquals(14, len([c for c in plan.changes
|
||||
self.assertEquals(16, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
self.assertEquals(16, provider.apply(plan))
|
||||
self.assertEquals(18, provider.apply(plan))
|
||||
self.assertTrue(plan.exists)
|
||||
|
||||
@@ -186,7 +186,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
expected_n = len(expected.records) - 3
|
||||
self.assertEquals(16, expected_n)
|
||||
self.assertEquals(19, expected_n)
|
||||
|
||||
# No diffs == no changes
|
||||
with requests_mock() as mock:
|
||||
@@ -194,7 +194,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals(16, len(zone.records))
|
||||
self.assertEquals(19, len(zone.records))
|
||||
changes = expected.changes(zone, provider)
|
||||
self.assertEquals(0, len(changes))
|
||||
|
||||
@@ -291,7 +291,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
self.assertEquals(19, len(expected.records))
|
||||
self.assertEquals(22, len(expected.records))
|
||||
|
||||
# A small change to a single record
|
||||
with requests_mock() as mock:
|
||||
|
||||
@@ -222,7 +222,7 @@ N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
plan = provider.plan(_expected)
|
||||
|
||||
self.assertEqual(12, plan.change_counts['Create'])
|
||||
self.assertEqual(14, plan.change_counts['Create'])
|
||||
self.assertEqual(0, plan.change_counts['Update'])
|
||||
self.assertEqual(0, plan.change_counts['Delete'])
|
||||
|
||||
@@ -235,7 +235,7 @@ N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
|
||||
provider = TransipProvider('test', 'unittest', self.bogus_key)
|
||||
provider._client = MockDomainService('unittest', self.bogus_key)
|
||||
plan = provider.plan(_expected)
|
||||
self.assertEqual(12, len(plan.changes))
|
||||
self.assertEqual(14, len(plan.changes))
|
||||
changes = provider.apply(plan)
|
||||
self.assertEqual(changes, len(plan.changes))
|
||||
|
||||
|
||||
@@ -285,12 +285,12 @@ class TestUltraProvider(TestCase):
|
||||
provider._request.side_effect = [
|
||||
UltraNoZonesExistException('No Zones'),
|
||||
None, # zone create
|
||||
] + [None] * 13 # individual record creates
|
||||
] + [None] * 15 # individual record creates
|
||||
|
||||
# non-existent zone, create everything
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(13, len(plan.changes))
|
||||
self.assertEquals(13, provider.apply(plan))
|
||||
self.assertEquals(15, len(plan.changes))
|
||||
self.assertEquals(15, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
@@ -320,7 +320,7 @@ class TestUltraProvider(TestCase):
|
||||
'p=A/kinda+of/long/string+with+numb3rs']}),
|
||||
], True)
|
||||
# expected number of total calls
|
||||
self.assertEquals(15, provider._request.call_count)
|
||||
self.assertEquals(17, provider._request.call_count)
|
||||
|
||||
# Create sample rrset payload to attempt to alter
|
||||
page1 = json_load(open('tests/fixtures/ultra-records-page-1.json'))
|
||||
|
||||
@@ -35,7 +35,7 @@ class TestYamlProvider(TestCase):
|
||||
|
||||
# without it we see everything
|
||||
source.populate(zone)
|
||||
self.assertEquals(19, len(zone.records))
|
||||
self.assertEquals(22, len(zone.records))
|
||||
|
||||
source.populate(dynamic_zone)
|
||||
self.assertEquals(5, len(dynamic_zone.records))
|
||||
@@ -58,12 +58,12 @@ class TestYamlProvider(TestCase):
|
||||
|
||||
# We add everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(16, len([c for c in plan.changes
|
||||
self.assertEquals(19, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
self.assertFalse(isfile(yaml_file))
|
||||
|
||||
# Now actually do it
|
||||
self.assertEquals(16, target.apply(plan))
|
||||
self.assertEquals(19, 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(16, len([c for c in plan.changes
|
||||
self.assertEquals(19, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
|
||||
with open(yaml_file) as fh:
|
||||
@@ -106,7 +106,10 @@ class TestYamlProvider(TestCase):
|
||||
self.assertTrue('values' in data.pop('naptr'))
|
||||
self.assertTrue('values' in data.pop('sub'))
|
||||
self.assertTrue('values' in data.pop('txt'))
|
||||
self.assertTrue('values' in data.pop('loc'))
|
||||
# these are stored as singular 'value'
|
||||
self.assertTrue('value' in data.pop('_imap._tcp'))
|
||||
self.assertTrue('value' in data.pop('_pop3._tcp'))
|
||||
self.assertTrue('value' in data.pop('aaaa'))
|
||||
self.assertTrue('value' in data.pop('cname'))
|
||||
self.assertTrue('value' in data.pop('dname'))
|
||||
@@ -207,18 +210,20 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
def test_zone_directory(self):
|
||||
source = SplitYamlProvider(
|
||||
'test', join(dirname(__file__), 'config/split'))
|
||||
'test', join(dirname(__file__), 'config/split'),
|
||||
extension='.tst')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
|
||||
self.assertEqual(
|
||||
join(dirname(__file__), 'config/split/unit.tests.'),
|
||||
join(dirname(__file__), 'config/split', 'unit.tests.tst'),
|
||||
source._zone_directory(zone))
|
||||
|
||||
def test_apply_handles_existing_zone_directory(self):
|
||||
with TemporaryDirectory() as td:
|
||||
provider = SplitYamlProvider('test', join(td.dirname, 'config'))
|
||||
makedirs(join(td.dirname, 'config', 'does.exist.'))
|
||||
provider = SplitYamlProvider('test', join(td.dirname, 'config'),
|
||||
extension='.tst')
|
||||
makedirs(join(td.dirname, 'config', 'does.exist.tst'))
|
||||
|
||||
zone = Zone('does.exist.', [])
|
||||
self.assertTrue(isdir(provider._zone_directory(zone)))
|
||||
@@ -227,7 +232,8 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
def test_provider(self):
|
||||
source = SplitYamlProvider(
|
||||
'test', join(dirname(__file__), 'config/split'))
|
||||
'test', join(dirname(__file__), 'config/split'),
|
||||
extension='.tst')
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
dynamic_zone = Zone('dynamic.tests.', [])
|
||||
@@ -246,9 +252,10 @@ class TestSplitYamlProvider(TestCase):
|
||||
with TemporaryDirectory() as td:
|
||||
# Add some subdirs to make sure that it can create them
|
||||
directory = join(td.dirname, 'sub', 'dir')
|
||||
zone_dir = join(directory, 'unit.tests.')
|
||||
dynamic_zone_dir = join(directory, 'dynamic.tests.')
|
||||
target = SplitYamlProvider('test', directory)
|
||||
zone_dir = join(directory, 'unit.tests.tst')
|
||||
dynamic_zone_dir = join(directory, 'dynamic.tests.tst')
|
||||
target = SplitYamlProvider('test', directory,
|
||||
extension='.tst')
|
||||
|
||||
# We add everything
|
||||
plan = target.plan(zone)
|
||||
@@ -335,7 +342,8 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
def test_empty(self):
|
||||
source = SplitYamlProvider(
|
||||
'test', join(dirname(__file__), 'config/split'))
|
||||
'test', join(dirname(__file__), 'config/split'),
|
||||
extension='.tst')
|
||||
|
||||
zone = Zone('empty.', [])
|
||||
|
||||
@@ -345,7 +353,8 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
def test_unsorted(self):
|
||||
source = SplitYamlProvider(
|
||||
'test', join(dirname(__file__), 'config/split'))
|
||||
'test', join(dirname(__file__), 'config/split'),
|
||||
extension='.tst')
|
||||
|
||||
zone = Zone('unordered.', [])
|
||||
|
||||
@@ -356,14 +365,15 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
source = SplitYamlProvider(
|
||||
'test', join(dirname(__file__), 'config/split'),
|
||||
enforce_order=False)
|
||||
extension='.tst', enforce_order=False)
|
||||
# no exception
|
||||
source.populate(zone)
|
||||
self.assertEqual(2, len(zone.records))
|
||||
|
||||
def test_subzone_handling(self):
|
||||
source = SplitYamlProvider(
|
||||
'test', join(dirname(__file__), 'config/split'))
|
||||
'test', join(dirname(__file__), 'config/split'),
|
||||
extension='.tst')
|
||||
|
||||
# If we add `sub` as a sub-zone we'll reject `www.sub`
|
||||
zone = Zone('unit.tests.', ['sub'])
|
||||
|
||||
@@ -9,10 +9,11 @@ from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \
|
||||
CaaValue, CnameRecord, DnameRecord, Create, Delete, GeoValue, MxRecord, \
|
||||
MxValue, NaptrRecord, NaptrValue, NsRecord, PtrRecord, Record, \
|
||||
SshfpRecord, SshfpValue, SpfRecord, SrvRecord, SrvValue, TxtRecord, \
|
||||
Update, ValidationError, _Dynamic, _DynamicPool, _DynamicRule
|
||||
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
|
||||
from octodns.zone import Zone
|
||||
|
||||
from helpers import DynamicProvider, GeoProvider, SimpleProvider
|
||||
@@ -379,6 +380,98 @@ class TestRecord(TestCase):
|
||||
self.assertSingleValue(DnameRecord, 'target.foo.com.',
|
||||
'other.foo.com.')
|
||||
|
||||
def test_loc(self):
|
||||
a_values = [{
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}]
|
||||
a_data = {'ttl': 30, 'values': a_values}
|
||||
a = LocRecord(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]['lat_degrees'], a.values[0].lat_degrees)
|
||||
self.assertEquals(a_values[0]['lat_minutes'], a.values[0].lat_minutes)
|
||||
self.assertEquals(a_values[0]['lat_seconds'], a.values[0].lat_seconds)
|
||||
self.assertEquals(a_values[0]['lat_direction'],
|
||||
a.values[0].lat_direction)
|
||||
self.assertEquals(a_values[0]['long_degrees'],
|
||||
a.values[0].long_degrees)
|
||||
self.assertEquals(a_values[0]['long_minutes'],
|
||||
a.values[0].long_minutes)
|
||||
self.assertEquals(a_values[0]['long_seconds'],
|
||||
a.values[0].long_seconds)
|
||||
self.assertEquals(a_values[0]['long_direction'],
|
||||
a.values[0].long_direction)
|
||||
self.assertEquals(a_values[0]['altitude'], a.values[0].altitude)
|
||||
self.assertEquals(a_values[0]['size'], a.values[0].size)
|
||||
self.assertEquals(a_values[0]['precision_horz'],
|
||||
a.values[0].precision_horz)
|
||||
self.assertEquals(a_values[0]['precision_vert'],
|
||||
a.values[0].precision_vert)
|
||||
|
||||
b_value = {
|
||||
'lat_degrees': 32,
|
||||
'lat_minutes': 7,
|
||||
'lat_seconds': 19,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 116,
|
||||
'long_minutes': 2,
|
||||
'long_seconds': 25,
|
||||
'long_direction': 'E',
|
||||
'altitude': 10,
|
||||
'size': 1,
|
||||
'precision_horz': 10000,
|
||||
'precision_vert': 10,
|
||||
}
|
||||
b_data = {'ttl': 30, 'value': b_value}
|
||||
b = LocRecord(self.zone, 'b', b_data)
|
||||
self.assertEquals(b_value['lat_degrees'], b.values[0].lat_degrees)
|
||||
self.assertEquals(b_value['lat_minutes'], b.values[0].lat_minutes)
|
||||
self.assertEquals(b_value['lat_seconds'], b.values[0].lat_seconds)
|
||||
self.assertEquals(b_value['lat_direction'], b.values[0].lat_direction)
|
||||
self.assertEquals(b_value['long_degrees'], b.values[0].long_degrees)
|
||||
self.assertEquals(b_value['long_minutes'], b.values[0].long_minutes)
|
||||
self.assertEquals(b_value['long_seconds'], b.values[0].long_seconds)
|
||||
self.assertEquals(b_value['long_direction'],
|
||||
b.values[0].long_direction)
|
||||
self.assertEquals(b_value['altitude'], b.values[0].altitude)
|
||||
self.assertEquals(b_value['size'], b.values[0].size)
|
||||
self.assertEquals(b_value['precision_horz'],
|
||||
b.values[0].precision_horz)
|
||||
self.assertEquals(b_value['precision_vert'],
|
||||
b.values[0].precision_vert)
|
||||
self.assertEquals(b_data, b.data)
|
||||
|
||||
target = SimpleProvider()
|
||||
# No changes with self
|
||||
self.assertFalse(a.changes(a, target))
|
||||
# Diff in lat_direction causes change
|
||||
other = LocRecord(self.zone, 'a', {'ttl': 30, 'values': a_values})
|
||||
other.values[0].lat_direction = 'N'
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
# Diff in altitude causes change
|
||||
other.values[0].altitude = a.values[0].altitude
|
||||
other.values[0].altitude = -10
|
||||
change = a.changes(other, target)
|
||||
self.assertEqual(change.existing, a)
|
||||
self.assertEqual(change.new, other)
|
||||
|
||||
# __repr__ doesn't blow up
|
||||
a.__repr__()
|
||||
|
||||
def test_mx(self):
|
||||
a_values = [{
|
||||
'preference': 10,
|
||||
@@ -1127,6 +1220,93 @@ class TestRecord(TestCase):
|
||||
self.assertTrue(d >= d)
|
||||
self.assertTrue(d <= d)
|
||||
|
||||
def test_loc_value(self):
|
||||
a = LocValue({
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
})
|
||||
b = LocValue({
|
||||
'lat_degrees': 32,
|
||||
'lat_minutes': 7,
|
||||
'lat_seconds': 19,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 116,
|
||||
'long_minutes': 2,
|
||||
'long_seconds': 25,
|
||||
'long_direction': 'E',
|
||||
'altitude': 10,
|
||||
'size': 1,
|
||||
'precision_horz': 10000,
|
||||
'precision_vert': 10,
|
||||
})
|
||||
c = LocValue({
|
||||
'lat_degrees': 53,
|
||||
'lat_minutes': 14,
|
||||
'lat_seconds': 10,
|
||||
'lat_direction': 'N',
|
||||
'long_degrees': 2,
|
||||
'long_minutes': 18,
|
||||
'long_seconds': 26,
|
||||
'long_direction': 'W',
|
||||
'altitude': 10,
|
||||
'size': 1,
|
||||
'precision_horz': 1000,
|
||||
'precision_vert': 10,
|
||||
})
|
||||
|
||||
self.assertEqual(a, a)
|
||||
self.assertEqual(b, b)
|
||||
self.assertEqual(c, c)
|
||||
|
||||
self.assertNotEqual(a, b)
|
||||
self.assertNotEqual(a, c)
|
||||
self.assertNotEqual(b, a)
|
||||
self.assertNotEqual(b, c)
|
||||
self.assertNotEqual(c, a)
|
||||
self.assertNotEqual(c, b)
|
||||
|
||||
self.assertTrue(a < b)
|
||||
self.assertTrue(a < c)
|
||||
|
||||
self.assertTrue(b > a)
|
||||
self.assertTrue(b < c)
|
||||
|
||||
self.assertTrue(c > a)
|
||||
self.assertTrue(c > b)
|
||||
|
||||
self.assertTrue(a <= b)
|
||||
self.assertTrue(a <= c)
|
||||
self.assertTrue(a <= a)
|
||||
self.assertTrue(a >= a)
|
||||
|
||||
self.assertTrue(b >= a)
|
||||
self.assertTrue(b <= c)
|
||||
self.assertTrue(b >= b)
|
||||
self.assertTrue(b <= b)
|
||||
|
||||
self.assertTrue(c >= a)
|
||||
self.assertTrue(c >= b)
|
||||
self.assertTrue(c >= c)
|
||||
self.assertTrue(c <= c)
|
||||
|
||||
# Hash
|
||||
values = set()
|
||||
values.add(a)
|
||||
self.assertTrue(a in values)
|
||||
self.assertFalse(b in values)
|
||||
values.add(b)
|
||||
self.assertTrue(b in values)
|
||||
|
||||
def test_mx_value(self):
|
||||
a = MxValue({'preference': 0, 'priority': 'a', 'exchange': 'v',
|
||||
'value': '1'})
|
||||
@@ -1960,6 +2140,306 @@ class TestRecordValidation(TestCase):
|
||||
self.assertEquals(['DNAME value "foo.bar.com" missing trailing .'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
def test_LOC(self):
|
||||
# doesn't blow up
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
# missing int key
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['missing lat_degrees'], ctx.exception.reasons)
|
||||
|
||||
# missing float key
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['missing lat_seconds'], ctx.exception.reasons)
|
||||
|
||||
# missing text key
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['missing lat_direction'], ctx.exception.reasons)
|
||||
|
||||
# invalid direction
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'U',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid direction for lat_direction "U"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'N',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid direction for long_direction "N"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# invalid degrees
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 360,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid value for lat_degrees "360"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 'nope',
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid lat_degrees "nope"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# invalid minutes
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 60,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid value for lat_minutes "60"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# invalid seconds
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 60,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid value for lat_seconds "60"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 'nope',
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid lat_seconds "nope"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# invalid altitude
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': -666666,
|
||||
'size': 10,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid value for altitude "-666666"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
# invalid size
|
||||
with self.assertRaises(ValidationError) as ctx:
|
||||
Record.new(self.zone, '', {
|
||||
'type': 'LOC',
|
||||
'ttl': 600,
|
||||
'value': {
|
||||
'lat_degrees': 31,
|
||||
'lat_minutes': 58,
|
||||
'lat_seconds': 52.1,
|
||||
'lat_direction': 'S',
|
||||
'long_degrees': 115,
|
||||
'long_minutes': 49,
|
||||
'long_seconds': 11.7,
|
||||
'long_direction': 'E',
|
||||
'altitude': 20,
|
||||
'size': 99999999.99,
|
||||
'precision_horz': 10,
|
||||
'precision_vert': 2,
|
||||
}
|
||||
})
|
||||
|
||||
self.assertEquals(['invalid value for size "99999999.99"'],
|
||||
ctx.exception.reasons)
|
||||
|
||||
def test_MX(self):
|
||||
# doesn't blow up
|
||||
Record.new(self.zone, '', {
|
||||
|
||||
@@ -9,6 +9,8 @@ import dns.zone
|
||||
from dns.exception import DNSException
|
||||
|
||||
from mock import patch
|
||||
from os.path import exists
|
||||
from shutil import copyfile
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
@@ -21,7 +23,7 @@ from octodns.record import ValidationError
|
||||
class TestAxfrSource(TestCase):
|
||||
source = AxfrSource('test', 'localhost')
|
||||
|
||||
forward_zonefile = dns.zone.from_file('./tests/zones/unit.tests.',
|
||||
forward_zonefile = dns.zone.from_file('./tests/zones/unit.tests.tst',
|
||||
'unit.tests', relativize=False)
|
||||
|
||||
@patch('dns.zone.from_xfr')
|
||||
@@ -34,7 +36,7 @@ class TestAxfrSource(TestCase):
|
||||
]
|
||||
|
||||
self.source.populate(got)
|
||||
self.assertEquals(12, len(got.records))
|
||||
self.assertEquals(15, len(got.records))
|
||||
|
||||
with self.assertRaises(AxfrSourceZoneTransferFailed) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
@@ -44,25 +46,45 @@ class TestAxfrSource(TestCase):
|
||||
|
||||
|
||||
class TestZoneFileSource(TestCase):
|
||||
source = ZoneFileSource('test', './tests/zones')
|
||||
source = ZoneFileSource('test', './tests/zones', file_extension='.tst')
|
||||
|
||||
def test_zonefiles_with_extension(self):
|
||||
source = ZoneFileSource('test', './tests/zones', 'extension')
|
||||
source = ZoneFileSource('test', './tests/zones', '.extension')
|
||||
# Load zonefiles with a specified file extension
|
||||
valid = Zone('ext.unit.tests.', [])
|
||||
source.populate(valid)
|
||||
self.assertEquals(1, len(valid.records))
|
||||
|
||||
def test_zonefiles_without_extension(self):
|
||||
# Windows doesn't let files end with a `.` so we add a .tst to them in
|
||||
# the repo and then try and create the `.` version we need for the
|
||||
# default case (no extension.)
|
||||
copyfile('./tests/zones/unit.tests.tst', './tests/zones/unit.tests.')
|
||||
# Unfortunately copyfile silently works and create the file without
|
||||
# the `.` so we have to check to see if it did that
|
||||
if exists('./tests/zones/unit.tests'):
|
||||
# It did so we need to skip this test, that means windows won't
|
||||
# have full code coverage, but skipping the test is going out of
|
||||
# our way enough for a os-specific/oddball case.
|
||||
self.skipTest('Unable to create unit.tests. (ending with .) so '
|
||||
'skipping default filename testing.')
|
||||
|
||||
source = ZoneFileSource('test', './tests/zones')
|
||||
# Load zonefiles without a specified file extension
|
||||
valid = Zone('unit.tests.', [])
|
||||
source.populate(valid)
|
||||
self.assertEquals(15, len(valid.records))
|
||||
|
||||
def test_populate(self):
|
||||
# Valid zone file in directory
|
||||
valid = Zone('unit.tests.', [])
|
||||
self.source.populate(valid)
|
||||
self.assertEquals(12, len(valid.records))
|
||||
self.assertEquals(15, len(valid.records))
|
||||
|
||||
# 2nd populate does not read file again
|
||||
again = Zone('unit.tests.', [])
|
||||
self.source.populate(again)
|
||||
self.assertEquals(12, len(again.records))
|
||||
self.assertEquals(15, len(again.records))
|
||||
|
||||
# bust the cache
|
||||
del self.source._zone_records[valid.name]
|
||||
|
||||
@@ -20,6 +20,9 @@ caa 1800 IN CAA 0 iodef "mailto:admin@unit.tests"
|
||||
; SRV Records
|
||||
_srv._tcp 600 IN SRV 10 20 30 foo-1.unit.tests.
|
||||
_srv._tcp 600 IN SRV 10 20 30 foo-2.unit.tests.
|
||||
; NULL SRV Records
|
||||
_pop3._tcp 600 IN SRV 0 0 0 .
|
||||
_imap._tcp 600 IN SRV 0 0 0 .
|
||||
|
||||
; TXT Records
|
||||
txt 600 IN TXT "Bah bah black sheep"
|
||||
@@ -32,6 +35,10 @@ mx 300 IN MX 20 smtp-2.unit.tests.
|
||||
mx 300 IN MX 30 smtp-3.unit.tests.
|
||||
mx 300 IN MX 40 smtp-1.unit.tests.
|
||||
|
||||
; LOC Records
|
||||
loc 300 IN LOC 31 58 52.1 S 115 49 11.7 E 20m 10m 10m 2m
|
||||
loc 300 IN LOC 53 14 10 N 2 18 26 W 20m 10m 1000m 2m
|
||||
|
||||
; A Records
|
||||
@ 300 IN A 1.2.3.4
|
||||
@ 300 IN A 1.2.3.5
|
||||
Reference in New Issue
Block a user