mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge pull request #384 from github/python3-start
Start on supporting python3 using backwards compat bits for now
This commit is contained in:
+5
-2
@@ -1,6 +1,9 @@
|
||||
language: python
|
||||
python:
|
||||
- 2.7
|
||||
matrix:
|
||||
include:
|
||||
- python: 2.7
|
||||
- python: 3.7
|
||||
before_install: pip install --upgrade pip
|
||||
script: ./script/cibuild
|
||||
notifications:
|
||||
email:
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
## v0.9.9 - 2019-??-?? - Python 3.7 Support
|
||||
|
||||
* Extensive pass through the whole codebase to support Python 3
|
||||
* Tons of updates to replace `def __cmp__` with `__eq__` and friends to
|
||||
preserve custom equality and ordering behaviors that are essential to
|
||||
octoDNS's processes.
|
||||
* Quite a few objects required the addition of `__eq__` and friends so that
|
||||
they're sortable in Python 3 now that those things are more strict. A few
|
||||
places this required jumping through hoops of sorts. Thankfully our tests
|
||||
are pretty thorough and caught a lot of issues and hopefully the whole
|
||||
plan, review, apply process will backstop that.
|
||||
* Explicit ordering of changes by (name, type) to address inconsistent
|
||||
ordering for a number of providers that just convert changes into API
|
||||
calls as they come. Python 2 sets ordered consistently, Python 3 they do
|
||||
not. https://github.com/github/octodns/pull/384/commits/7958233fccf9ea22d95e2fd06c48d7d0a4529e26
|
||||
* Route53 `_mod_keyer` ordering wasn't 100% complete and thus unreliable and
|
||||
random in Python 3. This has been addressed and may result in value
|
||||
reordering on next plan, no actual changes in behavior should occur.
|
||||
* `incf.countryutils` (in pypi) was last released in 2009 is not python 3
|
||||
compatible (it's country data is also pretty stale.) `pycountry_convert`
|
||||
appears to have the functionality required to replace its usage so it has
|
||||
been removed as a dependency/requirement.
|
||||
* Bunch of additional unit tests and supporting config to exercise new code
|
||||
and verify things that were run into during the Python 3 work
|
||||
* lots of `six`ing of things
|
||||
|
||||
## v0.9.8 - 2019-09-30 - One with no changes b/c PyPi description problems
|
||||
|
||||
* No material changes
|
||||
|
||||
@@ -13,6 +13,8 @@ from logging import getLogger
|
||||
from sys import stdout
|
||||
import re
|
||||
|
||||
from six import text_type
|
||||
|
||||
from octodns.cmds.args import ArgumentParser
|
||||
from octodns.manager import Manager
|
||||
from octodns.zone import Zone
|
||||
@@ -65,7 +67,7 @@ def main():
|
||||
resolver = AsyncResolver(configure=False,
|
||||
num_workers=int(args.num_workers))
|
||||
if not ip_addr_re.match(server):
|
||||
server = unicode(query(server, 'A')[0])
|
||||
server = text_type(query(server, 'A')[0])
|
||||
log.info('server=%s', server)
|
||||
resolver.nameservers = [server]
|
||||
resolver.lifetime = int(args.timeout)
|
||||
@@ -81,12 +83,12 @@ def main():
|
||||
stdout.write(',')
|
||||
stdout.write(record._type)
|
||||
stdout.write(',')
|
||||
stdout.write(unicode(record.ttl))
|
||||
stdout.write(text_type(record.ttl))
|
||||
compare = {}
|
||||
for future in futures:
|
||||
stdout.write(',')
|
||||
try:
|
||||
answers = [unicode(r) for r in future.result()]
|
||||
answers = [text_type(r) for r in future.result()]
|
||||
except (NoAnswer, NoNameservers):
|
||||
answers = ['*no answer*']
|
||||
except NXDOMAIN:
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
|
||||
class EqualityTupleMixin(object):
|
||||
|
||||
def _equality_tuple(self):
|
||||
raise NotImplementedError('_equality_tuple method not implemented')
|
||||
|
||||
def __eq__(self, other):
|
||||
return self._equality_tuple() == other._equality_tuple()
|
||||
|
||||
def __ne__(self, other):
|
||||
return self._equality_tuple() != other._equality_tuple()
|
||||
|
||||
def __lt__(self, other):
|
||||
return self._equality_tuple() < other._equality_tuple()
|
||||
|
||||
def __le__(self, other):
|
||||
return self._equality_tuple() <= other._equality_tuple()
|
||||
|
||||
def __gt__(self, other):
|
||||
return self._equality_tuple() > other._equality_tuple()
|
||||
|
||||
def __ge__(self, other):
|
||||
return self._equality_tuple() >= other._equality_tuple()
|
||||
+51
-30
@@ -69,6 +69,10 @@ class MainThreadExecutor(object):
|
||||
return MakeThreadFuture(func, args, kwargs)
|
||||
|
||||
|
||||
class ManagerException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Manager(object):
|
||||
log = logging.getLogger('Manager')
|
||||
|
||||
@@ -105,16 +109,16 @@ class Manager(object):
|
||||
_class = provider_config.pop('class')
|
||||
except KeyError:
|
||||
self.log.exception('Invalid provider class')
|
||||
raise Exception('Provider {} is missing class'
|
||||
.format(provider_name))
|
||||
raise ManagerException('Provider {} is missing class'
|
||||
.format(provider_name))
|
||||
_class = self._get_named_class('provider', _class)
|
||||
kwargs = self._build_kwargs(provider_config)
|
||||
try:
|
||||
self.providers[provider_name] = _class(provider_name, **kwargs)
|
||||
except TypeError:
|
||||
self.log.exception('Invalid provider config')
|
||||
raise Exception('Incorrect provider config for {}'
|
||||
.format(provider_name))
|
||||
raise ManagerException('Incorrect provider config for {}'
|
||||
.format(provider_name))
|
||||
|
||||
zone_tree = {}
|
||||
# sort by reversed strings so that parent zones always come first
|
||||
@@ -148,8 +152,8 @@ class Manager(object):
|
||||
_class = plan_output_config.pop('class')
|
||||
except KeyError:
|
||||
self.log.exception('Invalid plan_output class')
|
||||
raise Exception('plan_output {} is missing class'
|
||||
.format(plan_output_name))
|
||||
raise ManagerException('plan_output {} is missing class'
|
||||
.format(plan_output_name))
|
||||
_class = self._get_named_class('plan_output', _class)
|
||||
kwargs = self._build_kwargs(plan_output_config)
|
||||
try:
|
||||
@@ -157,8 +161,8 @@ class Manager(object):
|
||||
_class(plan_output_name, **kwargs)
|
||||
except TypeError:
|
||||
self.log.exception('Invalid plan_output config')
|
||||
raise Exception('Incorrect plan_output config for {}'
|
||||
.format(plan_output_name))
|
||||
raise ManagerException('Incorrect plan_output config for {}'
|
||||
.format(plan_output_name))
|
||||
|
||||
def _get_named_class(self, _type, _class):
|
||||
try:
|
||||
@@ -167,13 +171,15 @@ class Manager(object):
|
||||
except (ImportError, ValueError):
|
||||
self.log.exception('_get_{}_class: Unable to import '
|
||||
'module %s', _class)
|
||||
raise Exception('Unknown {} class: {}'.format(_type, _class))
|
||||
raise ManagerException('Unknown {} class: {}'
|
||||
.format(_type, _class))
|
||||
try:
|
||||
return getattr(module, class_name)
|
||||
except AttributeError:
|
||||
self.log.exception('_get_{}_class: Unable to get class %s '
|
||||
'from module %s', class_name, module)
|
||||
raise Exception('Unknown {} class: {}'.format(_type, _class))
|
||||
raise ManagerException('Unknown {} class: {}'
|
||||
.format(_type, _class))
|
||||
|
||||
def _build_kwargs(self, source):
|
||||
# Build up the arguments we need to pass to the provider
|
||||
@@ -186,9 +192,9 @@ class Manager(object):
|
||||
v = environ[env_var]
|
||||
except KeyError:
|
||||
self.log.exception('Invalid provider config')
|
||||
raise Exception('Incorrect provider config, '
|
||||
'missing env var {}'
|
||||
.format(env_var))
|
||||
raise ManagerException('Incorrect provider config, '
|
||||
'missing env var {}'
|
||||
.format(env_var))
|
||||
except AttributeError:
|
||||
pass
|
||||
kwargs[k] = v
|
||||
@@ -248,7 +254,7 @@ class Manager(object):
|
||||
|
||||
zones = self.config['zones'].items()
|
||||
if eligible_zones:
|
||||
zones = filter(lambda d: d[0] in eligible_zones, zones)
|
||||
zones = [z for z in zones if z[0] in eligible_zones]
|
||||
|
||||
futures = []
|
||||
for zone_name, config in zones:
|
||||
@@ -256,14 +262,16 @@ class Manager(object):
|
||||
try:
|
||||
sources = config['sources']
|
||||
except KeyError:
|
||||
raise Exception('Zone {} is missing sources'.format(zone_name))
|
||||
raise ManagerException('Zone {} is missing sources'
|
||||
.format(zone_name))
|
||||
|
||||
try:
|
||||
targets = config['targets']
|
||||
except KeyError:
|
||||
raise Exception('Zone {} is missing targets'.format(zone_name))
|
||||
raise ManagerException('Zone {} is missing targets'
|
||||
.format(zone_name))
|
||||
if eligible_targets:
|
||||
targets = filter(lambda d: d in eligible_targets, targets)
|
||||
targets = [t for t in targets if t in eligible_targets]
|
||||
|
||||
if not targets:
|
||||
# Don't bother planning (and more importantly populating) zones
|
||||
@@ -275,23 +283,29 @@ class Manager(object):
|
||||
self.log.info('sync: sources=%s -> targets=%s', sources, targets)
|
||||
|
||||
try:
|
||||
sources = [self.providers[source] for source in sources]
|
||||
# rather than using a list comprehension, we break this loop
|
||||
# out so that the `except` block below can reference the
|
||||
# `source`
|
||||
collected = []
|
||||
for source in sources:
|
||||
collected.append(self.providers[source])
|
||||
sources = collected
|
||||
except KeyError:
|
||||
raise Exception('Zone {}, unknown source: {}'.format(zone_name,
|
||||
source))
|
||||
raise ManagerException('Zone {}, unknown source: {}'
|
||||
.format(zone_name, source))
|
||||
|
||||
try:
|
||||
trgs = []
|
||||
for target in targets:
|
||||
trg = self.providers[target]
|
||||
if not isinstance(trg, BaseProvider):
|
||||
raise Exception('{} - "{}" does not support targeting'
|
||||
.format(trg, target))
|
||||
raise ManagerException('{} - "{}" does not support '
|
||||
'targeting'.format(trg, target))
|
||||
trgs.append(trg)
|
||||
targets = trgs
|
||||
except KeyError:
|
||||
raise Exception('Zone {}, unknown target: {}'.format(zone_name,
|
||||
target))
|
||||
raise ManagerException('Zone {}, unknown target: {}'
|
||||
.format(zone_name, target))
|
||||
|
||||
futures.append(self._executor.submit(self._populate_and_plan,
|
||||
zone_name, sources, targets))
|
||||
@@ -344,7 +358,7 @@ class Manager(object):
|
||||
a = [self.providers[source] for source in a]
|
||||
b = [self.providers[source] for source in b]
|
||||
except KeyError as e:
|
||||
raise Exception('Unknown source: {}'.format(e.args[0]))
|
||||
raise ManagerException('Unknown source: {}'.format(e.args[0]))
|
||||
|
||||
sub_zones = self.configured_sub_zones(zone)
|
||||
za = Zone(zone, sub_zones)
|
||||
@@ -370,7 +384,7 @@ class Manager(object):
|
||||
try:
|
||||
sources = [self.providers[s] for s in sources]
|
||||
except KeyError as e:
|
||||
raise Exception('Unknown source: {}'.format(e.args[0]))
|
||||
raise ManagerException('Unknown source: {}'.format(e.args[0]))
|
||||
|
||||
clz = YamlProvider
|
||||
if split:
|
||||
@@ -393,13 +407,20 @@ class Manager(object):
|
||||
try:
|
||||
sources = config['sources']
|
||||
except KeyError:
|
||||
raise Exception('Zone {} is missing sources'.format(zone_name))
|
||||
raise ManagerException('Zone {} is missing sources'
|
||||
.format(zone_name))
|
||||
|
||||
try:
|
||||
sources = [self.providers[source] for source in sources]
|
||||
# rather than using a list comprehension, we break this loop
|
||||
# out so that the `except` block below can reference the
|
||||
# `source`
|
||||
collected = []
|
||||
for source in sources:
|
||||
collected.append(self.providers[source])
|
||||
sources = collected
|
||||
except KeyError:
|
||||
raise Exception('Zone {}, unknown source: {}'.format(zone_name,
|
||||
source))
|
||||
raise ManagerException('Zone {}, unknown source: {}'
|
||||
.format(zone_name, source))
|
||||
|
||||
for source in sources:
|
||||
if isinstance(source, YamlProvider):
|
||||
|
||||
@@ -175,6 +175,10 @@ class _AzureRecord(object):
|
||||
|
||||
:type return: bool
|
||||
'''
|
||||
|
||||
def key_dict(d):
|
||||
return sum([hash('{}:{}'.format(k, v)) for k, v in d.items()])
|
||||
|
||||
def parse_dict(params):
|
||||
vals = []
|
||||
for char in params:
|
||||
@@ -185,7 +189,7 @@ class _AzureRecord(object):
|
||||
vals.append(record.__dict__)
|
||||
except:
|
||||
vals.append(list_records.__dict__)
|
||||
vals.sort()
|
||||
vals.sort(key=key_dict)
|
||||
return vals
|
||||
|
||||
return (self.resource_group == b.resource_group) & \
|
||||
@@ -373,13 +377,13 @@ class AzureProvider(BaseProvider):
|
||||
self._populate_zones()
|
||||
self._check_zone(zone_name)
|
||||
|
||||
_records = set()
|
||||
_records = []
|
||||
records = self._dns_client.record_sets.list_by_dns_zone
|
||||
if self._check_zone(zone_name):
|
||||
exists = True
|
||||
for azrecord in records(self._resource_group, zone_name):
|
||||
if _parse_azure_type(azrecord.type) in self.SUPPORTS:
|
||||
_records.add(azrecord)
|
||||
_records.append(azrecord)
|
||||
for azrecord in _records:
|
||||
record_name = azrecord.name if azrecord.name != '@' else ''
|
||||
typ = _parse_azure_type(azrecord.type)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from six import text_type
|
||||
|
||||
from ..source.base import BaseSource
|
||||
from ..zone import Zone
|
||||
from .plan import Plan
|
||||
@@ -58,7 +60,7 @@ class BaseProvider(BaseSource):
|
||||
|
||||
# allow the provider to filter out false positives
|
||||
before = len(changes)
|
||||
changes = filter(self._include_change, changes)
|
||||
changes = [c for c in changes if self._include_change(c)]
|
||||
after = len(changes)
|
||||
if before != after:
|
||||
self.log.info('plan: filtered out %s changes', before - after)
|
||||
@@ -68,7 +70,7 @@ class BaseProvider(BaseSource):
|
||||
changes=changes)
|
||||
if extra:
|
||||
self.log.info('plan: extra changes\n %s', '\n '
|
||||
.join([unicode(c) for c in extra]))
|
||||
.join([text_type(c) for c in extra]))
|
||||
changes += extra
|
||||
|
||||
if changes:
|
||||
|
||||
@@ -442,7 +442,7 @@ class CloudflareProvider(BaseProvider):
|
||||
# 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 = self._gen_data(r).next()
|
||||
data = next(self._gen_data(r))
|
||||
|
||||
# Record the record_id and data for this existing record
|
||||
key = self._gen_key(data)
|
||||
|
||||
@@ -9,6 +9,7 @@ from collections import defaultdict
|
||||
from requests import Session
|
||||
from base64 import b64encode
|
||||
from ipaddress import ip_address
|
||||
from six import string_types
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
@@ -122,7 +123,7 @@ class ConstellixClient(object):
|
||||
# change relative values to absolute
|
||||
value = record['value']
|
||||
if record['type'] in ['ALIAS', 'CNAME', 'MX', 'NS', 'SRV']:
|
||||
if isinstance(value, unicode):
|
||||
if isinstance(value, string_types):
|
||||
record['value'] = self._absolutize_value(value,
|
||||
zone_name)
|
||||
if isinstance(value, list):
|
||||
|
||||
@@ -903,10 +903,10 @@ class DynProvider(BaseProvider):
|
||||
# Sort the values for consistent ordering so that we can compare
|
||||
values = sorted(values, key=_dynamic_value_sort_key)
|
||||
# Ensure that weight is included and if not use the default
|
||||
values = map(lambda v: {
|
||||
values = [{
|
||||
'value': v['value'],
|
||||
'weight': v.get('weight', 1),
|
||||
}, values)
|
||||
} for v in values]
|
||||
|
||||
# Walk through our existing pools looking for a match we can use
|
||||
for pool in pools:
|
||||
|
||||
@@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
|
||||
from requests import Session
|
||||
from akamai.edgegrid import EdgeGridAuth
|
||||
from urlparse import urljoin
|
||||
from six.moves.urllib.parse import urljoin
|
||||
from collections import defaultdict
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
@@ -328,7 +328,7 @@ class MythicBeastsProvider(BaseProvider):
|
||||
|
||||
exists = True
|
||||
for line in resp.content.splitlines():
|
||||
match = MythicBeastsProvider.RE_POPLINE.match(line)
|
||||
match = MythicBeastsProvider.RE_POPLINE.match(line.decode("utf-8"))
|
||||
|
||||
if match is None:
|
||||
self.log.debug('failed to match line: %s', line)
|
||||
|
||||
@@ -10,9 +10,11 @@ from itertools import chain
|
||||
from collections import OrderedDict, defaultdict
|
||||
from ns1 import NS1
|
||||
from ns1.rest.errors import RateLimitException, ResourceException
|
||||
from incf.countryutils import transformations
|
||||
from pycountry_convert import country_alpha2_to_continent_code
|
||||
from time import sleep
|
||||
|
||||
from six import text_type
|
||||
|
||||
from ..record import Record
|
||||
from .base import BaseProvider
|
||||
|
||||
@@ -60,8 +62,7 @@ class Ns1Provider(BaseProvider):
|
||||
us_state = meta.get('us_state', [])
|
||||
ca_province = meta.get('ca_province', [])
|
||||
for cntry in country:
|
||||
cn = transformations.cc_to_cn(cntry)
|
||||
con = transformations.cn_to_ctca2(cn)
|
||||
con = country_alpha2_to_continent_code(cntry)
|
||||
key = '{}-{}'.format(con, cntry)
|
||||
geo[key].extend(answer['answer'])
|
||||
for state in us_state:
|
||||
@@ -76,9 +77,9 @@ class Ns1Provider(BaseProvider):
|
||||
else:
|
||||
values.extend(answer['answer'])
|
||||
codes.append([])
|
||||
values = [unicode(x) for x in values]
|
||||
values = [text_type(x) for x in values]
|
||||
geo = OrderedDict(
|
||||
{unicode(k): [unicode(x) for x in v] for k, v in geo.items()}
|
||||
{text_type(k): [text_type(x) for x in v] for k, v in geo.items()}
|
||||
)
|
||||
data['values'] = values
|
||||
data['geo'] = geo
|
||||
|
||||
@@ -9,6 +9,7 @@ import base64
|
||||
import binascii
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from six import text_type
|
||||
|
||||
import ovh
|
||||
from ovh import ResourceNotFoundError
|
||||
@@ -64,7 +65,7 @@ class OvhProvider(BaseProvider):
|
||||
records = self.get_records(zone_name=zone_name)
|
||||
exists = True
|
||||
except ResourceNotFoundError as e:
|
||||
if e.message != self.ZONE_NOT_FOUND_MESSAGE:
|
||||
if text_type(e) != self.ZONE_NOT_FOUND_MESSAGE:
|
||||
raise
|
||||
exists = False
|
||||
records = []
|
||||
@@ -325,7 +326,7 @@ class OvhProvider(BaseProvider):
|
||||
splitted = value.split('\\;')
|
||||
found_key = False
|
||||
for splitted_value in splitted:
|
||||
sub_split = map(lambda x: x.strip(), splitted_value.split("=", 1))
|
||||
sub_split = [x.strip() for x in splitted_value.split("=", 1)]
|
||||
if len(sub_split) < 2:
|
||||
return False
|
||||
key, value = sub_split[0], sub_split[1]
|
||||
@@ -343,7 +344,7 @@ class OvhProvider(BaseProvider):
|
||||
@staticmethod
|
||||
def _is_valid_dkim_key(key):
|
||||
try:
|
||||
base64.decodestring(key)
|
||||
base64.decodestring(bytearray(key, 'utf-8'))
|
||||
except binascii.Error:
|
||||
return False
|
||||
return True
|
||||
|
||||
+17
-12
@@ -5,10 +5,11 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from StringIO import StringIO
|
||||
from logging import DEBUG, ERROR, INFO, WARN, getLogger
|
||||
from sys import stdout
|
||||
|
||||
from six import StringIO, text_type
|
||||
|
||||
|
||||
class UnsafePlan(Exception):
|
||||
pass
|
||||
@@ -26,7 +27,11 @@ class Plan(object):
|
||||
delete_pcent_threshold=MAX_SAFE_DELETE_PCENT):
|
||||
self.existing = existing
|
||||
self.desired = desired
|
||||
self.changes = changes
|
||||
# Sort changes to ensure we always have a consistent ordering for
|
||||
# things that make assumptions about that. Many providers will do their
|
||||
# own ordering to ensure things happen in a way that makes sense to
|
||||
# them and/or is as safe as possible.
|
||||
self.changes = sorted(changes)
|
||||
self.exists = exists
|
||||
self.update_pcent_threshold = update_pcent_threshold
|
||||
self.delete_pcent_threshold = delete_pcent_threshold
|
||||
@@ -122,7 +127,7 @@ class PlanLogger(_PlanOutput):
|
||||
buf.write('* ')
|
||||
buf.write(target.id)
|
||||
buf.write(' (')
|
||||
buf.write(target)
|
||||
buf.write(text_type(target))
|
||||
buf.write(')\n* ')
|
||||
|
||||
if plan.exists is False:
|
||||
@@ -135,7 +140,7 @@ class PlanLogger(_PlanOutput):
|
||||
buf.write('\n* ')
|
||||
|
||||
buf.write('Summary: ')
|
||||
buf.write(plan)
|
||||
buf.write(text_type(plan))
|
||||
buf.write('\n')
|
||||
else:
|
||||
buf.write(hr)
|
||||
@@ -147,11 +152,11 @@ class PlanLogger(_PlanOutput):
|
||||
|
||||
def _value_stringifier(record, sep):
|
||||
try:
|
||||
values = [unicode(v) for v in record.values]
|
||||
values = [text_type(v) for v in record.values]
|
||||
except AttributeError:
|
||||
values = [record.value]
|
||||
for code, gv in sorted(getattr(record, 'geo', {}).items()):
|
||||
vs = ', '.join([unicode(v) for v in gv.values])
|
||||
vs = ', '.join([text_type(v) for v in gv.values])
|
||||
values.append('{}: {}'.format(code, vs))
|
||||
return sep.join(values)
|
||||
|
||||
@@ -193,7 +198,7 @@ class PlanMarkdown(_PlanOutput):
|
||||
fh.write(' | ')
|
||||
# TTL
|
||||
if existing:
|
||||
fh.write(unicode(existing.ttl))
|
||||
fh.write(text_type(existing.ttl))
|
||||
fh.write(' | ')
|
||||
fh.write(_value_stringifier(existing, '; '))
|
||||
fh.write(' | |\n')
|
||||
@@ -201,7 +206,7 @@ class PlanMarkdown(_PlanOutput):
|
||||
fh.write('| | | | ')
|
||||
|
||||
if new:
|
||||
fh.write(unicode(new.ttl))
|
||||
fh.write(text_type(new.ttl))
|
||||
fh.write(' | ')
|
||||
fh.write(_value_stringifier(new, '; '))
|
||||
fh.write(' | ')
|
||||
@@ -210,7 +215,7 @@ class PlanMarkdown(_PlanOutput):
|
||||
fh.write(' |\n')
|
||||
|
||||
fh.write('\nSummary: ')
|
||||
fh.write(unicode(plan))
|
||||
fh.write(text_type(plan))
|
||||
fh.write('\n\n')
|
||||
else:
|
||||
fh.write('## No changes were planned\n')
|
||||
@@ -261,7 +266,7 @@ class PlanHtml(_PlanOutput):
|
||||
# TTL
|
||||
if existing:
|
||||
fh.write(' <td>')
|
||||
fh.write(unicode(existing.ttl))
|
||||
fh.write(text_type(existing.ttl))
|
||||
fh.write('</td>\n <td>')
|
||||
fh.write(_value_stringifier(existing, '<br/>'))
|
||||
fh.write('</td>\n <td></td>\n </tr>\n')
|
||||
@@ -270,7 +275,7 @@ class PlanHtml(_PlanOutput):
|
||||
|
||||
if new:
|
||||
fh.write(' <td>')
|
||||
fh.write(unicode(new.ttl))
|
||||
fh.write(text_type(new.ttl))
|
||||
fh.write('</td>\n <td>')
|
||||
fh.write(_value_stringifier(new, '<br/>'))
|
||||
fh.write('</td>\n <td>')
|
||||
@@ -279,7 +284,7 @@ class PlanHtml(_PlanOutput):
|
||||
fh.write('</td>\n </tr>\n')
|
||||
|
||||
fh.write(' <tr>\n <td colspan=6>Summary: ')
|
||||
fh.write(unicode(plan))
|
||||
fh.write(text_type(plan))
|
||||
fh.write('</td>\n </tr>\n</table>\n')
|
||||
else:
|
||||
fh.write('<b>No changes were planned</b>')
|
||||
|
||||
@@ -7,13 +7,16 @@ from __future__ import absolute_import, division, print_function, \
|
||||
from requests import HTTPError, Session, post
|
||||
from collections import defaultdict
|
||||
import logging
|
||||
import string
|
||||
import time
|
||||
|
||||
from ..record import Record
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
def _value_keyer(v):
|
||||
return (v.get('type', ''), v['name'], v.get('data', ''))
|
||||
|
||||
|
||||
def add_trailing_dot(s):
|
||||
assert s
|
||||
assert s[-1] != '.'
|
||||
@@ -28,12 +31,12 @@ def remove_trailing_dot(s):
|
||||
|
||||
def escape_semicolon(s):
|
||||
assert s
|
||||
return string.replace(s, ';', '\\;')
|
||||
return s.replace(';', '\\;')
|
||||
|
||||
|
||||
def unescape_semicolon(s):
|
||||
assert s
|
||||
return string.replace(s, '\\;', ';')
|
||||
return s.replace('\\;', ';')
|
||||
|
||||
|
||||
class RackspaceProvider(BaseProvider):
|
||||
@@ -367,11 +370,9 @@ class RackspaceProvider(BaseProvider):
|
||||
self._delete('domains/{}/records?{}'.format(domain_id, params))
|
||||
|
||||
if updates:
|
||||
data = {"records": sorted(updates, key=lambda v: v['name'])}
|
||||
data = {"records": sorted(updates, key=_value_keyer)}
|
||||
self._put('domains/{}/records'.format(domain_id), data=data)
|
||||
|
||||
if creates:
|
||||
data = {"records": sorted(creates, key=lambda v: v['type'] +
|
||||
v['name'] +
|
||||
v.get('data', ''))}
|
||||
data = {"records": sorted(creates, key=_value_keyer)}
|
||||
self._post('domains/{}/records'.format(domain_id), data=data)
|
||||
|
||||
+21
-25
@@ -8,17 +8,19 @@ from __future__ import absolute_import, division, print_function, \
|
||||
from boto3 import client
|
||||
from botocore.config import Config
|
||||
from collections import defaultdict
|
||||
from incf.countryutils.transformations import cca_to_ctca2
|
||||
from ipaddress import AddressValueError, ip_address
|
||||
from pycountry_convert import country_alpha2_to_continent_code
|
||||
from uuid import uuid4
|
||||
import logging
|
||||
import re
|
||||
|
||||
from six import text_type
|
||||
|
||||
from ..equality import EqualityTupleMixin
|
||||
from ..record import Record, Update
|
||||
from ..record.geo import GeoCodes
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
octal_re = re.compile(r'\\(\d\d\d)')
|
||||
|
||||
|
||||
@@ -28,7 +30,7 @@ def _octal_replace(s):
|
||||
return octal_re.sub(lambda m: chr(int(m.group(1), 8)), s)
|
||||
|
||||
|
||||
class _Route53Record(object):
|
||||
class _Route53Record(EqualityTupleMixin):
|
||||
|
||||
@classmethod
|
||||
def _new_dynamic(cls, provider, record, hosted_zone_id, creating):
|
||||
@@ -147,7 +149,7 @@ class _Route53Record(object):
|
||||
}
|
||||
}
|
||||
|
||||
# NOTE: we're using __hash__ and __cmp__ methods that consider
|
||||
# NOTE: we're using __hash__ and ordering methods that consider
|
||||
# _Route53Records equivalent if they have the same class, fqdn, and _type.
|
||||
# Values are ignored. This is useful when computing diffs/changes.
|
||||
|
||||
@@ -155,17 +157,10 @@ class _Route53Record(object):
|
||||
'sub-classes should never use this method'
|
||||
return '{}:{}'.format(self.fqdn, self._type).__hash__()
|
||||
|
||||
def __cmp__(self, other):
|
||||
'''sub-classes should call up to this and return its value if non-zero.
|
||||
When it's zero they should compute their own __cmp__'''
|
||||
if self.__class__ != other.__class__:
|
||||
return cmp(self.__class__, other.__class__)
|
||||
elif self.fqdn != other.fqdn:
|
||||
return cmp(self.fqdn, other.fqdn)
|
||||
elif self._type != other._type:
|
||||
return cmp(self._type, other._type)
|
||||
# We're ignoring ttl, it's not an actual differentiator
|
||||
return 0
|
||||
def _equality_tuple(self):
|
||||
'''Sub-classes should call up to this and return its value and add
|
||||
any additional fields they need to hav considered.'''
|
||||
return (self.__class__.__name__, self.fqdn, self._type)
|
||||
|
||||
def __repr__(self):
|
||||
return '_Route53Record<{} {} {} {}>'.format(self.fqdn, self._type,
|
||||
@@ -506,11 +501,9 @@ class _Route53GeoRecord(_Route53Record):
|
||||
return '{}:{}:{}'.format(self.fqdn, self._type,
|
||||
self.geo.code).__hash__()
|
||||
|
||||
def __cmp__(self, other):
|
||||
ret = super(_Route53GeoRecord, self).__cmp__(other)
|
||||
if ret != 0:
|
||||
return ret
|
||||
return cmp(self.geo.code, other.geo.code)
|
||||
def _equality_tuple(self):
|
||||
return super(_Route53GeoRecord, self)._equality_tuple() + \
|
||||
(self.geo.code,)
|
||||
|
||||
def __repr__(self):
|
||||
return '_Route53GeoRecord<{} {} {} {} {}>'.format(self.fqdn,
|
||||
@@ -553,7 +546,10 @@ def _mod_keyer(mod):
|
||||
if rrset.get('GeoLocation', False):
|
||||
unique_id = rrset['SetIdentifier']
|
||||
else:
|
||||
unique_id = rrset['Name']
|
||||
if 'SetIdentifier' in rrset:
|
||||
unique_id = '{}-{}'.format(rrset['Name'], rrset['SetIdentifier'])
|
||||
else:
|
||||
unique_id = rrset['Name']
|
||||
|
||||
# Prioritise within the action_priority, ensuring targets come first.
|
||||
if rrset.get('GeoLocation', False):
|
||||
@@ -700,7 +696,7 @@ class Route53Provider(BaseProvider):
|
||||
if cc == '*':
|
||||
# This is the default
|
||||
return
|
||||
cn = cca_to_ctca2(cc)
|
||||
cn = country_alpha2_to_continent_code(cc)
|
||||
try:
|
||||
return '{}-{}-{}'.format(cn, cc, loc['SubdivisionCode'])
|
||||
except KeyError:
|
||||
@@ -1037,8 +1033,8 @@ class Route53Provider(BaseProvider):
|
||||
# ip_address's returned object for equivalence
|
||||
# E.g 2001:4860:4860::8842 -> 2001:4860:4860:0:0:0:0:8842
|
||||
if value:
|
||||
value = ip_address(unicode(value))
|
||||
config_ip_address = ip_address(unicode(config['IPAddress']))
|
||||
value = ip_address(text_type(value))
|
||||
config_ip_address = ip_address(text_type(config['IPAddress']))
|
||||
else:
|
||||
# No value so give this a None to match value's
|
||||
config_ip_address = None
|
||||
@@ -1059,7 +1055,7 @@ class Route53Provider(BaseProvider):
|
||||
fqdn, record._type, value)
|
||||
|
||||
try:
|
||||
ip_address(unicode(value))
|
||||
ip_address(text_type(value))
|
||||
# We're working with an IP, host is the Host header
|
||||
healthcheck_host = record.healthcheck_host
|
||||
except (AddressValueError, ValueError):
|
||||
|
||||
@@ -137,7 +137,7 @@ class TransipProvider(BaseProvider):
|
||||
try:
|
||||
self._client.get_info(plan.desired.name[:-1])
|
||||
except WebFault as e:
|
||||
self.log.warning('_apply: %s ', e.message)
|
||||
self.log.exception('_apply: get_info failed')
|
||||
raise e
|
||||
|
||||
_dns_entries = []
|
||||
|
||||
+52
-60
@@ -9,6 +9,9 @@ from ipaddress import IPv4Address, IPv6Address
|
||||
from logging import getLogger
|
||||
import re
|
||||
|
||||
from six import string_types, text_type
|
||||
|
||||
from ..equality import EqualityTupleMixin
|
||||
from .geo import GeoCodes
|
||||
|
||||
|
||||
@@ -23,6 +26,12 @@ class Change(object):
|
||||
'Returns new if we have one, existing otherwise'
|
||||
return self.new or self.existing
|
||||
|
||||
def __lt__(self, other):
|
||||
self_record = self.record
|
||||
other_record = other.record
|
||||
return ((self_record.name, self_record._type) <
|
||||
(other_record.name, other_record._type))
|
||||
|
||||
|
||||
class Create(Change):
|
||||
|
||||
@@ -68,7 +77,7 @@ class ValidationError(Exception):
|
||||
self.reasons = reasons
|
||||
|
||||
|
||||
class Record(object):
|
||||
class Record(EqualityTupleMixin):
|
||||
log = getLogger('Record')
|
||||
|
||||
@classmethod
|
||||
@@ -130,7 +139,7 @@ class Record(object):
|
||||
self.__class__.__name__, name)
|
||||
self.zone = zone
|
||||
# force everything lower-case just to be safe
|
||||
self.name = unicode(name).lower() if name else name
|
||||
self.name = text_type(name).lower() if name else name
|
||||
self.source = source
|
||||
self.ttl = int(data['ttl'])
|
||||
|
||||
@@ -194,24 +203,22 @@ class Record(object):
|
||||
if self.ttl != other.ttl:
|
||||
return Update(self, other)
|
||||
|
||||
# NOTE: we're using __hash__ and __cmp__ methods that consider Records
|
||||
# NOTE: we're using __hash__ and ordering methods that consider Records
|
||||
# equivalent if they have the same name & _type. Values are ignored. This
|
||||
# is useful when computing diffs/changes.
|
||||
|
||||
def __hash__(self):
|
||||
return '{}:{}'.format(self.name, self._type).__hash__()
|
||||
|
||||
def __cmp__(self, other):
|
||||
a = '{}:{}'.format(self.name, self._type)
|
||||
b = '{}:{}'.format(other.name, other._type)
|
||||
return cmp(a, b)
|
||||
def _equality_tuple(self):
|
||||
return (self.name, self._type)
|
||||
|
||||
def __repr__(self):
|
||||
# Make sure this is always overridden
|
||||
raise NotImplementedError('Abstract base class, __repr__ required')
|
||||
|
||||
|
||||
class GeoValue(object):
|
||||
class GeoValue(EqualityTupleMixin):
|
||||
geo_re = re.compile(r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)'
|
||||
r'(-(?P<subdivision_code>\w\w))?)?$')
|
||||
|
||||
@@ -238,11 +245,9 @@ class GeoValue(object):
|
||||
yield '-'.join(bits)
|
||||
bits.pop()
|
||||
|
||||
def __cmp__(self, other):
|
||||
return 0 if (self.continent_code == other.continent_code and
|
||||
self.country_code == other.country_code and
|
||||
self.subdivision_code == other.subdivision_code and
|
||||
self.values == other.values) else 1
|
||||
def _equality_tuple(self):
|
||||
return (self.continent_code, self.country_code, self.subdivision_code,
|
||||
self.values)
|
||||
|
||||
def __repr__(self):
|
||||
return "'Geo {} {} {} {}'".format(self.continent_code,
|
||||
@@ -268,7 +273,6 @@ class _ValuesMixin(object):
|
||||
values = data['values']
|
||||
except KeyError:
|
||||
values = [data['value']]
|
||||
# TODO: should we natsort values?
|
||||
self.values = sorted(self._value_type.process(values))
|
||||
|
||||
def changes(self, other, target):
|
||||
@@ -292,7 +296,7 @@ class _ValuesMixin(object):
|
||||
return ret
|
||||
|
||||
def __repr__(self):
|
||||
values = "['{}']".format("', '".join([unicode(v)
|
||||
values = "['{}']".format("', '".join([text_type(v)
|
||||
for v in self.values]))
|
||||
return '<{} {} {}, {}, {}>'.format(self.__class__.__name__,
|
||||
self._type, self.ttl,
|
||||
@@ -574,7 +578,7 @@ class _DynamicMixin(object):
|
||||
reasons.append('rule {} missing pool'.format(rule_num))
|
||||
continue
|
||||
|
||||
if not isinstance(pool, basestring):
|
||||
if not isinstance(pool, string_types):
|
||||
reasons.append('rule {} invalid pool "{}"'
|
||||
.format(rule_num, pool))
|
||||
elif pool not in pools:
|
||||
@@ -671,13 +675,13 @@ class _IpList(object):
|
||||
return ['missing value(s)']
|
||||
reasons = []
|
||||
for value in data:
|
||||
if value is '':
|
||||
if value == '':
|
||||
reasons.append('empty value')
|
||||
elif value is None:
|
||||
reasons.append('missing value(s)')
|
||||
else:
|
||||
try:
|
||||
cls._address_type(unicode(value))
|
||||
cls._address_type(text_type(value))
|
||||
except Exception:
|
||||
reasons.append('invalid {} address "{}"'
|
||||
.format(cls._address_name, value))
|
||||
@@ -685,7 +689,8 @@ class _IpList(object):
|
||||
|
||||
@classmethod
|
||||
def process(cls, values):
|
||||
return values
|
||||
# Translating None into '' so that the list will be sortable in python3
|
||||
return [v if v is not None else '' for v in values]
|
||||
|
||||
|
||||
class Ipv4List(_IpList):
|
||||
@@ -742,7 +747,7 @@ class AliasRecord(_ValueMixin, Record):
|
||||
_value_type = AliasValue
|
||||
|
||||
|
||||
class CaaValue(object):
|
||||
class CaaValue(EqualityTupleMixin):
|
||||
# https://tools.ietf.org/html/rfc6844#page-5
|
||||
|
||||
@classmethod
|
||||
@@ -781,12 +786,8 @@ class CaaValue(object):
|
||||
'value': self.value,
|
||||
}
|
||||
|
||||
def __cmp__(self, other):
|
||||
if self.flags == other.flags:
|
||||
if self.tag == other.tag:
|
||||
return cmp(self.value, other.value)
|
||||
return cmp(self.tag, other.tag)
|
||||
return cmp(self.flags, other.flags)
|
||||
def _equality_tuple(self):
|
||||
return (self.flags, self.tag, self.value)
|
||||
|
||||
def __repr__(self):
|
||||
return '{} {} "{}"'.format(self.flags, self.tag, self.value)
|
||||
@@ -810,7 +811,7 @@ class CnameRecord(_DynamicMixin, _ValueMixin, Record):
|
||||
return reasons
|
||||
|
||||
|
||||
class MxValue(object):
|
||||
class MxValue(EqualityTupleMixin):
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data, _type):
|
||||
@@ -863,10 +864,11 @@ class MxValue(object):
|
||||
'exchange': self.exchange,
|
||||
}
|
||||
|
||||
def __cmp__(self, other):
|
||||
if self.preference == other.preference:
|
||||
return cmp(self.exchange, other.exchange)
|
||||
return cmp(self.preference, other.preference)
|
||||
def __hash__(self):
|
||||
return hash((self.preference, self.exchange))
|
||||
|
||||
def _equality_tuple(self):
|
||||
return (self.preference, self.exchange)
|
||||
|
||||
def __repr__(self):
|
||||
return "'{} {}'".format(self.preference, self.exchange)
|
||||
@@ -877,7 +879,7 @@ class MxRecord(_ValuesMixin, Record):
|
||||
_value_type = MxValue
|
||||
|
||||
|
||||
class NaptrValue(object):
|
||||
class NaptrValue(EqualityTupleMixin):
|
||||
VALID_FLAGS = ('S', 'A', 'U', 'P')
|
||||
|
||||
@classmethod
|
||||
@@ -936,18 +938,12 @@ class NaptrValue(object):
|
||||
'replacement': self.replacement,
|
||||
}
|
||||
|
||||
def __cmp__(self, other):
|
||||
if self.order != other.order:
|
||||
return cmp(self.order, other.order)
|
||||
elif self.preference != other.preference:
|
||||
return cmp(self.preference, other.preference)
|
||||
elif self.flags != other.flags:
|
||||
return cmp(self.flags, other.flags)
|
||||
elif self.service != other.service:
|
||||
return cmp(self.service, other.service)
|
||||
elif self.regexp != other.regexp:
|
||||
return cmp(self.regexp, other.regexp)
|
||||
return cmp(self.replacement, other.replacement)
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
def _equality_tuple(self):
|
||||
return (self.order, self.preference, self.flags, self.service,
|
||||
self.regexp, self.replacement)
|
||||
|
||||
def __repr__(self):
|
||||
flags = self.flags if self.flags is not None else ''
|
||||
@@ -997,7 +993,7 @@ class PtrRecord(_ValueMixin, Record):
|
||||
_value_type = PtrValue
|
||||
|
||||
|
||||
class SshfpValue(object):
|
||||
class SshfpValue(EqualityTupleMixin):
|
||||
VALID_ALGORITHMS = (1, 2, 3, 4)
|
||||
VALID_FINGERPRINT_TYPES = (1, 2)
|
||||
|
||||
@@ -1048,12 +1044,11 @@ class SshfpValue(object):
|
||||
'fingerprint': self.fingerprint,
|
||||
}
|
||||
|
||||
def __cmp__(self, other):
|
||||
if self.algorithm != other.algorithm:
|
||||
return cmp(self.algorithm, other.algorithm)
|
||||
elif self.fingerprint_type != other.fingerprint_type:
|
||||
return cmp(self.fingerprint_type, other.fingerprint_type)
|
||||
return cmp(self.fingerprint, other.fingerprint)
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
def _equality_tuple(self):
|
||||
return (self.algorithm, self.fingerprint_type, self.fingerprint)
|
||||
|
||||
def __repr__(self):
|
||||
return "'{} {} {}'".format(self.algorithm, self.fingerprint_type,
|
||||
@@ -1114,7 +1109,7 @@ class SpfRecord(_ChunkedValuesMixin, Record):
|
||||
_value_type = _ChunkedValue
|
||||
|
||||
|
||||
class SrvValue(object):
|
||||
class SrvValue(EqualityTupleMixin):
|
||||
|
||||
@classmethod
|
||||
def validate(cls, data, _type):
|
||||
@@ -1169,14 +1164,11 @@ class SrvValue(object):
|
||||
'target': self.target,
|
||||
}
|
||||
|
||||
def __cmp__(self, other):
|
||||
if self.priority != other.priority:
|
||||
return cmp(self.priority, other.priority)
|
||||
elif self.weight != other.weight:
|
||||
return cmp(self.weight, other.weight)
|
||||
elif self.port != other.port:
|
||||
return cmp(self.port, other.port)
|
||||
return cmp(self.target, other.target)
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
def _equality_tuple(self):
|
||||
return (self.priority, self.weight, self.port, self.target)
|
||||
|
||||
def __repr__(self):
|
||||
return "'{} {} {} {}'".format(self.priority, self.weight, self.port,
|
||||
|
||||
@@ -15,6 +15,7 @@ from dns.exception import DNSException
|
||||
from collections import defaultdict
|
||||
from os import listdir
|
||||
from os.path import join
|
||||
from six import text_type
|
||||
import logging
|
||||
|
||||
from ..record import Record
|
||||
@@ -179,8 +180,7 @@ class ZoneFileSourceNotFound(ZoneFileSourceException):
|
||||
class ZoneFileSourceLoadFailure(ZoneFileSourceException):
|
||||
|
||||
def __init__(self, error):
|
||||
super(ZoneFileSourceLoadFailure, self).__init__(
|
||||
error.message)
|
||||
super(ZoneFileSourceLoadFailure, self).__init__(text_type(error))
|
||||
|
||||
|
||||
class ZoneFileSource(AxfrBaseSource):
|
||||
|
||||
@@ -67,7 +67,8 @@ class TinyDnsBaseSource(BaseSource):
|
||||
values = []
|
||||
|
||||
for record in records:
|
||||
new_value = record[0].decode('unicode-escape').replace(";", "\\;")
|
||||
new_value = record[0].encode('latin1').decode('unicode-escape') \
|
||||
.replace(";", "\\;")
|
||||
values.append(new_value)
|
||||
|
||||
try:
|
||||
@@ -252,7 +253,7 @@ class TinyDnsFileSource(TinyDnsBaseSource):
|
||||
# Ignore hidden files
|
||||
continue
|
||||
with open(join(self.directory, filename), 'r') as fh:
|
||||
lines += filter(lambda l: l, fh.read().split('\n'))
|
||||
lines += [l for l in fh.read().split('\n') if l]
|
||||
|
||||
self._cache = lines
|
||||
|
||||
|
||||
+1
-2
@@ -49,8 +49,7 @@ class SortingDumper(SafeDumper):
|
||||
'''
|
||||
|
||||
def _representer(self, data):
|
||||
data = data.items()
|
||||
data.sort(key=lambda d: _natsort_key(d[0]))
|
||||
data = sorted(data.items(), key=lambda d: _natsort_key(d[0]))
|
||||
return self.represent_mapping(self.DEFAULT_MAPPING_TAG, data)
|
||||
|
||||
|
||||
|
||||
+5
-3
@@ -9,6 +9,8 @@ from collections import defaultdict
|
||||
from logging import getLogger
|
||||
import re
|
||||
|
||||
from six import text_type
|
||||
|
||||
from .record import Create, Delete
|
||||
|
||||
|
||||
@@ -38,7 +40,7 @@ class Zone(object):
|
||||
raise Exception('Invalid zone name {}, missing ending dot'
|
||||
.format(name))
|
||||
# Force everything to lowercase just to be safe
|
||||
self.name = unicode(name).lower() if name else name
|
||||
self.name = text_type(name).lower() if name else name
|
||||
self.sub_zones = sub_zones
|
||||
# We're grouping by node, it allows us to efficiently search for
|
||||
# duplicates and detect when CNAMEs co-exist with other records
|
||||
@@ -82,8 +84,8 @@ class Zone(object):
|
||||
raise DuplicateRecordException('Duplicate record {}, type {}'
|
||||
.format(record.fqdn,
|
||||
record._type))
|
||||
elif not lenient and (((record._type == 'CNAME' and len(node) > 0) or
|
||||
('CNAME' in map(lambda r: r._type, node)))):
|
||||
elif not lenient and ((record._type == 'CNAME' and len(node) > 0) or
|
||||
('CNAME' in [r._type for r in node])):
|
||||
# We're adding a CNAME to existing records or adding to an existing
|
||||
# CNAME
|
||||
raise InvalidNodeException('Invalid state, CNAME at {} cannot '
|
||||
|
||||
@@ -2,8 +2,6 @@ coverage
|
||||
mock
|
||||
nose
|
||||
pycodestyle==2.4.0
|
||||
pycountry>=18.12.8
|
||||
pycountry_convert>=0.7.2
|
||||
pyflakes==1.6.0
|
||||
readme_renderer[md]==24.0
|
||||
requests_mock
|
||||
|
||||
+7
-6
@@ -1,25 +1,26 @@
|
||||
PyYaml==4.2b1
|
||||
azure-common==1.1.18
|
||||
azure-mgmt-dns==2.1.0
|
||||
azure-common==1.1.23
|
||||
azure-mgmt-dns==3.0.0
|
||||
boto3==1.7.5
|
||||
botocore==1.10.5
|
||||
dnspython==1.15.0
|
||||
docutils==0.14
|
||||
dyn==1.8.1
|
||||
edgegrid-python==1.1.1
|
||||
futures==3.2.0
|
||||
futures==3.2.0; python_version < '3.0'
|
||||
google-cloud-core==0.28.1
|
||||
google-cloud-dns==0.29.0
|
||||
incf.countryutils==1.0
|
||||
ipaddress==1.0.22
|
||||
jmespath==0.9.3
|
||||
msrestazure==0.6.0
|
||||
msrestazure==0.6.2
|
||||
natsort==5.5.0
|
||||
ns1-python==0.12.0
|
||||
ovh==0.4.8
|
||||
pycountry-convert==0.7.2
|
||||
pycountry==19.8.18
|
||||
python-dateutil==2.6.1
|
||||
requests==2.22.0
|
||||
s3transfer==0.1.13
|
||||
six==1.11.0
|
||||
setuptools==38.5.2
|
||||
six==1.12.0
|
||||
transip==2.0.0
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export GOOGLE_APPLICATION_CREDENTIALS=
|
||||
coverage run --branch --source=octodns --omit=octodns/cmds/* "$(command -v nosetests)" --with-xunit "$@"
|
||||
coverage html
|
||||
coverage xml
|
||||
coverage report
|
||||
coverage report --show-missing
|
||||
coverage report | grep ^TOTAL | grep -qv 100% && {
|
||||
echo "Incomplete code coverage" >&2
|
||||
exit 1
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from StringIO import StringIO
|
||||
try:
|
||||
from StringIO import StringIO
|
||||
except ImportError:
|
||||
from io import StringIO
|
||||
from os.path import dirname, join
|
||||
import octodns
|
||||
|
||||
@@ -65,10 +68,11 @@ setup(
|
||||
install_requires=[
|
||||
'PyYaml>=4.2b1',
|
||||
'dnspython>=1.15.0',
|
||||
'futures>=3.2.0',
|
||||
'incf.countryutils>=1.0',
|
||||
'futures>=3.2.0; python_version<"3.2"',
|
||||
'ipaddress>=1.0.22',
|
||||
'natsort>=5.5.0',
|
||||
'pycountry>=19.8.18',
|
||||
'pycountry-convert>=0.7.2',
|
||||
# botocore doesn't like >=2.7.0 for some reason
|
||||
'python-dateutil>=2.6.0,<2.7.0',
|
||||
'requests>=2.20.0'
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
providers:
|
||||
yaml:
|
||||
class: octodns.provider.yaml.YamlProvider
|
||||
directory: ./config
|
||||
simple_source:
|
||||
class: helpers.SimpleSource
|
||||
zones:
|
||||
missing.sources.:
|
||||
targets:
|
||||
- yaml
|
||||
missing.targets.:
|
||||
sources:
|
||||
- yaml
|
||||
unknown.source.:
|
||||
sources:
|
||||
- not-there
|
||||
targets:
|
||||
- yaml
|
||||
unknown.target.:
|
||||
sources:
|
||||
- yaml
|
||||
targets:
|
||||
- not-there-either
|
||||
not.targetable.:
|
||||
sources:
|
||||
- yaml
|
||||
targets:
|
||||
- simple_source
|
||||
@@ -5,24 +5,8 @@ providers:
|
||||
simple_source:
|
||||
class: helpers.SimpleSource
|
||||
zones:
|
||||
missing.sources.:
|
||||
targets:
|
||||
- yaml
|
||||
missing.targets.:
|
||||
sources:
|
||||
- yaml
|
||||
unknown.source.:
|
||||
sources:
|
||||
- not-there
|
||||
targets:
|
||||
- yaml
|
||||
unknown.target.:
|
||||
sources:
|
||||
- yaml
|
||||
targets:
|
||||
- not-there-either
|
||||
not.targetable.:
|
||||
sources:
|
||||
- yaml
|
||||
targets:
|
||||
- simple_source
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.equality import EqualityTupleMixin
|
||||
|
||||
|
||||
class TestEqualityTupleMixin(TestCase):
|
||||
|
||||
def test_basics(self):
|
||||
|
||||
class Simple(EqualityTupleMixin):
|
||||
|
||||
def __init__(self, a, b, c):
|
||||
self.a = a
|
||||
self.b = b
|
||||
self.c = c
|
||||
|
||||
def _equality_tuple(self):
|
||||
return (self.a, self.b)
|
||||
|
||||
one = Simple(1, 2, 3)
|
||||
same = Simple(1, 2, 3)
|
||||
matches = Simple(1, 2, 'ignored')
|
||||
doesnt = Simple(2, 3, 4)
|
||||
|
||||
# equality
|
||||
self.assertEquals(one, one)
|
||||
self.assertEquals(one, same)
|
||||
self.assertEquals(same, one)
|
||||
# only a & c are considered
|
||||
self.assertEquals(one, matches)
|
||||
self.assertEquals(matches, one)
|
||||
self.assertNotEquals(one, doesnt)
|
||||
self.assertNotEquals(doesnt, one)
|
||||
|
||||
# lt
|
||||
self.assertTrue(one < doesnt)
|
||||
self.assertFalse(doesnt < one)
|
||||
self.assertFalse(one < same)
|
||||
|
||||
# le
|
||||
self.assertTrue(one <= doesnt)
|
||||
self.assertFalse(doesnt <= one)
|
||||
self.assertTrue(one <= same)
|
||||
|
||||
# gt
|
||||
self.assertFalse(one > doesnt)
|
||||
self.assertTrue(doesnt > one)
|
||||
self.assertFalse(one > same)
|
||||
|
||||
# ge
|
||||
self.assertFalse(one >= doesnt)
|
||||
self.assertTrue(doesnt >= one)
|
||||
self.assertTrue(one >= same)
|
||||
|
||||
def test_not_implemented(self):
|
||||
|
||||
class MissingMethod(EqualityTupleMixin):
|
||||
pass
|
||||
|
||||
with self.assertRaises(NotImplementedError):
|
||||
MissingMethod() == MissingMethod()
|
||||
@@ -7,10 +7,12 @@ from __future__ import absolute_import, division, print_function, \
|
||||
|
||||
from os import environ
|
||||
from os.path import dirname, join
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager
|
||||
from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager, \
|
||||
ManagerException
|
||||
from octodns.yaml import safe_load
|
||||
from octodns.zone import Zone
|
||||
|
||||
@@ -27,80 +29,81 @@ def get_config_filename(which):
|
||||
class TestManager(TestCase):
|
||||
|
||||
def test_missing_provider_class(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('missing-provider-class.yaml')).sync()
|
||||
self.assertTrue('missing class' in ctx.exception.message)
|
||||
self.assertTrue('missing class' in text_type(ctx.exception))
|
||||
|
||||
def test_bad_provider_class(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('bad-provider-class.yaml')).sync()
|
||||
self.assertTrue('Unknown provider class' in ctx.exception.message)
|
||||
self.assertTrue('Unknown provider class' in text_type(ctx.exception))
|
||||
|
||||
def test_bad_provider_class_module(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('bad-provider-class-module.yaml')) \
|
||||
.sync()
|
||||
self.assertTrue('Unknown provider class' in ctx.exception.message)
|
||||
self.assertTrue('Unknown provider class' in text_type(ctx.exception))
|
||||
|
||||
def test_bad_provider_class_no_module(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('bad-provider-class-no-module.yaml')) \
|
||||
.sync()
|
||||
self.assertTrue('Unknown provider class' in ctx.exception.message)
|
||||
self.assertTrue('Unknown provider class' in text_type(ctx.exception))
|
||||
|
||||
def test_missing_provider_config(self):
|
||||
# Missing provider config
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('missing-provider-config.yaml')).sync()
|
||||
self.assertTrue('provider config' in ctx.exception.message)
|
||||
self.assertTrue('provider config' in text_type(ctx.exception))
|
||||
|
||||
def test_missing_env_config(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('missing-provider-env.yaml')).sync()
|
||||
self.assertTrue('missing env var' in ctx.exception.message)
|
||||
self.assertTrue('missing env var' in text_type(ctx.exception))
|
||||
|
||||
def test_missing_source(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Manager(get_config_filename('unknown-provider.yaml')) \
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('provider-problems.yaml')) \
|
||||
.sync(['missing.sources.'])
|
||||
self.assertTrue('missing sources' in ctx.exception.message)
|
||||
self.assertTrue('missing sources' in text_type(ctx.exception))
|
||||
|
||||
def test_missing_targets(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Manager(get_config_filename('unknown-provider.yaml')) \
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('provider-problems.yaml')) \
|
||||
.sync(['missing.targets.'])
|
||||
self.assertTrue('missing targets' in ctx.exception.message)
|
||||
self.assertTrue('missing targets' in text_type(ctx.exception))
|
||||
|
||||
def test_unknown_source(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Manager(get_config_filename('unknown-provider.yaml')) \
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('provider-problems.yaml')) \
|
||||
.sync(['unknown.source.'])
|
||||
self.assertTrue('unknown source' in ctx.exception.message)
|
||||
self.assertTrue('unknown source' in text_type(ctx.exception))
|
||||
|
||||
def test_unknown_target(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Manager(get_config_filename('unknown-provider.yaml')) \
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('provider-problems.yaml')) \
|
||||
.sync(['unknown.target.'])
|
||||
self.assertTrue('unknown target' in ctx.exception.message)
|
||||
self.assertTrue('unknown target' in text_type(ctx.exception))
|
||||
|
||||
def test_bad_plan_output_class(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
name = 'bad-plan-output-missing-class.yaml'
|
||||
Manager(get_config_filename(name)).sync()
|
||||
self.assertEquals('plan_output bad is missing class',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
def test_bad_plan_output_config(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('bad-plan-output-config.yaml')).sync()
|
||||
self.assertEqual('Incorrect plan_output config for bad',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
def test_source_only_as_a_target(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Manager(get_config_filename('unknown-provider.yaml')) \
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('provider-problems.yaml')) \
|
||||
.sync(['not.targetable.'])
|
||||
self.assertTrue('does not support targeting' in ctx.exception.message)
|
||||
self.assertTrue('does not support targeting' in
|
||||
text_type(ctx.exception))
|
||||
|
||||
def test_always_dry_run(self):
|
||||
with TemporaryDirectory() as tmpdir:
|
||||
@@ -180,9 +183,9 @@ class TestManager(TestCase):
|
||||
'unit.tests.')
|
||||
self.assertEquals(14, len(changes))
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
manager.compare(['nope'], ['dump'], 'unit.tests.')
|
||||
self.assertEquals('Unknown source: nope', ctx.exception.message)
|
||||
self.assertEquals('Unknown source: nope', text_type(ctx.exception))
|
||||
|
||||
def test_aggregate_target(self):
|
||||
simple = SimpleProvider()
|
||||
@@ -220,10 +223,10 @@ class TestManager(TestCase):
|
||||
environ['YAML_TMP_DIR'] = tmpdir.dirname
|
||||
manager = Manager(get_config_filename('simple.yaml'))
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
manager.dump('unit.tests.', tmpdir.dirname, False, False,
|
||||
'nope')
|
||||
self.assertEquals('Unknown source: nope', ctx.exception.message)
|
||||
self.assertEquals('Unknown source: nope', text_type(ctx.exception))
|
||||
|
||||
manager.dump('unit.tests.', tmpdir.dirname, False, False, 'in')
|
||||
|
||||
@@ -249,10 +252,10 @@ class TestManager(TestCase):
|
||||
environ['YAML_TMP_DIR'] = tmpdir.dirname
|
||||
manager = Manager(get_config_filename('simple-split.yaml'))
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
manager.dump('unit.tests.', tmpdir.dirname, False, True,
|
||||
'nope')
|
||||
self.assertEquals('Unknown source: nope', ctx.exception.message)
|
||||
self.assertEquals('Unknown source: nope', text_type(ctx.exception))
|
||||
|
||||
manager.dump('unit.tests.', tmpdir.dirname, False, True, 'in')
|
||||
|
||||
@@ -265,15 +268,15 @@ class TestManager(TestCase):
|
||||
def test_validate_configs(self):
|
||||
Manager(get_config_filename('simple-validate.yaml')).validate_configs()
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('missing-sources.yaml')) \
|
||||
.validate_configs()
|
||||
self.assertTrue('missing sources' in ctx.exception.message)
|
||||
self.assertTrue('missing sources' in text_type(ctx.exception))
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
with self.assertRaises(ManagerException) as ctx:
|
||||
Manager(get_config_filename('unknown-provider.yaml')) \
|
||||
.validate_configs()
|
||||
self.assertTrue('unknown source' in ctx.exception.message)
|
||||
self.assertTrue('unknown source' in text_type(ctx.exception))
|
||||
|
||||
|
||||
class TestMainThreadExecutor(TestCase):
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from StringIO import StringIO
|
||||
from logging import getLogger
|
||||
from six import StringIO, text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.provider.plan import Plan, PlanHtml, PlanLogger, PlanMarkdown
|
||||
@@ -59,7 +59,7 @@ class TestPlanLogger(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
PlanLogger('invalid', 'not-a-level')
|
||||
self.assertEquals('Unsupported level: not-a-level',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
def test_create(self):
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from logging import getLogger
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Create, Delete, Record, Update
|
||||
@@ -48,7 +49,7 @@ class TestBaseProvider(TestCase):
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
BaseProvider('base')
|
||||
self.assertEquals('Abstract base class, log property missing',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
class HasLog(BaseProvider):
|
||||
log = getLogger('HasLog')
|
||||
@@ -56,7 +57,7 @@ class TestBaseProvider(TestCase):
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasLog('haslog')
|
||||
self.assertEquals('Abstract base class, SUPPORTS_GEO property missing',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
class HasSupportsGeo(HasLog):
|
||||
SUPPORTS_GEO = False
|
||||
@@ -65,14 +66,14 @@ class TestBaseProvider(TestCase):
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupportsGeo('hassupportsgeo').populate(zone)
|
||||
self.assertEquals('Abstract base class, SUPPORTS property missing',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
class HasSupports(HasSupportsGeo):
|
||||
SUPPORTS = set(('A',))
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupports('hassupports').populate(zone)
|
||||
self.assertEquals('Abstract base class, populate method missing',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
# SUPPORTS_DYNAMIC has a default/fallback
|
||||
self.assertFalse(HasSupports('hassupports').SUPPORTS_DYNAMIC)
|
||||
@@ -118,7 +119,7 @@ class TestBaseProvider(TestCase):
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasPopulate('haspopulate').apply(plan)
|
||||
self.assertEquals('Abstract base class, _apply method missing',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
def test_plan(self):
|
||||
ignored = Zone('unit.tests.', [])
|
||||
@@ -193,7 +194,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
zone.add_record(Record.new(zone, text_type(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -225,7 +226,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
zone.add_record(Record.new(zone, text_type(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -238,7 +239,7 @@ class TestBaseProvider(TestCase):
|
||||
with self.assertRaises(UnsafePlan) as ctx:
|
||||
Plan(zone, zone, changes, True).raise_if_unsafe()
|
||||
|
||||
self.assertTrue('Too many updates' in ctx.exception.message)
|
||||
self.assertTrue('Too many updates' in text_type(ctx.exception))
|
||||
|
||||
def test_safe_updates_min_existing_pcent(self):
|
||||
# MAX_SAFE_UPDATE_PCENT is safe when more
|
||||
@@ -251,7 +252,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
zone.add_record(Record.new(zone, text_type(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -273,7 +274,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
zone.add_record(Record.new(zone, text_type(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -286,7 +287,7 @@ class TestBaseProvider(TestCase):
|
||||
with self.assertRaises(UnsafePlan) as ctx:
|
||||
Plan(zone, zone, changes, True).raise_if_unsafe()
|
||||
|
||||
self.assertTrue('Too many deletes' in ctx.exception.message)
|
||||
self.assertTrue('Too many deletes' in text_type(ctx.exception))
|
||||
|
||||
def test_safe_deletes_min_existing_pcent(self):
|
||||
# MAX_SAFE_DELETE_PCENT is safe when more
|
||||
@@ -299,7 +300,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
zone.add_record(Record.new(zone, text_type(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -322,7 +323,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
zone.add_record(Record.new(zone, text_type(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -336,7 +337,7 @@ class TestBaseProvider(TestCase):
|
||||
Plan(zone, zone, changes, True,
|
||||
update_pcent_threshold=safe_pcent).raise_if_unsafe()
|
||||
|
||||
self.assertTrue('Too many updates' in ctx.exception.message)
|
||||
self.assertTrue('Too many updates' in text_type(ctx.exception))
|
||||
|
||||
def test_safe_deletes_min_existing_override(self):
|
||||
safe_pcent = .4
|
||||
@@ -350,7 +351,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS)):
|
||||
zone.add_record(Record.new(zone, unicode(i), {
|
||||
zone.add_record(Record.new(zone, text_type(i), {
|
||||
'ttl': 60,
|
||||
'type': 'A',
|
||||
'value': '2.3.4.5'
|
||||
@@ -364,4 +365,4 @@ class TestBaseProvider(TestCase):
|
||||
Plan(zone, zone, changes, True,
|
||||
delete_pcent_threshold=safe_pcent).raise_if_unsafe()
|
||||
|
||||
self.assertTrue('Too many deletes' in ctx.exception.message)
|
||||
self.assertTrue('Too many deletes' in text_type(ctx.exception))
|
||||
|
||||
@@ -9,6 +9,7 @@ from mock import Mock, call
|
||||
from os.path import dirname, join
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record, Update
|
||||
@@ -65,7 +66,7 @@ class TestCloudflareProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
|
||||
self.assertEquals('CloudflareError', type(ctx.exception).__name__)
|
||||
self.assertEquals('request was invalid', ctx.exception.message)
|
||||
self.assertEquals('request was invalid', text_type(ctx.exception))
|
||||
|
||||
# Bad auth
|
||||
with requests_mock() as mock:
|
||||
@@ -80,7 +81,7 @@ class TestCloudflareProvider(TestCase):
|
||||
self.assertEquals('CloudflareAuthenticationError',
|
||||
type(ctx.exception).__name__)
|
||||
self.assertEquals('Unknown X-Auth-Key or X-Auth-Email',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
# Bad auth, unknown resp
|
||||
with requests_mock() as mock:
|
||||
@@ -91,7 +92,7 @@ class TestCloudflareProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals('CloudflareAuthenticationError',
|
||||
type(ctx.exception).__name__)
|
||||
self.assertEquals('Cloudflare error', ctx.exception.message)
|
||||
self.assertEquals('Cloudflare error', text_type(ctx.exception))
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
@@ -742,23 +743,25 @@ class TestCloudflareProvider(TestCase):
|
||||
# the CDN.
|
||||
self.assertEquals(3, len(zone.records))
|
||||
|
||||
record = list(zone.records)[0]
|
||||
self.assertEquals('multi', record.name)
|
||||
self.assertEquals('multi.unit.tests.', record.fqdn)
|
||||
self.assertEquals('CNAME', record._type)
|
||||
self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value)
|
||||
ordered = sorted(zone.records, key=lambda r: r.name)
|
||||
|
||||
record = list(zone.records)[1]
|
||||
record = ordered[0]
|
||||
self.assertEquals('a', record.name)
|
||||
self.assertEquals('a.unit.tests.', record.fqdn)
|
||||
self.assertEquals('CNAME', record._type)
|
||||
self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value)
|
||||
|
||||
record = ordered[1]
|
||||
self.assertEquals('cname', record.name)
|
||||
self.assertEquals('cname.unit.tests.', record.fqdn)
|
||||
self.assertEquals('CNAME', record._type)
|
||||
self.assertEquals('cname.unit.tests.cdn.cloudflare.net.', record.value)
|
||||
|
||||
record = list(zone.records)[2]
|
||||
self.assertEquals('a', record.name)
|
||||
self.assertEquals('a.unit.tests.', record.fqdn)
|
||||
record = ordered[2]
|
||||
self.assertEquals('multi', record.name)
|
||||
self.assertEquals('multi.unit.tests.', record.fqdn)
|
||||
self.assertEquals('CNAME', record._type)
|
||||
self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value)
|
||||
self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value)
|
||||
|
||||
# CDN enabled records can't be updated, we don't know the real values
|
||||
# never point a Cloudflare record to itself.
|
||||
@@ -950,7 +953,7 @@ class TestCloudflareProvider(TestCase):
|
||||
'value': 'ns1.unit.tests.'
|
||||
})
|
||||
|
||||
data = provider._gen_data(record).next()
|
||||
data = next(provider._gen_data(record))
|
||||
|
||||
self.assertFalse('proxied' in data)
|
||||
|
||||
@@ -965,7 +968,7 @@ class TestCloudflareProvider(TestCase):
|
||||
}), False
|
||||
)
|
||||
|
||||
data = provider._gen_data(record).next()
|
||||
data = next(provider._gen_data(record))
|
||||
|
||||
self.assertFalse(data['proxied'])
|
||||
|
||||
@@ -980,7 +983,7 @@ class TestCloudflareProvider(TestCase):
|
||||
}), True
|
||||
)
|
||||
|
||||
data = provider._gen_data(record).next()
|
||||
data = next(provider._gen_data(record))
|
||||
|
||||
self.assertTrue(data['proxied'])
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ from mock import Mock, call
|
||||
from os.path import dirname, join
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
@@ -65,7 +66,7 @@ class TestConstellixProvider(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('Unauthorized', ctx.exception.message)
|
||||
self.assertEquals('Unauthorized', text_type(ctx.exception))
|
||||
|
||||
# Bad request
|
||||
with requests_mock() as mock:
|
||||
@@ -77,7 +78,7 @@ class TestConstellixProvider(TestCase):
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('\n - "unittests" is not a valid domain name',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
@@ -148,6 +149,11 @@ class TestConstellixProvider(TestCase):
|
||||
call('POST', '/', data={'names': ['unit.tests']}),
|
||||
# get all domains to build the cache
|
||||
call('GET', '/'),
|
||||
])
|
||||
# These two checks are broken up so that ordering doesn't break things.
|
||||
# Python3 doesn't make the calls in a consistent order so different
|
||||
# things follow the GET / on different runs
|
||||
provider._client._request.assert_has_calls([
|
||||
call('POST', '/123123/records/SRV', data={
|
||||
'roundRobin': [{
|
||||
'priority': 10,
|
||||
|
||||
@@ -10,6 +10,7 @@ from mock import Mock, call
|
||||
from os.path import dirname, join
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
@@ -50,7 +51,7 @@ class TestDigitalOceanProvider(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('Unauthorized', ctx.exception.message)
|
||||
self.assertEquals('Unauthorized', text_type(ctx.exception))
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
@@ -175,7 +176,20 @@ class TestDigitalOceanProvider(TestCase):
|
||||
call('GET', '/domains/unit.tests/records', {'page': 1}),
|
||||
# delete the initial A record
|
||||
call('DELETE', '/domains/unit.tests/records/11189877'),
|
||||
# created at least one of the record with expected data
|
||||
# created at least some of the record with expected data
|
||||
call('POST', '/domains/unit.tests/records', data={
|
||||
'data': '1.2.3.4',
|
||||
'name': '@',
|
||||
'ttl': 300, 'type': 'A'}),
|
||||
call('POST', '/domains/unit.tests/records', data={
|
||||
'data': '1.2.3.5',
|
||||
'name': '@',
|
||||
'ttl': 300, 'type': 'A'}),
|
||||
call('POST', '/domains/unit.tests/records', data={
|
||||
'data': 'ca.unit.tests.',
|
||||
'flags': 0, 'name': '@',
|
||||
'tag': 'issue',
|
||||
'ttl': 3600, 'type': 'CAA'}),
|
||||
call('POST', '/domains/unit.tests/records', data={
|
||||
'name': '_srv._tcp',
|
||||
'weight': 20,
|
||||
|
||||
@@ -9,6 +9,7 @@ from mock import Mock, call
|
||||
from os.path import dirname, join
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
@@ -47,7 +48,7 @@ class TestDnsimpleProvider(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('Unauthorized', ctx.exception.message)
|
||||
self.assertEquals('Unauthorized', text_type(ctx.exception))
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
@@ -138,7 +139,32 @@ class TestDnsimpleProvider(TestCase):
|
||||
provider._client._request.assert_has_calls([
|
||||
# created the domain
|
||||
call('POST', '/domains', data={'name': 'unit.tests'}),
|
||||
# created at least one of the record with expected data
|
||||
# created at least some of the record with expected data
|
||||
call('POST', '/zones/unit.tests/records', data={
|
||||
'content': '1.2.3.4',
|
||||
'type': 'A',
|
||||
'name': '',
|
||||
'ttl': 300}),
|
||||
call('POST', '/zones/unit.tests/records', data={
|
||||
'content': '1.2.3.5',
|
||||
'type': 'A',
|
||||
'name': '',
|
||||
'ttl': 300}),
|
||||
call('POST', '/zones/unit.tests/records', data={
|
||||
'content': '0 issue "ca.unit.tests"',
|
||||
'type': 'CAA',
|
||||
'name': '',
|
||||
'ttl': 3600}),
|
||||
call('POST', '/zones/unit.tests/records', data={
|
||||
'content': '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49',
|
||||
'type': 'SSHFP',
|
||||
'name': '',
|
||||
'ttl': 3600}),
|
||||
call('POST', '/zones/unit.tests/records', data={
|
||||
'content': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73',
|
||||
'type': 'SSHFP',
|
||||
'name': '',
|
||||
'ttl': 3600}),
|
||||
call('POST', '/zones/unit.tests/records', data={
|
||||
'content': '20 30 foo-1.unit.tests.',
|
||||
'priority': 10,
|
||||
|
||||
@@ -10,6 +10,7 @@ from mock import Mock, call
|
||||
from os.path import dirname, join
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
@@ -65,7 +66,7 @@ class TestDnsMadeEasyProvider(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('Unauthorized', ctx.exception.message)
|
||||
self.assertEquals('Unauthorized', text_type(ctx.exception))
|
||||
|
||||
# Bad request
|
||||
with requests_mock() as mock:
|
||||
@@ -76,7 +77,7 @@ class TestDnsMadeEasyProvider(TestCase):
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertEquals('\n - Rate limit exceeded',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
@@ -148,7 +149,27 @@ class TestDnsMadeEasyProvider(TestCase):
|
||||
call('POST', '/', data={'name': 'unit.tests'}),
|
||||
# get all domains to build the cache
|
||||
call('GET', '/'),
|
||||
# created at least one of the record with expected data
|
||||
# created at least some of the record with expected data
|
||||
call('POST', '/123123/records', data={
|
||||
'type': 'A',
|
||||
'name': '',
|
||||
'value': '1.2.3.4',
|
||||
'ttl': 300}),
|
||||
call('POST', '/123123/records', data={
|
||||
'type': 'A',
|
||||
'name': '',
|
||||
'value': '1.2.3.5',
|
||||
'ttl': 300}),
|
||||
call('POST', '/123123/records', data={
|
||||
'type': 'ANAME',
|
||||
'name': '',
|
||||
'value': 'aname.unit.tests.',
|
||||
'ttl': 1800}),
|
||||
call('POST', '/123123/records', data={
|
||||
'name': '',
|
||||
'value': 'ca.unit.tests',
|
||||
'issuerCritical': 0, 'caaType': 'issue',
|
||||
'ttl': 3600, 'type': 'CAA'}),
|
||||
call('POST', '/123123/records', data={
|
||||
'name': '_srv._tcp',
|
||||
'weight': 20,
|
||||
|
||||
@@ -670,8 +670,8 @@ class TestDynProviderGeo(TestCase):
|
||||
tds = provider.traffic_directors
|
||||
self.assertEquals(set(['unit.tests.', 'geo.unit.tests.']),
|
||||
set(tds.keys()))
|
||||
self.assertEquals(['A'], tds['unit.tests.'].keys())
|
||||
self.assertEquals(['A'], tds['geo.unit.tests.'].keys())
|
||||
self.assertEquals(['A'], list(tds['unit.tests.'].keys()))
|
||||
self.assertEquals(['A'], list(tds['geo.unit.tests.'].keys()))
|
||||
provider.log.warn.assert_called_with("Unsupported TrafficDirector "
|
||||
"'%s'", 'something else')
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
from os.path import dirname, join
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
@@ -147,4 +148,4 @@ class TestFastdnsProvider(TestCase):
|
||||
changes = provider.apply(plan)
|
||||
except NameError as e:
|
||||
expected = "contractId not specified to create zone"
|
||||
self.assertEquals(e.message, expected)
|
||||
self.assertEquals(text_type(e), expected)
|
||||
|
||||
@@ -193,8 +193,13 @@ class DummyIterator:
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
# python2
|
||||
def next(self):
|
||||
return self.iterable.next()
|
||||
return next(self.iterable)
|
||||
|
||||
# python3
|
||||
def __next__(self):
|
||||
return next(self.iterable)
|
||||
|
||||
|
||||
class TestGoogleCloudProvider(TestCase):
|
||||
@@ -247,7 +252,7 @@ class TestGoogleCloudProvider(TestCase):
|
||||
return_values_for_status = iter(
|
||||
["pending"] * 11 + ['done', 'done'])
|
||||
type(status_mock).status = PropertyMock(
|
||||
side_effect=return_values_for_status.next)
|
||||
side_effect=lambda: next(return_values_for_status))
|
||||
gcloud_zone_mock.changes = Mock(return_value=status_mock)
|
||||
|
||||
provider = self._get_provider()
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
from os.path import dirname, join
|
||||
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.provider.mythicbeasts import MythicBeastsProvider, \
|
||||
@@ -31,12 +32,12 @@ class TestMythicBeastsProvider(TestCase):
|
||||
with self.assertRaises(AssertionError) as err:
|
||||
add_trailing_dot('unit.tests.')
|
||||
self.assertEquals('Value already has trailing dot',
|
||||
err.exception.message)
|
||||
text_type(err.exception))
|
||||
|
||||
with self.assertRaises(AssertionError) as err:
|
||||
remove_trailing_dot('unit.tests')
|
||||
self.assertEquals('Value already missing trailing dot',
|
||||
err.exception.message)
|
||||
text_type(err.exception))
|
||||
|
||||
self.assertEquals(add_trailing_dot('unit.tests'), 'unit.tests.')
|
||||
self.assertEquals(remove_trailing_dot('unit.tests.'), 'unit.tests')
|
||||
@@ -91,7 +92,7 @@ class TestMythicBeastsProvider(TestCase):
|
||||
{'raw_values': [{'value': '', 'ttl': 0}]}
|
||||
)
|
||||
self.assertEquals('Unable to parse MX data',
|
||||
err.exception.message)
|
||||
text_type(err.exception))
|
||||
|
||||
def test_data_for_CNAME(self):
|
||||
test_data = {
|
||||
@@ -129,7 +130,7 @@ class TestMythicBeastsProvider(TestCase):
|
||||
{'raw_values': [{'value': '', 'ttl': 0}]}
|
||||
)
|
||||
self.assertEquals('Unable to parse SRV data',
|
||||
err.exception.message)
|
||||
text_type(err.exception))
|
||||
|
||||
def test_data_for_SSHFP(self):
|
||||
test_data = {
|
||||
@@ -149,7 +150,7 @@ class TestMythicBeastsProvider(TestCase):
|
||||
{'raw_values': [{'value': '', 'ttl': 0}]}
|
||||
)
|
||||
self.assertEquals('Unable to parse SSHFP data',
|
||||
err.exception.message)
|
||||
text_type(err.exception))
|
||||
|
||||
def test_data_for_CAA(self):
|
||||
test_data = {
|
||||
@@ -166,7 +167,7 @@ class TestMythicBeastsProvider(TestCase):
|
||||
{'raw_values': [{'value': '', 'ttl': 0}]}
|
||||
)
|
||||
self.assertEquals('Unable to parse CAA data',
|
||||
err.exception.message)
|
||||
text_type(err.exception))
|
||||
|
||||
def test_command_generation(self):
|
||||
zone = Zone('unit.tests.', [])
|
||||
@@ -312,7 +313,7 @@ class TestMythicBeastsProvider(TestCase):
|
||||
with self.assertRaises(AssertionError) as err:
|
||||
provider = MythicBeastsProvider('test', None)
|
||||
self.assertEquals('Passwords must be a dictionary',
|
||||
err.exception.message)
|
||||
text_type(err.exception))
|
||||
|
||||
# Missing password
|
||||
with requests_mock() as mock:
|
||||
@@ -324,7 +325,7 @@ class TestMythicBeastsProvider(TestCase):
|
||||
provider.populate(zone)
|
||||
self.assertEquals(
|
||||
'Missing password for domain: unit.tests',
|
||||
err.exception.message)
|
||||
text_type(err.exception))
|
||||
|
||||
# Failed authentication
|
||||
with requests_mock() as mock:
|
||||
@@ -413,8 +414,7 @@ class TestMythicBeastsProvider(TestCase):
|
||||
provider.apply(plan)
|
||||
self.assertEquals(
|
||||
'Mythic Beasts could not action command: unit.tests '
|
||||
'ADD prawf.unit.tests 300 TXT prawf',
|
||||
err.exception.message)
|
||||
'ADD prawf.unit.tests 300 TXT prawf', err.exception.message)
|
||||
|
||||
# Check deleting and adding/changing test record
|
||||
existing = 'prawf 300 TXT prawf prawf prawf\ndileu 300 TXT dileu'
|
||||
@@ -441,11 +441,11 @@ class TestMythicBeastsProvider(TestCase):
|
||||
plan = provider.plan(wanted)
|
||||
|
||||
# Octo ignores NS records (15-1)
|
||||
self.assertEquals(1, len(filter(lambda u: isinstance(u, Update),
|
||||
plan.changes)))
|
||||
self.assertEquals(1, len(filter(lambda d: isinstance(d, Delete),
|
||||
plan.changes)))
|
||||
self.assertEquals(14, len(filter(lambda c: isinstance(c, Create),
|
||||
plan.changes)))
|
||||
self.assertEquals(1, len([c for c in plan.changes
|
||||
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
|
||||
if isinstance(c, Create)]))
|
||||
self.assertEquals(16, provider.apply(plan))
|
||||
self.assertTrue(plan.exists)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from collections import defaultdict
|
||||
from mock import Mock, call, patch
|
||||
from ns1.rest.errors import AuthException, RateLimitException, \
|
||||
ResourceException
|
||||
@@ -373,8 +374,12 @@ class TestNs1Provider(TestCase):
|
||||
load_mock.side_effect = [nsone_zone, nsone_zone]
|
||||
plan = provider.plan(desired)
|
||||
self.assertEquals(3, len(plan.changes))
|
||||
self.assertIsInstance(plan.changes[0], Update)
|
||||
self.assertIsInstance(plan.changes[2], Delete)
|
||||
# Shouldn't rely on order so just count classes
|
||||
classes = defaultdict(lambda: 0)
|
||||
for change in plan.changes:
|
||||
classes[change.__class__] += 1
|
||||
self.assertEquals(1, classes[Delete])
|
||||
self.assertEquals(2, classes[Update])
|
||||
# ugh, we need a mock record that can be returned from loadRecord for
|
||||
# the update and delete targets, we can add our side effects to that to
|
||||
# trigger rate limit handling
|
||||
@@ -395,8 +400,8 @@ class TestNs1Provider(TestCase):
|
||||
self.assertEquals(3, got_n)
|
||||
nsone_zone.loadRecord.assert_has_calls([
|
||||
call('unit.tests', u'A'),
|
||||
call('geo', u'A'),
|
||||
call('delete-me', u'A'),
|
||||
call('geo', u'A'),
|
||||
])
|
||||
mock_record.assert_has_calls([
|
||||
call.update(answers=[{'answer': [u'1.2.3.4'], 'meta': {}}],
|
||||
@@ -405,6 +410,8 @@ class TestNs1Provider(TestCase):
|
||||
call.update(answers=[{u'answer': [u'1.2.3.4'], u'meta': {}}],
|
||||
filters=[],
|
||||
ttl=32),
|
||||
call.delete(),
|
||||
call.delete(),
|
||||
call.update(
|
||||
answers=[
|
||||
{u'answer': [u'101.102.103.104'], u'meta': {}},
|
||||
@@ -422,8 +429,6 @@ class TestNs1Provider(TestCase):
|
||||
{u'filter': u'select_first_n', u'config': {u'N': 1}},
|
||||
],
|
||||
ttl=34),
|
||||
call.delete(),
|
||||
call.delete()
|
||||
])
|
||||
|
||||
def test_escaping(self):
|
||||
|
||||
@@ -382,64 +382,63 @@ class TestOvhProvider(TestCase):
|
||||
get_mock.side_effect = [[100], [101], [102], [103]]
|
||||
provider.apply(plan)
|
||||
wanted_calls = [
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'TXT',
|
||||
subDomain='txt', target=u'TXT text', ttl=1400),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM',
|
||||
subDomain='dkim', target=self.valid_dkim_key,
|
||||
ttl=1300),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||
subDomain=u'', target=u'1.2.3.4', ttl=100),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
|
||||
call('/domain/zone/unit.tests/record', fieldType='A',
|
||||
subDomain='', target='1.2.3.4', ttl=100),
|
||||
call('/domain/zone/unit.tests/record', fieldType='AAAA',
|
||||
subDomain='', target='1:1ec:1::1', ttl=200),
|
||||
call('/domain/zone/unit.tests/record', fieldType='MX',
|
||||
subDomain='', target='10 mx1.unit.tests.', ttl=400),
|
||||
call('/domain/zone/unit.tests/record', fieldType='SPF',
|
||||
subDomain='',
|
||||
target='v=spf1 include:unit.texts.redirect ~all',
|
||||
ttl=1000),
|
||||
call('/domain/zone/unit.tests/record', fieldType='SSHFP',
|
||||
subDomain='',
|
||||
target='1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73',
|
||||
ttl=1100),
|
||||
call('/domain/zone/unit.tests/record', fieldType='PTR',
|
||||
subDomain='4', target='unit.tests.', ttl=900),
|
||||
call('/domain/zone/unit.tests/record', fieldType='SRV',
|
||||
subDomain='_srv._tcp',
|
||||
target=u'10 20 30 foo-1.unit.tests.', ttl=800),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
|
||||
target='10 20 30 foo-1.unit.tests.', ttl=800),
|
||||
call('/domain/zone/unit.tests/record', fieldType='SRV',
|
||||
subDomain='_srv._tcp',
|
||||
target=u'40 50 60 foo-2.unit.tests.', ttl=800),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'PTR',
|
||||
subDomain='4', target=u'unit.tests.', ttl=900),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'NS',
|
||||
subDomain='www3', target=u'ns3.unit.tests.', ttl=700),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'NS',
|
||||
subDomain='www3', target=u'ns4.unit.tests.', ttl=700),
|
||||
call(u'/domain/zone/unit.tests/record',
|
||||
fieldType=u'SSHFP', subDomain=u'', ttl=1100,
|
||||
target=u'1 1 bf6b6825d2977c511a475bbefb88a'
|
||||
u'ad54'
|
||||
u'a92ac73',
|
||||
),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'AAAA',
|
||||
subDomain=u'', target=u'1:1ec:1::1', ttl=200),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'MX',
|
||||
subDomain=u'', target=u'10 mx1.unit.tests.', ttl=400),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'CNAME',
|
||||
subDomain='www2', target=u'unit.tests.', ttl=300),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SPF',
|
||||
subDomain=u'', ttl=1000,
|
||||
target=u'v=spf1 include:unit.texts.'
|
||||
u'redirect ~all',
|
||||
),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||
subDomain='sub', target=u'1.2.3.4', ttl=200),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'NAPTR',
|
||||
subDomain='naptr', ttl=500,
|
||||
target=u'10 100 "S" "SIP+D2U" "!^.*$!sip:'
|
||||
u'info@bar'
|
||||
u'.example.com!" .'
|
||||
),
|
||||
call(u'/domain/zone/unit.tests/refresh')]
|
||||
target='40 50 60 foo-2.unit.tests.', ttl=800),
|
||||
call('/domain/zone/unit.tests/record', fieldType='DKIM',
|
||||
subDomain='dkim',
|
||||
target='p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxLaG'
|
||||
'16G4SaEcXVdiIxTg7gKSGbHKQLm30CHib1h9FzS9nkcyvQSyQj1r'
|
||||
'MFyqC//tft3ohx3nvJl+bGCWxdtLYDSmir9PW54e5CTdxEh8MWRk'
|
||||
'BO3StF6QG/tAh3aTGDmkqhIJGLb87iHvpmVKqURmEUzJPv5KPJfW'
|
||||
'LofADI+q9lQIDAQAB', ttl=1300),
|
||||
call('/domain/zone/unit.tests/record', fieldType='NAPTR',
|
||||
subDomain='naptr',
|
||||
target='10 100 "S" "SIP+D2U" "!^.*$!sip:[email protected]'
|
||||
'ple.com!" .', ttl=500),
|
||||
call('/domain/zone/unit.tests/record', fieldType='A',
|
||||
subDomain='sub', target='1.2.3.4', ttl=200),
|
||||
call('/domain/zone/unit.tests/record', fieldType='TXT',
|
||||
subDomain='txt', target='TXT text', ttl=1400),
|
||||
call('/domain/zone/unit.tests/record', fieldType='CNAME',
|
||||
subDomain='www2', target='unit.tests.', ttl=300),
|
||||
call('/domain/zone/unit.tests/record', fieldType='NS',
|
||||
subDomain='www3', target='ns3.unit.tests.', ttl=700),
|
||||
call('/domain/zone/unit.tests/record', fieldType='NS',
|
||||
subDomain='www3', target='ns4.unit.tests.', ttl=700),
|
||||
call('/domain/zone/unit.tests/refresh')]
|
||||
|
||||
post_mock.assert_has_calls(wanted_calls)
|
||||
|
||||
# Get for delete calls
|
||||
wanted_get_calls = [
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'TXT',
|
||||
subDomain='txt'),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||
subDomain=u''),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM',
|
||||
subDomain='dkim'),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||
subDomain=u''),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||
subDomain='fake')]
|
||||
subDomain='fake'),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'TXT',
|
||||
subDomain='txt')]
|
||||
get_mock.assert_has_calls(wanted_get_calls)
|
||||
# 4 delete calls for update and delete
|
||||
delete_mock.assert_has_calls(
|
||||
|
||||
@@ -9,6 +9,7 @@ from json import loads, dumps
|
||||
from os.path import dirname, join
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
@@ -52,7 +53,7 @@ class TestPowerDnsProvider(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
self.assertTrue('unauthorized' in ctx.exception.message)
|
||||
self.assertTrue('unauthorized' in text_type(ctx.exception))
|
||||
|
||||
# General error
|
||||
with requests_mock() as mock:
|
||||
|
||||
@@ -7,8 +7,9 @@ from __future__ import absolute_import, division, print_function, \
|
||||
|
||||
import json
|
||||
import re
|
||||
from six import text_type
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from unittest import TestCase
|
||||
from urlparse import urlparse
|
||||
|
||||
from requests import HTTPError
|
||||
from requests_mock import ANY, mock as requests_mock
|
||||
@@ -39,7 +40,6 @@ with open('./tests/fixtures/rackspace-sample-recordset-page2.json') as fh:
|
||||
|
||||
class TestRackspaceProvider(TestCase):
|
||||
def setUp(self):
|
||||
self.maxDiff = 1000
|
||||
with requests_mock() as mock:
|
||||
mock.post(ANY, status_code=200, text=AUTH_RESPONSE)
|
||||
self.provider = RackspaceProvider('identity', 'test', 'api-key',
|
||||
@@ -53,7 +53,7 @@ class TestRackspaceProvider(TestCase):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
zone = Zone('unit.tests.', [])
|
||||
self.provider.populate(zone)
|
||||
self.assertTrue('unauthorized' in ctx.exception.message)
|
||||
self.assertTrue('unauthorized' in text_type(ctx.exception))
|
||||
self.assertTrue(mock.called_once)
|
||||
|
||||
def test_server_error(self):
|
||||
@@ -792,13 +792,13 @@ class TestRackspaceProvider(TestCase):
|
||||
ExpectedUpdates = {
|
||||
"records": [{
|
||||
"name": "unit.tests",
|
||||
"id": "A-222222",
|
||||
"data": "1.2.3.5",
|
||||
"id": "A-111111",
|
||||
"data": "1.2.3.4",
|
||||
"ttl": 3600
|
||||
}, {
|
||||
"name": "unit.tests",
|
||||
"id": "A-111111",
|
||||
"data": "1.2.3.4",
|
||||
"id": "A-222222",
|
||||
"data": "1.2.3.5",
|
||||
"ttl": 3600
|
||||
}, {
|
||||
"name": "unit.tests",
|
||||
|
||||
@@ -7,6 +7,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
|
||||
from botocore.exceptions import ClientError
|
||||
from botocore.stub import ANY, Stubber
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
from mock import patch
|
||||
|
||||
@@ -1881,10 +1882,10 @@ class TestRoute53Provider(TestCase):
|
||||
@patch('octodns.provider.route53.Route53Provider._really_apply')
|
||||
def test_apply_3(self, really_apply_mock, _):
|
||||
|
||||
# with a max of seven modifications, four calls
|
||||
# with a max of seven modifications, three calls
|
||||
provider, plan = self._get_test_plan(7)
|
||||
provider.apply(plan)
|
||||
self.assertEquals(4, really_apply_mock.call_count)
|
||||
self.assertEquals(3, really_apply_mock.call_count)
|
||||
|
||||
@patch('octodns.provider.route53.Route53Provider._load_records')
|
||||
@patch('octodns.provider.route53.Route53Provider._really_apply')
|
||||
@@ -1903,7 +1904,7 @@ class TestRoute53Provider(TestCase):
|
||||
provider, plan = self._get_test_plan(1)
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
provider.apply(plan)
|
||||
self.assertTrue('modifications' in ctx.exception.message)
|
||||
self.assertTrue('modifications' in text_type(ctx.exception))
|
||||
|
||||
def test_semicolon_fixup(self):
|
||||
provider = Route53Provider('test', 'abc', '123')
|
||||
@@ -2090,6 +2091,58 @@ class TestRoute53Records(TestCase):
|
||||
e.__repr__()
|
||||
f.__repr__()
|
||||
|
||||
def test_route53_record_ordering(self):
|
||||
# Matches
|
||||
a = _Route53Record(None, self.record_a, False)
|
||||
b = _Route53Record(None, self.record_a, False)
|
||||
self.assertTrue(a == b)
|
||||
self.assertFalse(a != b)
|
||||
self.assertFalse(a < b)
|
||||
self.assertTrue(a <= b)
|
||||
self.assertFalse(a > b)
|
||||
self.assertTrue(a >= b)
|
||||
|
||||
# Change the fqdn is greater
|
||||
fqdn = _Route53Record(None, self.record_a, False,
|
||||
fqdn_override='other')
|
||||
self.assertFalse(a == fqdn)
|
||||
self.assertTrue(a != fqdn)
|
||||
self.assertFalse(a < fqdn)
|
||||
self.assertFalse(a <= fqdn)
|
||||
self.assertTrue(a > fqdn)
|
||||
self.assertTrue(a >= fqdn)
|
||||
|
||||
provider = DummyProvider()
|
||||
geo_a = _Route53GeoRecord(provider, self.record_a, 'NA-US',
|
||||
self.record_a.geo['NA-US'], False)
|
||||
geo_b = _Route53GeoRecord(provider, self.record_a, 'NA-US',
|
||||
self.record_a.geo['NA-US'], False)
|
||||
self.assertTrue(geo_a == geo_b)
|
||||
self.assertFalse(geo_a != geo_b)
|
||||
self.assertFalse(geo_a < geo_b)
|
||||
self.assertTrue(geo_a <= geo_b)
|
||||
self.assertFalse(geo_a > geo_b)
|
||||
self.assertTrue(geo_a >= geo_b)
|
||||
|
||||
# Other base
|
||||
geo_fqdn = _Route53GeoRecord(provider, self.record_a, 'NA-US',
|
||||
self.record_a.geo['NA-US'], False)
|
||||
geo_fqdn.fqdn = 'other'
|
||||
self.assertFalse(geo_a == geo_fqdn)
|
||||
self.assertTrue(geo_a != geo_fqdn)
|
||||
self.assertFalse(geo_a < geo_fqdn)
|
||||
self.assertFalse(geo_a <= geo_fqdn)
|
||||
self.assertTrue(geo_a > geo_fqdn)
|
||||
self.assertTrue(geo_a >= geo_fqdn)
|
||||
|
||||
# Other class
|
||||
self.assertFalse(a == geo_a)
|
||||
self.assertTrue(a != geo_a)
|
||||
self.assertFalse(a < geo_a)
|
||||
self.assertFalse(a <= geo_a)
|
||||
self.assertTrue(a > geo_a)
|
||||
self.assertTrue(a >= geo_a)
|
||||
|
||||
def test_dynamic_value_delete(self):
|
||||
provider = DummyProvider()
|
||||
geo = _Route53DynamicValue(provider, self.record_a, 'iad', '2.2.2.2',
|
||||
@@ -2206,136 +2259,38 @@ class TestRoute53Records(TestCase):
|
||||
creating=True)
|
||||
self.assertEquals(18, len(route53_records))
|
||||
|
||||
expected_mods = [r.mod('CREATE', []) for r in route53_records]
|
||||
# Sort so that we get a consistent order and don't rely on set ordering
|
||||
expected_mods.sort(key=_mod_keyer)
|
||||
|
||||
# Convert the route53_records into mods
|
||||
self.assertEquals([{
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-ap-southeast-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.4.1.2'}],
|
||||
'SetIdentifier': 'ap-southeast-1-001',
|
||||
'ResourceRecords': [{'Value': '1.4.1.1'}],
|
||||
'SetIdentifier': 'ap-southeast-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 2
|
||||
}
|
||||
'Weight': 2}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-ap-southeast-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.4.1.1'}],
|
||||
'SetIdentifier': 'ap-southeast-1-000',
|
||||
'ResourceRecords': [{'Value': '1.4.1.2'}],
|
||||
'SetIdentifier': 'ap-southeast-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 2
|
||||
}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'
|
||||
},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'JP'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '0-ap-southeast-1-AS-JP',
|
||||
'Type': 'A'
|
||||
}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'US',
|
||||
'SubdivisionCode': 'FL',
|
||||
},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '1-eu-central-1-NA-US-FL',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': '*'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '2-us-east-1-None',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'ap-southeast-1-Secondary-us-east-1',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'CN'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '0-ap-southeast-1-AS-CN',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'us-east-1-Primary',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'ContinentCode': 'EU'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '1-eu-central-1-EU',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'eu-central-1-Primary',
|
||||
'Type': 'A'}
|
||||
'Weight': 2}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'Name': '_octodns-default-pool.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.1.2.1'},
|
||||
{
|
||||
'Value': '1.1.2.2'}],
|
||||
'ResourceRecords': [
|
||||
{'Value': '1.1.2.1'},
|
||||
{'Value': '1.1.2.2'}],
|
||||
'TTL': 60,
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
@@ -2343,56 +2298,41 @@ class TestRoute53Records(TestCase):
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.3.1.2'}],
|
||||
'ResourceRecords': [{'Value': '1.3.1.1'}],
|
||||
'SetIdentifier': 'eu-central-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.3.1.2'}],
|
||||
'SetIdentifier': 'eu-central-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.3.1.1'}],
|
||||
'SetIdentifier': 'eu-central-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-default-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'us-east-1-Secondary-default',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-us-east-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.5.1.2'}],
|
||||
'SetIdentifier': 'us-east-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-us-east-1-value.unit.tests.',
|
||||
'ResourceRecords': [{
|
||||
'Value': '1.5.1.1'}],
|
||||
'ResourceRecords': [{'Value': '1.5.1.1'}],
|
||||
'SetIdentifier': 'us-east-1-000',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'HealthCheckId': 'hc42',
|
||||
'Name': '_octodns-us-east-1-value.unit.tests.',
|
||||
'ResourceRecords': [{'Value': '1.5.1.2'}],
|
||||
'SetIdentifier': 'us-east-1-001',
|
||||
'TTL': 60,
|
||||
'Type': 'A',
|
||||
'Weight': 1}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
@@ -2404,6 +2344,39 @@ class TestRoute53Records(TestCase):
|
||||
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'ap-southeast-1-Primary',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'eu-central-1-Primary',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-value.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'PRIMARY',
|
||||
'Name': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'us-east-1-Primary',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'ap-southeast-1-Secondary-us-east-1',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
@@ -2415,7 +2388,79 @@ class TestRoute53Records(TestCase):
|
||||
'Name': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'eu-central-1-Secondary-us-east-1',
|
||||
'Type': 'A'}
|
||||
}], [r.mod('CREATE', []) for r in route53_records])
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-default-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'Failover': 'SECONDARY',
|
||||
'Name': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'SetIdentifier': 'us-east-1-Secondary-default',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'CN'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '0-ap-southeast-1-AS-CN',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'JP'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '0-ap-southeast-1-AS-JP',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'ContinentCode': 'EU'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '1-eu-central-1-EU',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': 'US',
|
||||
'SubdivisionCode': 'FL'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '1-eu-central-1-NA-US-FL',
|
||||
'Type': 'A'}
|
||||
}, {
|
||||
'Action': 'CREATE',
|
||||
'ResourceRecordSet': {
|
||||
'AliasTarget': {
|
||||
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
|
||||
'EvaluateTargetHealth': True,
|
||||
'HostedZoneId': 'z45'},
|
||||
'GeoLocation': {
|
||||
'CountryCode': '*'},
|
||||
'Name': 'unit.tests.',
|
||||
'SetIdentifier': '2-us-east-1-None',
|
||||
'Type': 'A'}
|
||||
}], expected_mods)
|
||||
|
||||
for route53_record in route53_records:
|
||||
# Smoke test stringification
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from unittest import TestCase
|
||||
from six import text_type
|
||||
|
||||
import requests_mock
|
||||
|
||||
@@ -288,7 +289,7 @@ class TestSelectelProvider(TestCase):
|
||||
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
SelectelProvider(123, 'fail_token')
|
||||
self.assertEquals(ctx.exception.message,
|
||||
self.assertEquals(text_type(ctx.exception),
|
||||
'Authorization failed. Invalid or empty token.')
|
||||
|
||||
@requests_mock.Mocker()
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
# from mock import Mock, call
|
||||
from os.path import dirname, join
|
||||
from six import text_type
|
||||
|
||||
from suds import WebFault
|
||||
|
||||
@@ -176,7 +176,7 @@ N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
|
||||
self.assertEquals(
|
||||
'populate: (102) Transip used as target' +
|
||||
' for non-existing zone: notfound.unit.tests.',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
# Happy Plan - Zone does not exists
|
||||
# Won't trigger an exception if provider is NOT used as a target for a
|
||||
|
||||
@@ -8,6 +8,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
from os import makedirs
|
||||
from os.path import basename, dirname, isdir, isfile, join
|
||||
from unittest import TestCase
|
||||
from six import text_type
|
||||
from yaml import safe_load
|
||||
from yaml.constructor import ConstructorError
|
||||
|
||||
@@ -57,8 +58,8 @@ class TestYamlProvider(TestCase):
|
||||
|
||||
# We add everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(15, len(filter(lambda c: isinstance(c, Create),
|
||||
plan.changes)))
|
||||
self.assertEquals(15, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
self.assertFalse(isfile(yaml_file))
|
||||
|
||||
# Now actually do it
|
||||
@@ -67,8 +68,8 @@ class TestYamlProvider(TestCase):
|
||||
|
||||
# Dynamic plan
|
||||
plan = target.plan(dynamic_zone)
|
||||
self.assertEquals(5, len(filter(lambda c: isinstance(c, Create),
|
||||
plan.changes)))
|
||||
self.assertEquals(5, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
self.assertFalse(isfile(dynamic_yaml_file))
|
||||
# Apply it
|
||||
self.assertEquals(5, target.apply(plan))
|
||||
@@ -79,16 +80,15 @@ class TestYamlProvider(TestCase):
|
||||
target.populate(reloaded)
|
||||
self.assertDictEqual(
|
||||
{'included': ['test']},
|
||||
filter(
|
||||
lambda x: x.name == 'included', reloaded.records
|
||||
)[0]._octodns)
|
||||
[x for x in reloaded.records
|
||||
if x.name == 'included'][0]._octodns)
|
||||
|
||||
self.assertFalse(zone.changes(reloaded, target=source))
|
||||
|
||||
# A 2nd sync should still create everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(15, len(filter(lambda c: isinstance(c, Create),
|
||||
plan.changes)))
|
||||
self.assertEquals(15, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
|
||||
with open(yaml_file) as fh:
|
||||
data = safe_load(fh.read())
|
||||
@@ -116,7 +116,7 @@ class TestYamlProvider(TestCase):
|
||||
self.assertTrue('value' in data.pop('www.sub'))
|
||||
|
||||
# make sure nothing is left
|
||||
self.assertEquals([], data.keys())
|
||||
self.assertEquals([], list(data.keys()))
|
||||
|
||||
with open(dynamic_yaml_file) as fh:
|
||||
data = safe_load(fh.read())
|
||||
@@ -145,7 +145,7 @@ class TestYamlProvider(TestCase):
|
||||
# self.assertTrue('dynamic' in dyna)
|
||||
|
||||
# make sure nothing is left
|
||||
self.assertEquals([], data.keys())
|
||||
self.assertEquals([], list(data.keys()))
|
||||
|
||||
def test_empty(self):
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
@@ -178,7 +178,7 @@ class TestYamlProvider(TestCase):
|
||||
with self.assertRaises(SubzoneRecordException) as ctx:
|
||||
source.populate(zone)
|
||||
self.assertEquals('Record www.sub.unit.tests. is under a managed '
|
||||
'subzone', ctx.exception.message)
|
||||
'subzone', text_type(ctx.exception))
|
||||
|
||||
|
||||
class TestSplitYamlProvider(TestCase):
|
||||
@@ -201,9 +201,8 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
# This isn't great, but given the variable nature of the temp dir
|
||||
# names, it's necessary.
|
||||
self.assertItemsEqual(
|
||||
yaml_files,
|
||||
(basename(f) for f in _list_all_yaml_files(directory)))
|
||||
d = list(basename(f) for f in _list_all_yaml_files(directory))
|
||||
self.assertEqual(len(yaml_files), len(d))
|
||||
|
||||
def test_zone_directory(self):
|
||||
source = SplitYamlProvider(
|
||||
@@ -252,8 +251,8 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
# We add everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(15, len(filter(lambda c: isinstance(c, Create),
|
||||
plan.changes)))
|
||||
self.assertEquals(15, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
self.assertFalse(isdir(zone_dir))
|
||||
|
||||
# Now actually do it
|
||||
@@ -261,8 +260,8 @@ class TestSplitYamlProvider(TestCase):
|
||||
|
||||
# Dynamic plan
|
||||
plan = target.plan(dynamic_zone)
|
||||
self.assertEquals(5, len(filter(lambda c: isinstance(c, Create),
|
||||
plan.changes)))
|
||||
self.assertEquals(5, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
self.assertFalse(isdir(dynamic_zone_dir))
|
||||
# Apply it
|
||||
self.assertEquals(5, target.apply(plan))
|
||||
@@ -273,16 +272,15 @@ class TestSplitYamlProvider(TestCase):
|
||||
target.populate(reloaded)
|
||||
self.assertDictEqual(
|
||||
{'included': ['test']},
|
||||
filter(
|
||||
lambda x: x.name == 'included', reloaded.records
|
||||
)[0]._octodns)
|
||||
[x for x in reloaded.records
|
||||
if x.name == 'included'][0]._octodns)
|
||||
|
||||
self.assertFalse(zone.changes(reloaded, target=source))
|
||||
|
||||
# A 2nd sync should still create everything
|
||||
plan = target.plan(zone)
|
||||
self.assertEquals(15, len(filter(lambda c: isinstance(c, Create),
|
||||
plan.changes)))
|
||||
self.assertEquals(15, len([c for c in plan.changes
|
||||
if isinstance(c, Create)]))
|
||||
|
||||
yaml_file = join(zone_dir, '$unit.tests.yaml')
|
||||
self.assertTrue(isfile(yaml_file))
|
||||
@@ -371,4 +369,4 @@ class TestSplitYamlProvider(TestCase):
|
||||
with self.assertRaises(SubzoneRecordException) as ctx:
|
||||
source.populate(zone)
|
||||
self.assertEquals('Record www.sub.unit.tests. is under a managed '
|
||||
'subzone', ctx.exception.message)
|
||||
'subzone', text_type(ctx.exception))
|
||||
|
||||
+391
-30
@@ -5,12 +5,14 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \
|
||||
CnameRecord, Create, Delete, GeoValue, MxRecord, NaptrRecord, NaptrValue, \
|
||||
NsRecord, PtrRecord, Record, SshfpRecord, SpfRecord, SrvRecord, \
|
||||
TxtRecord, Update, ValidationError, _Dynamic, _DynamicPool, _DynamicRule
|
||||
CaaValue, CnameRecord, Create, Delete, GeoValue, 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
|
||||
@@ -482,113 +484,140 @@ class TestRecord(TestCase):
|
||||
# full sorting
|
||||
# equivalent
|
||||
b_naptr_value = b.values[0]
|
||||
self.assertEquals(0, b_naptr_value.__cmp__(b_naptr_value))
|
||||
self.assertTrue(b_naptr_value == b_naptr_value)
|
||||
self.assertFalse(b_naptr_value != b_naptr_value)
|
||||
self.assertTrue(b_naptr_value <= b_naptr_value)
|
||||
self.assertTrue(b_naptr_value >= b_naptr_value)
|
||||
# by order
|
||||
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
|
||||
self.assertTrue(b_naptr_value > NaptrValue({
|
||||
'order': 10,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
|
||||
}))
|
||||
self.assertTrue(b_naptr_value < NaptrValue({
|
||||
'order': 40,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
}))
|
||||
# by preference
|
||||
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
|
||||
self.assertTrue(b_naptr_value > NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 10,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
|
||||
}))
|
||||
self.assertTrue(b_naptr_value < NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 40,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
}))
|
||||
# by flags
|
||||
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
|
||||
self.assertTrue(b_naptr_value > NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'A',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
|
||||
}))
|
||||
self.assertTrue(b_naptr_value < NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'Z',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
}))
|
||||
# by service
|
||||
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
|
||||
self.assertTrue(b_naptr_value > NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'A',
|
||||
'regexp': 'O',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
|
||||
}))
|
||||
self.assertTrue(b_naptr_value < NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'Z',
|
||||
'regexp': 'O',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
}))
|
||||
# by regexp
|
||||
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
|
||||
self.assertTrue(b_naptr_value > NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'A',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
|
||||
}))
|
||||
self.assertTrue(b_naptr_value < NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'Z',
|
||||
'replacement': 'x',
|
||||
})))
|
||||
}))
|
||||
# by replacement
|
||||
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
|
||||
self.assertTrue(b_naptr_value > NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'a',
|
||||
})))
|
||||
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
|
||||
}))
|
||||
self.assertTrue(b_naptr_value < NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'z',
|
||||
})))
|
||||
}))
|
||||
|
||||
# __repr__ doesn't blow up
|
||||
a.__repr__()
|
||||
|
||||
# Hash
|
||||
v = NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 31,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'z',
|
||||
})
|
||||
o = NaptrValue({
|
||||
'order': 30,
|
||||
'preference': 32,
|
||||
'flags': 'M',
|
||||
'service': 'N',
|
||||
'regexp': 'O',
|
||||
'replacement': 'z',
|
||||
})
|
||||
values = set()
|
||||
values.add(v)
|
||||
self.assertTrue(v in values)
|
||||
self.assertFalse(o in values)
|
||||
values.add(o)
|
||||
self.assertTrue(o in values)
|
||||
|
||||
def test_ns(self):
|
||||
a_values = ['5.6.7.8.', '6.7.8.9.', '7.8.9.0.']
|
||||
a_data = {'ttl': 30, 'values': a_values}
|
||||
@@ -758,14 +787,14 @@ class TestRecord(TestCase):
|
||||
# Missing type
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Record.new(self.zone, 'unknown', {})
|
||||
self.assertTrue('missing type' in ctx.exception.message)
|
||||
self.assertTrue('missing type' in text_type(ctx.exception))
|
||||
|
||||
# Unknown type
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Record.new(self.zone, 'unknown', {
|
||||
'type': 'XXX',
|
||||
})
|
||||
self.assertTrue('Unknown record type' in ctx.exception.message)
|
||||
self.assertTrue('Unknown record type' in text_type(ctx.exception))
|
||||
|
||||
def test_change(self):
|
||||
existing = Record.new(self.zone, 'txt', {
|
||||
@@ -796,6 +825,38 @@ class TestRecord(TestCase):
|
||||
self.assertEquals(values, geo.values)
|
||||
self.assertEquals(['NA-US', 'NA'], list(geo.parents))
|
||||
|
||||
a = GeoValue('NA-US-CA', values)
|
||||
b = GeoValue('AP-JP', values)
|
||||
c = GeoValue('NA-US-CA', ['2.3.4.5'])
|
||||
|
||||
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 >= a)
|
||||
self.assertTrue(a >= b)
|
||||
self.assertTrue(a <= c)
|
||||
self.assertTrue(b <= a)
|
||||
self.assertTrue(b <= b)
|
||||
self.assertTrue(b <= c)
|
||||
self.assertTrue(c > a)
|
||||
self.assertTrue(c > b)
|
||||
self.assertTrue(c >= b)
|
||||
|
||||
def test_healthcheck(self):
|
||||
new = Record.new(self.zone, 'a', {
|
||||
'ttl': 44,
|
||||
@@ -851,6 +912,306 @@ class TestRecord(TestCase):
|
||||
})
|
||||
self.assertFalse(new.ignored)
|
||||
|
||||
def test_ordering_functions(self):
|
||||
a = Record.new(self.zone, 'a', {
|
||||
'ttl': 44,
|
||||
'type': 'A',
|
||||
'value': '1.2.3.4',
|
||||
})
|
||||
b = Record.new(self.zone, 'b', {
|
||||
'ttl': 44,
|
||||
'type': 'A',
|
||||
'value': '1.2.3.4',
|
||||
})
|
||||
c = Record.new(self.zone, 'c', {
|
||||
'ttl': 44,
|
||||
'type': 'A',
|
||||
'value': '1.2.3.4',
|
||||
})
|
||||
aaaa = Record.new(self.zone, 'a', {
|
||||
'ttl': 44,
|
||||
'type': 'AAAA',
|
||||
'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
|
||||
})
|
||||
|
||||
self.assertEquals(a, a)
|
||||
self.assertEquals(b, b)
|
||||
self.assertEquals(c, c)
|
||||
self.assertEquals(aaaa, aaaa)
|
||||
|
||||
self.assertNotEqual(a, b)
|
||||
self.assertNotEqual(a, c)
|
||||
self.assertNotEqual(a, aaaa)
|
||||
self.assertNotEqual(b, a)
|
||||
self.assertNotEqual(b, c)
|
||||
self.assertNotEqual(b, aaaa)
|
||||
self.assertNotEqual(c, a)
|
||||
self.assertNotEqual(c, b)
|
||||
self.assertNotEqual(c, aaaa)
|
||||
self.assertNotEqual(aaaa, a)
|
||||
self.assertNotEqual(aaaa, b)
|
||||
self.assertNotEqual(aaaa, c)
|
||||
|
||||
self.assertTrue(a < b)
|
||||
self.assertTrue(a < c)
|
||||
self.assertTrue(a < aaaa)
|
||||
self.assertTrue(b > a)
|
||||
self.assertTrue(b < c)
|
||||
self.assertTrue(b > aaaa)
|
||||
self.assertTrue(c > a)
|
||||
self.assertTrue(c > b)
|
||||
self.assertTrue(c > aaaa)
|
||||
self.assertTrue(aaaa > a)
|
||||
self.assertTrue(aaaa < b)
|
||||
self.assertTrue(aaaa < c)
|
||||
|
||||
self.assertTrue(a <= a)
|
||||
self.assertTrue(a <= b)
|
||||
self.assertTrue(a <= c)
|
||||
self.assertTrue(a <= aaaa)
|
||||
self.assertTrue(b >= a)
|
||||
self.assertTrue(b >= b)
|
||||
self.assertTrue(b <= c)
|
||||
self.assertTrue(b >= aaaa)
|
||||
self.assertTrue(c >= a)
|
||||
self.assertTrue(c >= b)
|
||||
self.assertTrue(c >= c)
|
||||
self.assertTrue(c >= aaaa)
|
||||
self.assertTrue(aaaa >= a)
|
||||
self.assertTrue(aaaa <= b)
|
||||
self.assertTrue(aaaa <= c)
|
||||
self.assertTrue(aaaa <= aaaa)
|
||||
|
||||
def test_caa_value(self):
|
||||
a = CaaValue({'flags': 0, 'tag': 'a', 'value': 'v'})
|
||||
b = CaaValue({'flags': 1, 'tag': 'a', 'value': 'v'})
|
||||
c = CaaValue({'flags': 0, 'tag': 'c', 'value': 'v'})
|
||||
d = CaaValue({'flags': 0, 'tag': 'a', 'value': 'z'})
|
||||
|
||||
self.assertEqual(a, a)
|
||||
self.assertEqual(b, b)
|
||||
self.assertEqual(c, c)
|
||||
self.assertEqual(d, d)
|
||||
|
||||
self.assertNotEqual(a, b)
|
||||
self.assertNotEqual(a, c)
|
||||
self.assertNotEqual(a, d)
|
||||
self.assertNotEqual(b, a)
|
||||
self.assertNotEqual(b, c)
|
||||
self.assertNotEqual(b, d)
|
||||
self.assertNotEqual(c, a)
|
||||
self.assertNotEqual(c, b)
|
||||
self.assertNotEqual(c, d)
|
||||
|
||||
self.assertTrue(a < b)
|
||||
self.assertTrue(a < c)
|
||||
self.assertTrue(a < d)
|
||||
|
||||
self.assertTrue(b > a)
|
||||
self.assertTrue(b > c)
|
||||
self.assertTrue(b > d)
|
||||
|
||||
self.assertTrue(c > a)
|
||||
self.assertTrue(c < b)
|
||||
self.assertTrue(c > d)
|
||||
|
||||
self.assertTrue(d > a)
|
||||
self.assertTrue(d < b)
|
||||
self.assertTrue(d < c)
|
||||
|
||||
self.assertTrue(a <= b)
|
||||
self.assertTrue(a <= c)
|
||||
self.assertTrue(a <= d)
|
||||
self.assertTrue(a <= a)
|
||||
self.assertTrue(a >= a)
|
||||
|
||||
self.assertTrue(b >= a)
|
||||
self.assertTrue(b >= c)
|
||||
self.assertTrue(b >= d)
|
||||
self.assertTrue(b >= b)
|
||||
self.assertTrue(b <= b)
|
||||
|
||||
self.assertTrue(c >= a)
|
||||
self.assertTrue(c <= b)
|
||||
self.assertTrue(c >= d)
|
||||
self.assertTrue(c >= c)
|
||||
self.assertTrue(c <= c)
|
||||
|
||||
self.assertTrue(d >= a)
|
||||
self.assertTrue(d <= b)
|
||||
self.assertTrue(d <= c)
|
||||
self.assertTrue(d >= d)
|
||||
self.assertTrue(d <= d)
|
||||
|
||||
def test_mx_value(self):
|
||||
a = MxValue({'preference': 0, 'priority': 'a', 'exchange': 'v',
|
||||
'value': '1'})
|
||||
b = MxValue({'preference': 10, 'priority': 'a', 'exchange': 'v',
|
||||
'value': '2'})
|
||||
c = MxValue({'preference': 0, 'priority': 'b', 'exchange': 'z',
|
||||
'value': '3'})
|
||||
|
||||
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)
|
||||
|
||||
def test_sshfp_value(self):
|
||||
a = SshfpValue({'algorithm': 0, 'fingerprint_type': 0,
|
||||
'fingerprint': 'abcd'})
|
||||
b = SshfpValue({'algorithm': 1, 'fingerprint_type': 0,
|
||||
'fingerprint': 'abcd'})
|
||||
c = SshfpValue({'algorithm': 0, 'fingerprint_type': 1,
|
||||
'fingerprint': 'abcd'})
|
||||
d = SshfpValue({'algorithm': 0, 'fingerprint_type': 0,
|
||||
'fingerprint': 'bcde'})
|
||||
|
||||
self.assertEqual(a, a)
|
||||
self.assertEqual(b, b)
|
||||
self.assertEqual(c, c)
|
||||
self.assertEqual(d, d)
|
||||
|
||||
self.assertNotEqual(a, b)
|
||||
self.assertNotEqual(a, c)
|
||||
self.assertNotEqual(a, d)
|
||||
self.assertNotEqual(b, a)
|
||||
self.assertNotEqual(b, c)
|
||||
self.assertNotEqual(b, d)
|
||||
self.assertNotEqual(c, a)
|
||||
self.assertNotEqual(c, b)
|
||||
self.assertNotEqual(c, d)
|
||||
self.assertNotEqual(d, a)
|
||||
self.assertNotEqual(d, b)
|
||||
self.assertNotEqual(d, c)
|
||||
|
||||
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_srv_value(self):
|
||||
a = SrvValue({'priority': 0, 'weight': 0, 'port': 0, 'target': 'foo.'})
|
||||
b = SrvValue({'priority': 1, 'weight': 0, 'port': 0, 'target': 'foo.'})
|
||||
c = SrvValue({'priority': 0, 'weight': 2, 'port': 0, 'target': 'foo.'})
|
||||
d = SrvValue({'priority': 0, 'weight': 0, 'port': 3, 'target': 'foo.'})
|
||||
e = SrvValue({'priority': 0, 'weight': 0, 'port': 0, 'target': 'mmm.'})
|
||||
|
||||
self.assertEqual(a, a)
|
||||
self.assertEqual(b, b)
|
||||
self.assertEqual(c, c)
|
||||
self.assertEqual(d, d)
|
||||
self.assertEqual(e, e)
|
||||
|
||||
self.assertNotEqual(a, b)
|
||||
self.assertNotEqual(a, c)
|
||||
self.assertNotEqual(a, d)
|
||||
self.assertNotEqual(a, e)
|
||||
self.assertNotEqual(b, a)
|
||||
self.assertNotEqual(b, c)
|
||||
self.assertNotEqual(b, d)
|
||||
self.assertNotEqual(b, e)
|
||||
self.assertNotEqual(c, a)
|
||||
self.assertNotEqual(c, b)
|
||||
self.assertNotEqual(c, d)
|
||||
self.assertNotEqual(c, e)
|
||||
self.assertNotEqual(d, a)
|
||||
self.assertNotEqual(d, b)
|
||||
self.assertNotEqual(d, c)
|
||||
self.assertNotEqual(d, e)
|
||||
self.assertNotEqual(e, a)
|
||||
self.assertNotEqual(e, b)
|
||||
self.assertNotEqual(e, c)
|
||||
self.assertNotEqual(e, d)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
class TestRecordValidation(TestCase):
|
||||
zone = Zone('unit.tests.', [])
|
||||
|
||||
@@ -9,6 +9,7 @@ import dns.zone
|
||||
from dns.exception import DNSException
|
||||
|
||||
from mock import patch
|
||||
from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.source.axfr import AxfrSource, AxfrSourceZoneTransferFailed, \
|
||||
@@ -38,7 +39,7 @@ class TestAxfrSource(TestCase):
|
||||
zone = Zone('unit.tests.', [])
|
||||
self.source.populate(zone)
|
||||
self.assertEquals('Unable to Perform Zone Transfer',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
|
||||
class TestZoneFileSource(TestCase):
|
||||
@@ -68,4 +69,4 @@ class TestZoneFileSource(TestCase):
|
||||
zone = Zone('invalid.zone.', [])
|
||||
self.source.populate(zone)
|
||||
self.assertEquals('The DNS zone has no NS RRset at its origin.',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from StringIO import StringIO
|
||||
from six import StringIO
|
||||
from unittest import TestCase
|
||||
from yaml.constructor import ConstructorError
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from unittest import TestCase
|
||||
from six import text_type
|
||||
|
||||
from octodns.record import ARecord, AaaaRecord, Create, Delete, Record, Update
|
||||
from octodns.zone import DuplicateRecordException, InvalidNodeException, \
|
||||
@@ -47,7 +48,7 @@ class TestZone(TestCase):
|
||||
with self.assertRaises(DuplicateRecordException) as ctx:
|
||||
zone.add_record(a)
|
||||
self.assertEquals('Duplicate record a.unit.tests., type A',
|
||||
ctx.exception.message)
|
||||
text_type(ctx.exception))
|
||||
self.assertEquals(zone.records, set([a]))
|
||||
|
||||
# can add duplicate with replace=True
|
||||
@@ -137,7 +138,7 @@ class TestZone(TestCase):
|
||||
def test_missing_dot(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Zone('not.allowed', [])
|
||||
self.assertTrue('missing ending dot' in ctx.exception.message)
|
||||
self.assertTrue('missing ending dot' in text_type(ctx.exception))
|
||||
|
||||
def test_sub_zones(self):
|
||||
|
||||
@@ -160,7 +161,7 @@ class TestZone(TestCase):
|
||||
})
|
||||
with self.assertRaises(SubzoneRecordException) as ctx:
|
||||
zone.add_record(record)
|
||||
self.assertTrue('not of type NS', ctx.exception.message)
|
||||
self.assertTrue('not of type NS', text_type(ctx.exception))
|
||||
# Can add it w/lenient
|
||||
zone.add_record(record, lenient=True)
|
||||
self.assertEquals(set([record]), zone.records)
|
||||
@@ -174,7 +175,7 @@ class TestZone(TestCase):
|
||||
})
|
||||
with self.assertRaises(SubzoneRecordException) as ctx:
|
||||
zone.add_record(record)
|
||||
self.assertTrue('under a managed sub-zone', ctx.exception.message)
|
||||
self.assertTrue('under a managed sub-zone', text_type(ctx.exception))
|
||||
# Can add it w/lenient
|
||||
zone.add_record(record, lenient=True)
|
||||
self.assertEquals(set([record]), zone.records)
|
||||
@@ -188,7 +189,7 @@ class TestZone(TestCase):
|
||||
})
|
||||
with self.assertRaises(SubzoneRecordException) as ctx:
|
||||
zone.add_record(record)
|
||||
self.assertTrue('under a managed sub-zone', ctx.exception.message)
|
||||
self.assertTrue('under a managed sub-zone', text_type(ctx.exception))
|
||||
# Can add it w/lenient
|
||||
zone.add_record(record, lenient=True)
|
||||
self.assertEquals(set([record]), zone.records)
|
||||
|
||||
Reference in New Issue
Block a user