Browse Source

Merge pull request #384 from github/python3-start

Start on supporting python3 using backwards compat bits for now
pull/415/head
Ross McFarland 6 years ago
committed by GitHub
parent
commit
895eb1b32b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1205 additions and 543 deletions
  1. +5
    -2
      .travis.yml
  2. +26
    -0
      CHANGELOG.md
  3. +5
    -3
      octodns/cmds/report.py
  4. +30
    -0
      octodns/equality.py
  5. +51
    -30
      octodns/manager.py
  6. +7
    -3
      octodns/provider/azuredns.py
  7. +4
    -2
      octodns/provider/base.py
  8. +1
    -1
      octodns/provider/cloudflare.py
  9. +2
    -1
      octodns/provider/constellix.py
  10. +2
    -2
      octodns/provider/dyn.py
  11. +1
    -1
      octodns/provider/fastdns.py
  12. +1
    -1
      octodns/provider/mythicbeasts.py
  13. +6
    -5
      octodns/provider/ns1.py
  14. +4
    -3
      octodns/provider/ovh.py
  15. +17
    -12
      octodns/provider/plan.py
  16. +8
    -7
      octodns/provider/rackspace.py
  17. +21
    -25
      octodns/provider/route53.py
  18. +1
    -1
      octodns/provider/transip.py
  19. +52
    -60
      octodns/record/__init__.py
  20. +2
    -2
      octodns/source/axfr.py
  21. +3
    -2
      octodns/source/tinydns.py
  22. +1
    -2
      octodns/yaml.py
  23. +5
    -3
      octodns/zone.py
  24. +0
    -2
      requirements-dev.txt
  25. +7
    -6
      requirements.txt
  26. +1
    -1
      script/coverage
  27. +7
    -3
      setup.py
  28. +28
    -0
      tests/config/provider-problems.yaml
  29. +0
    -16
      tests/config/unknown-provider.yaml
  30. +68
    -0
      tests/test_octodns_equality.py
  31. +45
    -42
      tests/test_octodns_manager.py
  32. +2
    -2
      tests/test_octodns_plan.py
  33. +17
    -16
      tests/test_octodns_provider_base.py
  34. +18
    -15
      tests/test_octodns_provider_cloudflare.py
  35. +8
    -2
      tests/test_octodns_provider_constellix.py
  36. +16
    -2
      tests/test_octodns_provider_digitalocean.py
  37. +28
    -2
      tests/test_octodns_provider_dnsimple.py
  38. +24
    -3
      tests/test_octodns_provider_dnsmadeeasy.py
  39. +2
    -2
      tests/test_octodns_provider_dyn.py
  40. +2
    -1
      tests/test_octodns_provider_fastdns.py
  41. +7
    -2
      tests/test_octodns_provider_googlecloud.py
  42. +16
    -16
      tests/test_octodns_provider_mythicbeasts.py
  43. +10
    -5
      tests/test_octodns_provider_ns1.py
  44. +47
    -48
      tests/test_octodns_provider_ovh.py
  45. +2
    -1
      tests/test_octodns_provider_powerdns.py
  46. +7
    -7
      tests/test_octodns_provider_rackspace.py
  47. +160
    -115
      tests/test_octodns_provider_route53.py
  48. +2
    -1
      tests/test_octodns_provider_selectel.py
  49. +2
    -2
      tests/test_octodns_provider_transip.py
  50. +23
    -25
      tests/test_octodns_provider_yaml.py
  51. +391
    -30
      tests/test_octodns_record.py
  52. +3
    -2
      tests/test_octodns_source_axfr.py
  53. +1
    -1
      tests/test_octodns_yaml.py
  54. +6
    -5
      tests/test_octodns_zone.py

+ 5
- 2
.travis.yml View File

@ -1,6 +1,9 @@
language: python language: python
python:
- 2.7
matrix:
include:
- python: 2.7
- python: 3.7
before_install: pip install --upgrade pip
script: ./script/cibuild script: ./script/cibuild
notifications: notifications:
email: email:


+ 26
- 0
CHANGELOG.md View File

@ -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 ## v0.9.8 - 2019-09-30 - One with no changes b/c PyPi description problems
* No material changes * No material changes


+ 5
- 3
octodns/cmds/report.py View File

@ -13,6 +13,8 @@ from logging import getLogger
from sys import stdout from sys import stdout
import re import re
from six import text_type
from octodns.cmds.args import ArgumentParser from octodns.cmds.args import ArgumentParser
from octodns.manager import Manager from octodns.manager import Manager
from octodns.zone import Zone from octodns.zone import Zone
@ -65,7 +67,7 @@ def main():
resolver = AsyncResolver(configure=False, resolver = AsyncResolver(configure=False,
num_workers=int(args.num_workers)) num_workers=int(args.num_workers))
if not ip_addr_re.match(server): 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) log.info('server=%s', server)
resolver.nameservers = [server] resolver.nameservers = [server]
resolver.lifetime = int(args.timeout) resolver.lifetime = int(args.timeout)
@ -81,12 +83,12 @@ def main():
stdout.write(',') stdout.write(',')
stdout.write(record._type) stdout.write(record._type)
stdout.write(',') stdout.write(',')
stdout.write(unicode(record.ttl))
stdout.write(text_type(record.ttl))
compare = {} compare = {}
for future in futures: for future in futures:
stdout.write(',') stdout.write(',')
try: try:
answers = [unicode(r) for r in future.result()]
answers = [text_type(r) for r in future.result()]
except (NoAnswer, NoNameservers): except (NoAnswer, NoNameservers):
answers = ['*no answer*'] answers = ['*no answer*']
except NXDOMAIN: except NXDOMAIN:


+ 30
- 0
octodns/equality.py View File

@ -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
octodns/manager.py View File

@ -69,6 +69,10 @@ class MainThreadExecutor(object):
return MakeThreadFuture(func, args, kwargs) return MakeThreadFuture(func, args, kwargs)
class ManagerException(Exception):
pass
class Manager(object): class Manager(object):
log = logging.getLogger('Manager') log = logging.getLogger('Manager')
@ -105,16 +109,16 @@ class Manager(object):
_class = provider_config.pop('class') _class = provider_config.pop('class')
except KeyError: except KeyError:
self.log.exception('Invalid provider class') 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) _class = self._get_named_class('provider', _class)
kwargs = self._build_kwargs(provider_config) kwargs = self._build_kwargs(provider_config)
try: try:
self.providers[provider_name] = _class(provider_name, **kwargs) self.providers[provider_name] = _class(provider_name, **kwargs)
except TypeError: except TypeError:
self.log.exception('Invalid provider config') 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 = {} zone_tree = {}
# sort by reversed strings so that parent zones always come first # sort by reversed strings so that parent zones always come first
@ -148,8 +152,8 @@ class Manager(object):
_class = plan_output_config.pop('class') _class = plan_output_config.pop('class')
except KeyError: except KeyError:
self.log.exception('Invalid plan_output class') 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) _class = self._get_named_class('plan_output', _class)
kwargs = self._build_kwargs(plan_output_config) kwargs = self._build_kwargs(plan_output_config)
try: try:
@ -157,8 +161,8 @@ class Manager(object):
_class(plan_output_name, **kwargs) _class(plan_output_name, **kwargs)
except TypeError: except TypeError:
self.log.exception('Invalid plan_output config') 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): def _get_named_class(self, _type, _class):
try: try:
@ -167,13 +171,15 @@ class Manager(object):
except (ImportError, ValueError): except (ImportError, ValueError):
self.log.exception('_get_{}_class: Unable to import ' self.log.exception('_get_{}_class: Unable to import '
'module %s', _class) 'module %s', _class)
raise Exception('Unknown {} class: {}'.format(_type, _class))
raise ManagerException('Unknown {} class: {}'
.format(_type, _class))
try: try:
return getattr(module, class_name) return getattr(module, class_name)
except AttributeError: except AttributeError:
self.log.exception('_get_{}_class: Unable to get class %s ' self.log.exception('_get_{}_class: Unable to get class %s '
'from module %s', class_name, module) '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): def _build_kwargs(self, source):
# Build up the arguments we need to pass to the provider # Build up the arguments we need to pass to the provider
@ -186,9 +192,9 @@ class Manager(object):
v = environ[env_var] v = environ[env_var]
except KeyError: except KeyError:
self.log.exception('Invalid provider config') 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: except AttributeError:
pass pass
kwargs[k] = v kwargs[k] = v
@ -248,7 +254,7 @@ class Manager(object):
zones = self.config['zones'].items() zones = self.config['zones'].items()
if eligible_zones: 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 = [] futures = []
for zone_name, config in zones: for zone_name, config in zones:
@ -256,14 +262,16 @@ class Manager(object):
try: try:
sources = config['sources'] sources = config['sources']
except KeyError: except KeyError:
raise Exception('Zone {} is missing sources'.format(zone_name))
raise ManagerException('Zone {} is missing sources'
.format(zone_name))
try: try:
targets = config['targets'] targets = config['targets']
except KeyError: except KeyError:
raise Exception('Zone {} is missing targets'.format(zone_name))
raise ManagerException('Zone {} is missing targets'
.format(zone_name))
if eligible_targets: 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: if not targets:
# Don't bother planning (and more importantly populating) zones # 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) self.log.info('sync: sources=%s -> targets=%s', sources, targets)
try: 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: except KeyError:
raise Exception('Zone {}, unknown source: {}'.format(zone_name,
source))
raise ManagerException('Zone {}, unknown source: {}'
.format(zone_name, source))
try: try:
trgs = [] trgs = []
for target in targets: for target in targets:
trg = self.providers[target] trg = self.providers[target]
if not isinstance(trg, BaseProvider): 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) trgs.append(trg)
targets = trgs targets = trgs
except KeyError: 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, futures.append(self._executor.submit(self._populate_and_plan,
zone_name, sources, targets)) zone_name, sources, targets))
@ -344,7 +358,7 @@ class Manager(object):
a = [self.providers[source] for source in a] a = [self.providers[source] for source in a]
b = [self.providers[source] for source in b] b = [self.providers[source] for source in b]
except KeyError as e: 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) sub_zones = self.configured_sub_zones(zone)
za = Zone(zone, sub_zones) za = Zone(zone, sub_zones)
@ -370,7 +384,7 @@ class Manager(object):
try: try:
sources = [self.providers[s] for s in sources] sources = [self.providers[s] for s in sources]
except KeyError as e: except KeyError as e:
raise Exception('Unknown source: {}'.format(e.args[0]))
raise ManagerException('Unknown source: {}'.format(e.args[0]))
clz = YamlProvider clz = YamlProvider
if split: if split:
@ -393,13 +407,20 @@ class Manager(object):
try: try:
sources = config['sources'] sources = config['sources']
except KeyError: except KeyError:
raise Exception('Zone {} is missing sources'.format(zone_name))
raise ManagerException('Zone {} is missing sources'
.format(zone_name))
try: 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: except KeyError:
raise Exception('Zone {}, unknown source: {}'.format(zone_name,
source))
raise ManagerException('Zone {}, unknown source: {}'
.format(zone_name, source))
for source in sources: for source in sources:
if isinstance(source, YamlProvider): if isinstance(source, YamlProvider):


+ 7
- 3
octodns/provider/azuredns.py View File

@ -175,6 +175,10 @@ class _AzureRecord(object):
:type return: bool :type return: bool
''' '''
def key_dict(d):
return sum([hash('{}:{}'.format(k, v)) for k, v in d.items()])
def parse_dict(params): def parse_dict(params):
vals = [] vals = []
for char in params: for char in params:
@ -185,7 +189,7 @@ class _AzureRecord(object):
vals.append(record.__dict__) vals.append(record.__dict__)
except: except:
vals.append(list_records.__dict__) vals.append(list_records.__dict__)
vals.sort()
vals.sort(key=key_dict)
return vals return vals
return (self.resource_group == b.resource_group) & \ return (self.resource_group == b.resource_group) & \
@ -373,13 +377,13 @@ class AzureProvider(BaseProvider):
self._populate_zones() self._populate_zones()
self._check_zone(zone_name) self._check_zone(zone_name)
_records = set()
_records = []
records = self._dns_client.record_sets.list_by_dns_zone records = self._dns_client.record_sets.list_by_dns_zone
if self._check_zone(zone_name): if self._check_zone(zone_name):
exists = True exists = True
for azrecord in records(self._resource_group, zone_name): for azrecord in records(self._resource_group, zone_name):
if _parse_azure_type(azrecord.type) in self.SUPPORTS: if _parse_azure_type(azrecord.type) in self.SUPPORTS:
_records.add(azrecord)
_records.append(azrecord)
for azrecord in _records: for azrecord in _records:
record_name = azrecord.name if azrecord.name != '@' else '' record_name = azrecord.name if azrecord.name != '@' else ''
typ = _parse_azure_type(azrecord.type) typ = _parse_azure_type(azrecord.type)


+ 4
- 2
octodns/provider/base.py View File

@ -5,6 +5,8 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from six import text_type
from ..source.base import BaseSource from ..source.base import BaseSource
from ..zone import Zone from ..zone import Zone
from .plan import Plan from .plan import Plan
@ -58,7 +60,7 @@ class BaseProvider(BaseSource):
# allow the provider to filter out false positives # allow the provider to filter out false positives
before = len(changes) before = len(changes)
changes = filter(self._include_change, changes)
changes = [c for c in changes if self._include_change(c)]
after = len(changes) after = len(changes)
if before != after: if before != after:
self.log.info('plan: filtered out %s changes', before - after) self.log.info('plan: filtered out %s changes', before - after)
@ -68,7 +70,7 @@ class BaseProvider(BaseSource):
changes=changes) changes=changes)
if extra: if extra:
self.log.info('plan: extra changes\n %s', '\n ' 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 changes += extra
if changes: if changes:


+ 1
- 1
octodns/provider/cloudflare.py View File

@ -442,7 +442,7 @@ class CloudflareProvider(BaseProvider):
# Round trip the single value through a record to contents flow # Round trip the single value through a record to contents flow
# to get a consistent _gen_data result that matches what # to get a consistent _gen_data result that matches what
# went in to new_contents # 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 # Record the record_id and data for this existing record
key = self._gen_key(data) key = self._gen_key(data)


+ 2
- 1
octodns/provider/constellix.py View File

@ -9,6 +9,7 @@ from collections import defaultdict
from requests import Session from requests import Session
from base64 import b64encode from base64 import b64encode
from ipaddress import ip_address from ipaddress import ip_address
from six import string_types
import hashlib import hashlib
import hmac import hmac
import logging import logging
@ -122,7 +123,7 @@ class ConstellixClient(object):
# change relative values to absolute # change relative values to absolute
value = record['value'] value = record['value']
if record['type'] in ['ALIAS', 'CNAME', 'MX', 'NS', 'SRV']: if record['type'] in ['ALIAS', 'CNAME', 'MX', 'NS', 'SRV']:
if isinstance(value, unicode):
if isinstance(value, string_types):
record['value'] = self._absolutize_value(value, record['value'] = self._absolutize_value(value,
zone_name) zone_name)
if isinstance(value, list): if isinstance(value, list):


+ 2
- 2
octodns/provider/dyn.py View File

@ -903,10 +903,10 @@ class DynProvider(BaseProvider):
# Sort the values for consistent ordering so that we can compare # Sort the values for consistent ordering so that we can compare
values = sorted(values, key=_dynamic_value_sort_key) values = sorted(values, key=_dynamic_value_sort_key)
# Ensure that weight is included and if not use the default # Ensure that weight is included and if not use the default
values = map(lambda v: {
values = [{
'value': v['value'], 'value': v['value'],
'weight': v.get('weight', 1), 'weight': v.get('weight', 1),
}, values)
} for v in values]
# Walk through our existing pools looking for a match we can use # Walk through our existing pools looking for a match we can use
for pool in pools: for pool in pools:


+ 1
- 1
octodns/provider/fastdns.py View File

@ -7,7 +7,7 @@ from __future__ import absolute_import, division, print_function, \
from requests import Session from requests import Session
from akamai.edgegrid import EdgeGridAuth from akamai.edgegrid import EdgeGridAuth
from urlparse import urljoin
from six.moves.urllib.parse import urljoin
from collections import defaultdict from collections import defaultdict
from logging import getLogger from logging import getLogger


+ 1
- 1
octodns/provider/mythicbeasts.py View File

@ -328,7 +328,7 @@ class MythicBeastsProvider(BaseProvider):
exists = True exists = True
for line in resp.content.splitlines(): 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: if match is None:
self.log.debug('failed to match line: %s', line) self.log.debug('failed to match line: %s', line)


+ 6
- 5
octodns/provider/ns1.py View File

@ -10,9 +10,11 @@ from itertools import chain
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from ns1 import NS1 from ns1 import NS1
from ns1.rest.errors import RateLimitException, ResourceException 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 time import sleep
from six import text_type
from ..record import Record from ..record import Record
from .base import BaseProvider from .base import BaseProvider
@ -60,8 +62,7 @@ class Ns1Provider(BaseProvider):
us_state = meta.get('us_state', []) us_state = meta.get('us_state', [])
ca_province = meta.get('ca_province', []) ca_province = meta.get('ca_province', [])
for cntry in country: 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) key = '{}-{}'.format(con, cntry)
geo[key].extend(answer['answer']) geo[key].extend(answer['answer'])
for state in us_state: for state in us_state:
@ -76,9 +77,9 @@ class Ns1Provider(BaseProvider):
else: else:
values.extend(answer['answer']) values.extend(answer['answer'])
codes.append([]) codes.append([])
values = [unicode(x) for x in values]
values = [text_type(x) for x in values]
geo = OrderedDict( 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['values'] = values
data['geo'] = geo data['geo'] = geo


+ 4
- 3
octodns/provider/ovh.py View File

@ -9,6 +9,7 @@ import base64
import binascii import binascii
import logging import logging
from collections import defaultdict from collections import defaultdict
from six import text_type
import ovh import ovh
from ovh import ResourceNotFoundError from ovh import ResourceNotFoundError
@ -64,7 +65,7 @@ class OvhProvider(BaseProvider):
records = self.get_records(zone_name=zone_name) records = self.get_records(zone_name=zone_name)
exists = True exists = True
except ResourceNotFoundError as e: except ResourceNotFoundError as e:
if e.message != self.ZONE_NOT_FOUND_MESSAGE:
if text_type(e) != self.ZONE_NOT_FOUND_MESSAGE:
raise raise
exists = False exists = False
records = [] records = []
@ -325,7 +326,7 @@ class OvhProvider(BaseProvider):
splitted = value.split('\\;') splitted = value.split('\\;')
found_key = False found_key = False
for splitted_value in splitted: 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: if len(sub_split) < 2:
return False return False
key, value = sub_split[0], sub_split[1] key, value = sub_split[0], sub_split[1]
@ -343,7 +344,7 @@ class OvhProvider(BaseProvider):
@staticmethod @staticmethod
def _is_valid_dkim_key(key): def _is_valid_dkim_key(key):
try: try:
base64.decodestring(key)
base64.decodestring(bytearray(key, 'utf-8'))
except binascii.Error: except binascii.Error:
return False return False
return True return True


+ 17
- 12
octodns/provider/plan.py View File

@ -5,10 +5,11 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from StringIO import StringIO
from logging import DEBUG, ERROR, INFO, WARN, getLogger from logging import DEBUG, ERROR, INFO, WARN, getLogger
from sys import stdout from sys import stdout
from six import StringIO, text_type
class UnsafePlan(Exception): class UnsafePlan(Exception):
pass pass
@ -26,7 +27,11 @@ class Plan(object):
delete_pcent_threshold=MAX_SAFE_DELETE_PCENT): delete_pcent_threshold=MAX_SAFE_DELETE_PCENT):
self.existing = existing self.existing = existing
self.desired = desired 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.exists = exists
self.update_pcent_threshold = update_pcent_threshold self.update_pcent_threshold = update_pcent_threshold
self.delete_pcent_threshold = delete_pcent_threshold self.delete_pcent_threshold = delete_pcent_threshold
@ -122,7 +127,7 @@ class PlanLogger(_PlanOutput):
buf.write('* ') buf.write('* ')
buf.write(target.id) buf.write(target.id)
buf.write(' (') buf.write(' (')
buf.write(target)
buf.write(text_type(target))
buf.write(')\n* ') buf.write(')\n* ')
if plan.exists is False: if plan.exists is False:
@ -135,7 +140,7 @@ class PlanLogger(_PlanOutput):
buf.write('\n* ') buf.write('\n* ')
buf.write('Summary: ') buf.write('Summary: ')
buf.write(plan)
buf.write(text_type(plan))
buf.write('\n') buf.write('\n')
else: else:
buf.write(hr) buf.write(hr)
@ -147,11 +152,11 @@ class PlanLogger(_PlanOutput):
def _value_stringifier(record, sep): def _value_stringifier(record, sep):
try: try:
values = [unicode(v) for v in record.values]
values = [text_type(v) for v in record.values]
except AttributeError: except AttributeError:
values = [record.value] values = [record.value]
for code, gv in sorted(getattr(record, 'geo', {}).items()): 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)) values.append('{}: {}'.format(code, vs))
return sep.join(values) return sep.join(values)
@ -193,7 +198,7 @@ class PlanMarkdown(_PlanOutput):
fh.write(' | ') fh.write(' | ')
# TTL # TTL
if existing: if existing:
fh.write(unicode(existing.ttl))
fh.write(text_type(existing.ttl))
fh.write(' | ') fh.write(' | ')
fh.write(_value_stringifier(existing, '; ')) fh.write(_value_stringifier(existing, '; '))
fh.write(' | |\n') fh.write(' | |\n')
@ -201,7 +206,7 @@ class PlanMarkdown(_PlanOutput):
fh.write('| | | | ') fh.write('| | | | ')
if new: if new:
fh.write(unicode(new.ttl))
fh.write(text_type(new.ttl))
fh.write(' | ') fh.write(' | ')
fh.write(_value_stringifier(new, '; ')) fh.write(_value_stringifier(new, '; '))
fh.write(' | ') fh.write(' | ')
@ -210,7 +215,7 @@ class PlanMarkdown(_PlanOutput):
fh.write(' |\n') fh.write(' |\n')
fh.write('\nSummary: ') fh.write('\nSummary: ')
fh.write(unicode(plan))
fh.write(text_type(plan))
fh.write('\n\n') fh.write('\n\n')
else: else:
fh.write('## No changes were planned\n') fh.write('## No changes were planned\n')
@ -261,7 +266,7 @@ class PlanHtml(_PlanOutput):
# TTL # TTL
if existing: if existing:
fh.write(' <td>') fh.write(' <td>')
fh.write(unicode(existing.ttl))
fh.write(text_type(existing.ttl))
fh.write('</td>\n <td>') fh.write('</td>\n <td>')
fh.write(_value_stringifier(existing, '<br/>')) fh.write(_value_stringifier(existing, '<br/>'))
fh.write('</td>\n <td></td>\n </tr>\n') fh.write('</td>\n <td></td>\n </tr>\n')
@ -270,7 +275,7 @@ class PlanHtml(_PlanOutput):
if new: if new:
fh.write(' <td>') fh.write(' <td>')
fh.write(unicode(new.ttl))
fh.write(text_type(new.ttl))
fh.write('</td>\n <td>') fh.write('</td>\n <td>')
fh.write(_value_stringifier(new, '<br/>')) fh.write(_value_stringifier(new, '<br/>'))
fh.write('</td>\n <td>') fh.write('</td>\n <td>')
@ -279,7 +284,7 @@ class PlanHtml(_PlanOutput):
fh.write('</td>\n </tr>\n') fh.write('</td>\n </tr>\n')
fh.write(' <tr>\n <td colspan=6>Summary: ') 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') fh.write('</td>\n </tr>\n</table>\n')
else: else:
fh.write('<b>No changes were planned</b>') fh.write('<b>No changes were planned</b>')

+ 8
- 7
octodns/provider/rackspace.py View File

@ -7,13 +7,16 @@ from __future__ import absolute_import, division, print_function, \
from requests import HTTPError, Session, post from requests import HTTPError, Session, post
from collections import defaultdict from collections import defaultdict
import logging import logging
import string
import time import time
from ..record import Record from ..record import Record
from .base import BaseProvider from .base import BaseProvider
def _value_keyer(v):
return (v.get('type', ''), v['name'], v.get('data', ''))
def add_trailing_dot(s): def add_trailing_dot(s):
assert s assert s
assert s[-1] != '.' assert s[-1] != '.'
@ -28,12 +31,12 @@ def remove_trailing_dot(s):
def escape_semicolon(s): def escape_semicolon(s):
assert s assert s
return string.replace(s, ';', '\\;')
return s.replace(';', '\\;')
def unescape_semicolon(s): def unescape_semicolon(s):
assert s assert s
return string.replace(s, '\\;', ';')
return s.replace('\\;', ';')
class RackspaceProvider(BaseProvider): class RackspaceProvider(BaseProvider):
@ -367,11 +370,9 @@ class RackspaceProvider(BaseProvider):
self._delete('domains/{}/records?{}'.format(domain_id, params)) self._delete('domains/{}/records?{}'.format(domain_id, params))
if updates: 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) self._put('domains/{}/records'.format(domain_id), data=data)
if creates: 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) self._post('domains/{}/records'.format(domain_id), data=data)

+ 21
- 25
octodns/provider/route53.py View File

@ -8,17 +8,19 @@ from __future__ import absolute_import, division, print_function, \
from boto3 import client from boto3 import client
from botocore.config import Config from botocore.config import Config
from collections import defaultdict from collections import defaultdict
from incf.countryutils.transformations import cca_to_ctca2
from ipaddress import AddressValueError, ip_address from ipaddress import AddressValueError, ip_address
from pycountry_convert import country_alpha2_to_continent_code
from uuid import uuid4 from uuid import uuid4
import logging import logging
import re import re
from six import text_type
from ..equality import EqualityTupleMixin
from ..record import Record, Update from ..record import Record, Update
from ..record.geo import GeoCodes from ..record.geo import GeoCodes
from .base import BaseProvider from .base import BaseProvider
octal_re = re.compile(r'\\(\d\d\d)') 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) return octal_re.sub(lambda m: chr(int(m.group(1), 8)), s)
class _Route53Record(object):
class _Route53Record(EqualityTupleMixin):
@classmethod @classmethod
def _new_dynamic(cls, provider, record, hosted_zone_id, creating): 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. # _Route53Records equivalent if they have the same class, fqdn, and _type.
# Values are ignored. This is useful when computing diffs/changes. # Values are ignored. This is useful when computing diffs/changes.
@ -155,17 +157,10 @@ class _Route53Record(object):
'sub-classes should never use this method' 'sub-classes should never use this method'
return '{}:{}'.format(self.fqdn, self._type).__hash__() 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): def __repr__(self):
return '_Route53Record<{} {} {} {}>'.format(self.fqdn, self._type, return '_Route53Record<{} {} {} {}>'.format(self.fqdn, self._type,
@ -506,11 +501,9 @@ class _Route53GeoRecord(_Route53Record):
return '{}:{}:{}'.format(self.fqdn, self._type, return '{}:{}:{}'.format(self.fqdn, self._type,
self.geo.code).__hash__() 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): def __repr__(self):
return '_Route53GeoRecord<{} {} {} {} {}>'.format(self.fqdn, return '_Route53GeoRecord<{} {} {} {} {}>'.format(self.fqdn,
@ -553,7 +546,10 @@ def _mod_keyer(mod):
if rrset.get('GeoLocation', False): if rrset.get('GeoLocation', False):
unique_id = rrset['SetIdentifier'] unique_id = rrset['SetIdentifier']
else: 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. # Prioritise within the action_priority, ensuring targets come first.
if rrset.get('GeoLocation', False): if rrset.get('GeoLocation', False):
@ -700,7 +696,7 @@ class Route53Provider(BaseProvider):
if cc == '*': if cc == '*':
# This is the default # This is the default
return return
cn = cca_to_ctca2(cc)
cn = country_alpha2_to_continent_code(cc)
try: try:
return '{}-{}-{}'.format(cn, cc, loc['SubdivisionCode']) return '{}-{}-{}'.format(cn, cc, loc['SubdivisionCode'])
except KeyError: except KeyError:
@ -1037,8 +1033,8 @@ class Route53Provider(BaseProvider):
# ip_address's returned object for equivalence # ip_address's returned object for equivalence
# E.g 2001:4860:4860::8842 -> 2001:4860:4860:0:0:0:0:8842 # E.g 2001:4860:4860::8842 -> 2001:4860:4860:0:0:0:0:8842
if value: 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: else:
# No value so give this a None to match value's # No value so give this a None to match value's
config_ip_address = None config_ip_address = None
@ -1059,7 +1055,7 @@ class Route53Provider(BaseProvider):
fqdn, record._type, value) fqdn, record._type, value)
try: try:
ip_address(unicode(value))
ip_address(text_type(value))
# We're working with an IP, host is the Host header # We're working with an IP, host is the Host header
healthcheck_host = record.healthcheck_host healthcheck_host = record.healthcheck_host
except (AddressValueError, ValueError): except (AddressValueError, ValueError):


+ 1
- 1
octodns/provider/transip.py View File

@ -137,7 +137,7 @@ class TransipProvider(BaseProvider):
try: try:
self._client.get_info(plan.desired.name[:-1]) self._client.get_info(plan.desired.name[:-1])
except WebFault as e: except WebFault as e:
self.log.warning('_apply: %s ', e.message)
self.log.exception('_apply: get_info failed')
raise e raise e
_dns_entries = [] _dns_entries = []


+ 52
- 60
octodns/record/__init__.py View File

@ -9,6 +9,9 @@ from ipaddress import IPv4Address, IPv6Address
from logging import getLogger from logging import getLogger
import re import re
from six import string_types, text_type
from ..equality import EqualityTupleMixin
from .geo import GeoCodes from .geo import GeoCodes
@ -23,6 +26,12 @@ class Change(object):
'Returns new if we have one, existing otherwise' 'Returns new if we have one, existing otherwise'
return self.new or self.existing 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): class Create(Change):
@ -68,7 +77,7 @@ class ValidationError(Exception):
self.reasons = reasons self.reasons = reasons
class Record(object):
class Record(EqualityTupleMixin):
log = getLogger('Record') log = getLogger('Record')
@classmethod @classmethod
@ -130,7 +139,7 @@ class Record(object):
self.__class__.__name__, name) self.__class__.__name__, name)
self.zone = zone self.zone = zone
# force everything lower-case just to be safe # 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.source = source
self.ttl = int(data['ttl']) self.ttl = int(data['ttl'])
@ -194,24 +203,22 @@ class Record(object):
if self.ttl != other.ttl: if self.ttl != other.ttl:
return Update(self, other) 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 # equivalent if they have the same name & _type. Values are ignored. This
# is useful when computing diffs/changes. # is useful when computing diffs/changes.
def __hash__(self): def __hash__(self):
return '{}:{}'.format(self.name, self._type).__hash__() 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): def __repr__(self):
# Make sure this is always overridden # Make sure this is always overridden
raise NotImplementedError('Abstract base class, __repr__ required') 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)' geo_re = re.compile(r'^(?P<continent_code>\w\w)(-(?P<country_code>\w\w)'
r'(-(?P<subdivision_code>\w\w))?)?$') r'(-(?P<subdivision_code>\w\w))?)?$')
@ -238,11 +245,9 @@ class GeoValue(object):
yield '-'.join(bits) yield '-'.join(bits)
bits.pop() 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): def __repr__(self):
return "'Geo {} {} {} {}'".format(self.continent_code, return "'Geo {} {} {} {}'".format(self.continent_code,
@ -268,7 +273,6 @@ class _ValuesMixin(object):
values = data['values'] values = data['values']
except KeyError: except KeyError:
values = [data['value']] values = [data['value']]
# TODO: should we natsort values?
self.values = sorted(self._value_type.process(values)) self.values = sorted(self._value_type.process(values))
def changes(self, other, target): def changes(self, other, target):
@ -292,7 +296,7 @@ class _ValuesMixin(object):
return ret return ret
def __repr__(self): def __repr__(self):
values = "['{}']".format("', '".join([unicode(v)
values = "['{}']".format("', '".join([text_type(v)
for v in self.values])) for v in self.values]))
return '<{} {} {}, {}, {}>'.format(self.__class__.__name__, return '<{} {} {}, {}, {}>'.format(self.__class__.__name__,
self._type, self.ttl, self._type, self.ttl,
@ -574,7 +578,7 @@ class _DynamicMixin(object):
reasons.append('rule {} missing pool'.format(rule_num)) reasons.append('rule {} missing pool'.format(rule_num))
continue continue
if not isinstance(pool, basestring):
if not isinstance(pool, string_types):
reasons.append('rule {} invalid pool "{}"' reasons.append('rule {} invalid pool "{}"'
.format(rule_num, pool)) .format(rule_num, pool))
elif pool not in pools: elif pool not in pools:
@ -671,13 +675,13 @@ class _IpList(object):
return ['missing value(s)'] return ['missing value(s)']
reasons = [] reasons = []
for value in data: for value in data:
if value is '':
if value == '':
reasons.append('empty value') reasons.append('empty value')
elif value is None: elif value is None:
reasons.append('missing value(s)') reasons.append('missing value(s)')
else: else:
try: try:
cls._address_type(unicode(value))
cls._address_type(text_type(value))
except Exception: except Exception:
reasons.append('invalid {} address "{}"' reasons.append('invalid {} address "{}"'
.format(cls._address_name, value)) .format(cls._address_name, value))
@ -685,7 +689,8 @@ class _IpList(object):
@classmethod @classmethod
def process(cls, values): 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): class Ipv4List(_IpList):
@ -742,7 +747,7 @@ class AliasRecord(_ValueMixin, Record):
_value_type = AliasValue _value_type = AliasValue
class CaaValue(object):
class CaaValue(EqualityTupleMixin):
# https://tools.ietf.org/html/rfc6844#page-5 # https://tools.ietf.org/html/rfc6844#page-5
@classmethod @classmethod
@ -781,12 +786,8 @@ class CaaValue(object):
'value': self.value, '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): def __repr__(self):
return '{} {} "{}"'.format(self.flags, self.tag, self.value) return '{} {} "{}"'.format(self.flags, self.tag, self.value)
@ -810,7 +811,7 @@ class CnameRecord(_DynamicMixin, _ValueMixin, Record):
return reasons return reasons
class MxValue(object):
class MxValue(EqualityTupleMixin):
@classmethod @classmethod
def validate(cls, data, _type): def validate(cls, data, _type):
@ -863,10 +864,11 @@ class MxValue(object):
'exchange': self.exchange, '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): def __repr__(self):
return "'{} {}'".format(self.preference, self.exchange) return "'{} {}'".format(self.preference, self.exchange)
@ -877,7 +879,7 @@ class MxRecord(_ValuesMixin, Record):
_value_type = MxValue _value_type = MxValue
class NaptrValue(object):
class NaptrValue(EqualityTupleMixin):
VALID_FLAGS = ('S', 'A', 'U', 'P') VALID_FLAGS = ('S', 'A', 'U', 'P')
@classmethod @classmethod
@ -936,18 +938,12 @@ class NaptrValue(object):
'replacement': self.replacement, '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): def __repr__(self):
flags = self.flags if self.flags is not None else '' flags = self.flags if self.flags is not None else ''
@ -997,7 +993,7 @@ class PtrRecord(_ValueMixin, Record):
_value_type = PtrValue _value_type = PtrValue
class SshfpValue(object):
class SshfpValue(EqualityTupleMixin):
VALID_ALGORITHMS = (1, 2, 3, 4) VALID_ALGORITHMS = (1, 2, 3, 4)
VALID_FINGERPRINT_TYPES = (1, 2) VALID_FINGERPRINT_TYPES = (1, 2)
@ -1048,12 +1044,11 @@ class SshfpValue(object):
'fingerprint': self.fingerprint, '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): def __repr__(self):
return "'{} {} {}'".format(self.algorithm, self.fingerprint_type, return "'{} {} {}'".format(self.algorithm, self.fingerprint_type,
@ -1114,7 +1109,7 @@ class SpfRecord(_ChunkedValuesMixin, Record):
_value_type = _ChunkedValue _value_type = _ChunkedValue
class SrvValue(object):
class SrvValue(EqualityTupleMixin):
@classmethod @classmethod
def validate(cls, data, _type): def validate(cls, data, _type):
@ -1169,14 +1164,11 @@ class SrvValue(object):
'target': self.target, '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): def __repr__(self):
return "'{} {} {} {}'".format(self.priority, self.weight, self.port, return "'{} {} {} {}'".format(self.priority, self.weight, self.port,


+ 2
- 2
octodns/source/axfr.py View File

@ -15,6 +15,7 @@ from dns.exception import DNSException
from collections import defaultdict from collections import defaultdict
from os import listdir from os import listdir
from os.path import join from os.path import join
from six import text_type
import logging import logging
from ..record import Record from ..record import Record
@ -179,8 +180,7 @@ class ZoneFileSourceNotFound(ZoneFileSourceException):
class ZoneFileSourceLoadFailure(ZoneFileSourceException): class ZoneFileSourceLoadFailure(ZoneFileSourceException):
def __init__(self, error): def __init__(self, error):
super(ZoneFileSourceLoadFailure, self).__init__(
error.message)
super(ZoneFileSourceLoadFailure, self).__init__(text_type(error))
class ZoneFileSource(AxfrBaseSource): class ZoneFileSource(AxfrBaseSource):


+ 3
- 2
octodns/source/tinydns.py View File

@ -67,7 +67,8 @@ class TinyDnsBaseSource(BaseSource):
values = [] values = []
for record in records: 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) values.append(new_value)
try: try:
@ -252,7 +253,7 @@ class TinyDnsFileSource(TinyDnsBaseSource):
# Ignore hidden files # Ignore hidden files
continue continue
with open(join(self.directory, filename), 'r') as fh: 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 self._cache = lines


+ 1
- 2
octodns/yaml.py View File

@ -49,8 +49,7 @@ class SortingDumper(SafeDumper):
''' '''
def _representer(self, data): 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) return self.represent_mapping(self.DEFAULT_MAPPING_TAG, data)


+ 5
- 3
octodns/zone.py View File

@ -9,6 +9,8 @@ from collections import defaultdict
from logging import getLogger from logging import getLogger
import re import re
from six import text_type
from .record import Create, Delete from .record import Create, Delete
@ -38,7 +40,7 @@ class Zone(object):
raise Exception('Invalid zone name {}, missing ending dot' raise Exception('Invalid zone name {}, missing ending dot'
.format(name)) .format(name))
# Force everything to lowercase just to be safe # 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 self.sub_zones = sub_zones
# We're grouping by node, it allows us to efficiently search for # We're grouping by node, it allows us to efficiently search for
# duplicates and detect when CNAMEs co-exist with other records # duplicates and detect when CNAMEs co-exist with other records
@ -82,8 +84,8 @@ class Zone(object):
raise DuplicateRecordException('Duplicate record {}, type {}' raise DuplicateRecordException('Duplicate record {}, type {}'
.format(record.fqdn, .format(record.fqdn,
record._type)) 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 # We're adding a CNAME to existing records or adding to an existing
# CNAME # CNAME
raise InvalidNodeException('Invalid state, CNAME at {} cannot ' raise InvalidNodeException('Invalid state, CNAME at {} cannot '


+ 0
- 2
requirements-dev.txt View File

@ -2,8 +2,6 @@ coverage
mock mock
nose nose
pycodestyle==2.4.0 pycodestyle==2.4.0
pycountry>=18.12.8
pycountry_convert>=0.7.2
pyflakes==1.6.0 pyflakes==1.6.0
readme_renderer[md]==24.0 readme_renderer[md]==24.0
requests_mock requests_mock


+ 7
- 6
requirements.txt View File

@ -1,25 +1,26 @@
PyYaml==4.2b1 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 boto3==1.7.5
botocore==1.10.5 botocore==1.10.5
dnspython==1.15.0 dnspython==1.15.0
docutils==0.14 docutils==0.14
dyn==1.8.1 dyn==1.8.1
edgegrid-python==1.1.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-core==0.28.1
google-cloud-dns==0.29.0 google-cloud-dns==0.29.0
incf.countryutils==1.0
ipaddress==1.0.22 ipaddress==1.0.22
jmespath==0.9.3 jmespath==0.9.3
msrestazure==0.6.0
msrestazure==0.6.2
natsort==5.5.0 natsort==5.5.0
ns1-python==0.12.0 ns1-python==0.12.0
ovh==0.4.8 ovh==0.4.8
pycountry-convert==0.7.2
pycountry==19.8.18
python-dateutil==2.6.1 python-dateutil==2.6.1
requests==2.22.0 requests==2.22.0
s3transfer==0.1.13 s3transfer==0.1.13
six==1.11.0
setuptools==38.5.2 setuptools==38.5.2
six==1.12.0
transip==2.0.0 transip==2.0.0

+ 1
- 1
script/coverage View File

@ -29,7 +29,7 @@ export GOOGLE_APPLICATION_CREDENTIALS=
coverage run --branch --source=octodns --omit=octodns/cmds/* "$(command -v nosetests)" --with-xunit "$@" coverage run --branch --source=octodns --omit=octodns/cmds/* "$(command -v nosetests)" --with-xunit "$@"
coverage html coverage html
coverage xml coverage xml
coverage report
coverage report --show-missing
coverage report | grep ^TOTAL | grep -qv 100% && { coverage report | grep ^TOTAL | grep -qv 100% && {
echo "Incomplete code coverage" >&2 echo "Incomplete code coverage" >&2
exit 1 exit 1


+ 7
- 3
setup.py View File

@ -1,6 +1,9 @@
#!/usr/bin/env python #!/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 from os.path import dirname, join
import octodns import octodns
@ -65,10 +68,11 @@ setup(
install_requires=[ install_requires=[
'PyYaml>=4.2b1', 'PyYaml>=4.2b1',
'dnspython>=1.15.0', 'dnspython>=1.15.0',
'futures>=3.2.0',
'incf.countryutils>=1.0',
'futures>=3.2.0; python_version<"3.2"',
'ipaddress>=1.0.22', 'ipaddress>=1.0.22',
'natsort>=5.5.0', 'natsort>=5.5.0',
'pycountry>=19.8.18',
'pycountry-convert>=0.7.2',
# botocore doesn't like >=2.7.0 for some reason # botocore doesn't like >=2.7.0 for some reason
'python-dateutil>=2.6.0,<2.7.0', 'python-dateutil>=2.6.0,<2.7.0',
'requests>=2.20.0' 'requests>=2.20.0'


+ 28
- 0
tests/config/provider-problems.yaml View File

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

+ 0
- 16
tests/config/unknown-provider.yaml View File

@ -5,24 +5,8 @@ providers:
simple_source: simple_source:
class: helpers.SimpleSource class: helpers.SimpleSource
zones: zones:
missing.sources.:
targets:
- yaml
missing.targets.:
sources:
- yaml
unknown.source.: unknown.source.:
sources: sources:
- not-there - not-there
targets: targets:
- yaml - yaml
unknown.target.:
sources:
- yaml
targets:
- not-there-either
not.targetable.:
sources:
- yaml
targets:
- simple_source

+ 68
- 0
tests/test_octodns_equality.py View File

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

+ 45
- 42
tests/test_octodns_manager.py View File

@ -7,10 +7,12 @@ from __future__ import absolute_import, division, print_function, \
from os import environ from os import environ
from os.path import dirname, join from os.path import dirname, join
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Record 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.yaml import safe_load
from octodns.zone import Zone from octodns.zone import Zone
@ -27,80 +29,81 @@ def get_config_filename(which):
class TestManager(TestCase): class TestManager(TestCase):
def test_missing_provider_class(self): 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() 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): 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() 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): 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')) \ Manager(get_config_filename('bad-provider-class-module.yaml')) \
.sync() .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): 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')) \ Manager(get_config_filename('bad-provider-class-no-module.yaml')) \
.sync() .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): def test_missing_provider_config(self):
# Missing provider config # Missing provider config
with self.assertRaises(Exception) as ctx:
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('missing-provider-config.yaml')).sync() 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): 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() 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): 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.']) .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): 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.']) .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): 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.']) .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): 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.']) .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): 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' name = 'bad-plan-output-missing-class.yaml'
Manager(get_config_filename(name)).sync() Manager(get_config_filename(name)).sync()
self.assertEquals('plan_output bad is missing class', self.assertEquals('plan_output bad is missing class',
ctx.exception.message)
text_type(ctx.exception))
def test_bad_plan_output_config(self): 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() Manager(get_config_filename('bad-plan-output-config.yaml')).sync()
self.assertEqual('Incorrect plan_output config for bad', self.assertEqual('Incorrect plan_output config for bad',
ctx.exception.message)
text_type(ctx.exception))
def test_source_only_as_a_target(self): 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.']) .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): def test_always_dry_run(self):
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
@ -180,9 +183,9 @@ class TestManager(TestCase):
'unit.tests.') 'unit.tests.')
self.assertEquals(14, len(changes)) self.assertEquals(14, len(changes))
with self.assertRaises(Exception) as ctx:
with self.assertRaises(ManagerException) as ctx:
manager.compare(['nope'], ['dump'], 'unit.tests.') 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): def test_aggregate_target(self):
simple = SimpleProvider() simple = SimpleProvider()
@ -220,10 +223,10 @@ class TestManager(TestCase):
environ['YAML_TMP_DIR'] = tmpdir.dirname environ['YAML_TMP_DIR'] = tmpdir.dirname
manager = Manager(get_config_filename('simple.yaml')) 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, manager.dump('unit.tests.', tmpdir.dirname, False, False,
'nope') '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') manager.dump('unit.tests.', tmpdir.dirname, False, False, 'in')
@ -249,10 +252,10 @@ class TestManager(TestCase):
environ['YAML_TMP_DIR'] = tmpdir.dirname environ['YAML_TMP_DIR'] = tmpdir.dirname
manager = Manager(get_config_filename('simple-split.yaml')) 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, manager.dump('unit.tests.', tmpdir.dirname, False, True,
'nope') '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') manager.dump('unit.tests.', tmpdir.dirname, False, True, 'in')
@ -265,15 +268,15 @@ class TestManager(TestCase):
def test_validate_configs(self): def test_validate_configs(self):
Manager(get_config_filename('simple-validate.yaml')).validate_configs() 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')) \ Manager(get_config_filename('missing-sources.yaml')) \
.validate_configs() .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')) \ Manager(get_config_filename('unknown-provider.yaml')) \
.validate_configs() .validate_configs()
self.assertTrue('unknown source' in ctx.exception.message)
self.assertTrue('unknown source' in text_type(ctx.exception))
class TestMainThreadExecutor(TestCase): class TestMainThreadExecutor(TestCase):


+ 2
- 2
tests/test_octodns_plan.py View File

@ -5,8 +5,8 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from StringIO import StringIO
from logging import getLogger from logging import getLogger
from six import StringIO, text_type
from unittest import TestCase from unittest import TestCase
from octodns.provider.plan import Plan, PlanHtml, PlanLogger, PlanMarkdown from octodns.provider.plan import Plan, PlanHtml, PlanLogger, PlanMarkdown
@ -59,7 +59,7 @@ class TestPlanLogger(TestCase):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
PlanLogger('invalid', 'not-a-level') PlanLogger('invalid', 'not-a-level')
self.assertEquals('Unsupported level: not-a-level', self.assertEquals('Unsupported level: not-a-level',
ctx.exception.message)
text_type(ctx.exception))
def test_create(self): def test_create(self):


+ 17
- 16
tests/test_octodns_provider_base.py View File

@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from logging import getLogger from logging import getLogger
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Create, Delete, Record, Update from octodns.record import Create, Delete, Record, Update
@ -48,7 +49,7 @@ class TestBaseProvider(TestCase):
with self.assertRaises(NotImplementedError) as ctx: with self.assertRaises(NotImplementedError) as ctx:
BaseProvider('base') BaseProvider('base')
self.assertEquals('Abstract base class, log property missing', self.assertEquals('Abstract base class, log property missing',
ctx.exception.message)
text_type(ctx.exception))
class HasLog(BaseProvider): class HasLog(BaseProvider):
log = getLogger('HasLog') log = getLogger('HasLog')
@ -56,7 +57,7 @@ class TestBaseProvider(TestCase):
with self.assertRaises(NotImplementedError) as ctx: with self.assertRaises(NotImplementedError) as ctx:
HasLog('haslog') HasLog('haslog')
self.assertEquals('Abstract base class, SUPPORTS_GEO property missing', self.assertEquals('Abstract base class, SUPPORTS_GEO property missing',
ctx.exception.message)
text_type(ctx.exception))
class HasSupportsGeo(HasLog): class HasSupportsGeo(HasLog):
SUPPORTS_GEO = False SUPPORTS_GEO = False
@ -65,14 +66,14 @@ class TestBaseProvider(TestCase):
with self.assertRaises(NotImplementedError) as ctx: with self.assertRaises(NotImplementedError) as ctx:
HasSupportsGeo('hassupportsgeo').populate(zone) HasSupportsGeo('hassupportsgeo').populate(zone)
self.assertEquals('Abstract base class, SUPPORTS property missing', self.assertEquals('Abstract base class, SUPPORTS property missing',
ctx.exception.message)
text_type(ctx.exception))
class HasSupports(HasSupportsGeo): class HasSupports(HasSupportsGeo):
SUPPORTS = set(('A',)) SUPPORTS = set(('A',))
with self.assertRaises(NotImplementedError) as ctx: with self.assertRaises(NotImplementedError) as ctx:
HasSupports('hassupports').populate(zone) HasSupports('hassupports').populate(zone)
self.assertEquals('Abstract base class, populate method missing', self.assertEquals('Abstract base class, populate method missing',
ctx.exception.message)
text_type(ctx.exception))
# SUPPORTS_DYNAMIC has a default/fallback # SUPPORTS_DYNAMIC has a default/fallback
self.assertFalse(HasSupports('hassupports').SUPPORTS_DYNAMIC) self.assertFalse(HasSupports('hassupports').SUPPORTS_DYNAMIC)
@ -118,7 +119,7 @@ class TestBaseProvider(TestCase):
with self.assertRaises(NotImplementedError) as ctx: with self.assertRaises(NotImplementedError) as ctx:
HasPopulate('haspopulate').apply(plan) HasPopulate('haspopulate').apply(plan)
self.assertEquals('Abstract base class, _apply method missing', self.assertEquals('Abstract base class, _apply method missing',
ctx.exception.message)
text_type(ctx.exception))
def test_plan(self): def test_plan(self):
ignored = Zone('unit.tests.', []) ignored = Zone('unit.tests.', [])
@ -193,7 +194,7 @@ class TestBaseProvider(TestCase):
}) })
for i in range(int(Plan.MIN_EXISTING_RECORDS)): 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, 'ttl': 60,
'type': 'A', 'type': 'A',
'value': '2.3.4.5' 'value': '2.3.4.5'
@ -225,7 +226,7 @@ class TestBaseProvider(TestCase):
}) })
for i in range(int(Plan.MIN_EXISTING_RECORDS)): 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, 'ttl': 60,
'type': 'A', 'type': 'A',
'value': '2.3.4.5' 'value': '2.3.4.5'
@ -238,7 +239,7 @@ class TestBaseProvider(TestCase):
with self.assertRaises(UnsafePlan) as ctx: with self.assertRaises(UnsafePlan) as ctx:
Plan(zone, zone, changes, True).raise_if_unsafe() 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): def test_safe_updates_min_existing_pcent(self):
# MAX_SAFE_UPDATE_PCENT is safe when more # MAX_SAFE_UPDATE_PCENT is safe when more
@ -251,7 +252,7 @@ class TestBaseProvider(TestCase):
}) })
for i in range(int(Plan.MIN_EXISTING_RECORDS)): 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, 'ttl': 60,
'type': 'A', 'type': 'A',
'value': '2.3.4.5' 'value': '2.3.4.5'
@ -273,7 +274,7 @@ class TestBaseProvider(TestCase):
}) })
for i in range(int(Plan.MIN_EXISTING_RECORDS)): 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, 'ttl': 60,
'type': 'A', 'type': 'A',
'value': '2.3.4.5' 'value': '2.3.4.5'
@ -286,7 +287,7 @@ class TestBaseProvider(TestCase):
with self.assertRaises(UnsafePlan) as ctx: with self.assertRaises(UnsafePlan) as ctx:
Plan(zone, zone, changes, True).raise_if_unsafe() 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): def test_safe_deletes_min_existing_pcent(self):
# MAX_SAFE_DELETE_PCENT is safe when more # MAX_SAFE_DELETE_PCENT is safe when more
@ -299,7 +300,7 @@ class TestBaseProvider(TestCase):
}) })
for i in range(int(Plan.MIN_EXISTING_RECORDS)): 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, 'ttl': 60,
'type': 'A', 'type': 'A',
'value': '2.3.4.5' 'value': '2.3.4.5'
@ -322,7 +323,7 @@ class TestBaseProvider(TestCase):
}) })
for i in range(int(Plan.MIN_EXISTING_RECORDS)): 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, 'ttl': 60,
'type': 'A', 'type': 'A',
'value': '2.3.4.5' 'value': '2.3.4.5'
@ -336,7 +337,7 @@ class TestBaseProvider(TestCase):
Plan(zone, zone, changes, True, Plan(zone, zone, changes, True,
update_pcent_threshold=safe_pcent).raise_if_unsafe() 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): def test_safe_deletes_min_existing_override(self):
safe_pcent = .4 safe_pcent = .4
@ -350,7 +351,7 @@ class TestBaseProvider(TestCase):
}) })
for i in range(int(Plan.MIN_EXISTING_RECORDS)): 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, 'ttl': 60,
'type': 'A', 'type': 'A',
'value': '2.3.4.5' 'value': '2.3.4.5'
@ -364,4 +365,4 @@ class TestBaseProvider(TestCase):
Plan(zone, zone, changes, True, Plan(zone, zone, changes, True,
delete_pcent_threshold=safe_pcent).raise_if_unsafe() 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))

+ 18
- 15
tests/test_octodns_provider_cloudflare.py View File

@ -9,6 +9,7 @@ from mock import Mock, call
from os.path import dirname, join from os.path import dirname, join
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Record, Update from octodns.record import Record, Update
@ -65,7 +66,7 @@ class TestCloudflareProvider(TestCase):
provider.populate(zone) provider.populate(zone)
self.assertEquals('CloudflareError', type(ctx.exception).__name__) 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 # Bad auth
with requests_mock() as mock: with requests_mock() as mock:
@ -80,7 +81,7 @@ class TestCloudflareProvider(TestCase):
self.assertEquals('CloudflareAuthenticationError', self.assertEquals('CloudflareAuthenticationError',
type(ctx.exception).__name__) type(ctx.exception).__name__)
self.assertEquals('Unknown X-Auth-Key or X-Auth-Email', self.assertEquals('Unknown X-Auth-Key or X-Auth-Email',
ctx.exception.message)
text_type(ctx.exception))
# Bad auth, unknown resp # Bad auth, unknown resp
with requests_mock() as mock: with requests_mock() as mock:
@ -91,7 +92,7 @@ class TestCloudflareProvider(TestCase):
provider.populate(zone) provider.populate(zone)
self.assertEquals('CloudflareAuthenticationError', self.assertEquals('CloudflareAuthenticationError',
type(ctx.exception).__name__) type(ctx.exception).__name__)
self.assertEquals('Cloudflare error', ctx.exception.message)
self.assertEquals('Cloudflare error', text_type(ctx.exception))
# General error # General error
with requests_mock() as mock: with requests_mock() as mock:
@ -742,23 +743,25 @@ class TestCloudflareProvider(TestCase):
# the CDN. # the CDN.
self.assertEquals(3, len(zone.records)) self.assertEquals(3, len(zone.records))
record = list(zone.records)[0]
self.assertEquals('multi', record.name)
self.assertEquals('multi.unit.tests.', record.fqdn)
ordered = sorted(zone.records, key=lambda r: r.name)
record = ordered[0]
self.assertEquals('a', record.name)
self.assertEquals('a.unit.tests.', record.fqdn)
self.assertEquals('CNAME', record._type) self.assertEquals('CNAME', record._type)
self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value)
self.assertEquals('a.unit.tests.cdn.cloudflare.net.', record.value)
record = list(zone.records)[1]
record = ordered[1]
self.assertEquals('cname', record.name) self.assertEquals('cname', record.name)
self.assertEquals('cname.unit.tests.', record.fqdn) self.assertEquals('cname.unit.tests.', record.fqdn)
self.assertEquals('CNAME', record._type) self.assertEquals('CNAME', record._type)
self.assertEquals('cname.unit.tests.cdn.cloudflare.net.', record.value) 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('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 # CDN enabled records can't be updated, we don't know the real values
# never point a Cloudflare record to itself. # never point a Cloudflare record to itself.
@ -950,7 +953,7 @@ class TestCloudflareProvider(TestCase):
'value': 'ns1.unit.tests.' 'value': 'ns1.unit.tests.'
}) })
data = provider._gen_data(record).next()
data = next(provider._gen_data(record))
self.assertFalse('proxied' in data) self.assertFalse('proxied' in data)
@ -965,7 +968,7 @@ class TestCloudflareProvider(TestCase):
}), False }), False
) )
data = provider._gen_data(record).next()
data = next(provider._gen_data(record))
self.assertFalse(data['proxied']) self.assertFalse(data['proxied'])
@ -980,7 +983,7 @@ class TestCloudflareProvider(TestCase):
}), True }), True
) )
data = provider._gen_data(record).next()
data = next(provider._gen_data(record))
self.assertTrue(data['proxied']) self.assertTrue(data['proxied'])


+ 8
- 2
tests/test_octodns_provider_constellix.py View File

@ -10,6 +10,7 @@ from mock import Mock, call
from os.path import dirname, join from os.path import dirname, join
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Record from octodns.record import Record
@ -65,7 +66,7 @@ class TestConstellixProvider(TestCase):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals('Unauthorized', ctx.exception.message)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# Bad request # Bad request
with requests_mock() as mock: with requests_mock() as mock:
@ -77,7 +78,7 @@ class TestConstellixProvider(TestCase):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals('\n - "unittests" is not a valid domain name', self.assertEquals('\n - "unittests" is not a valid domain name',
ctx.exception.message)
text_type(ctx.exception))
# General error # General error
with requests_mock() as mock: with requests_mock() as mock:
@ -148,6 +149,11 @@ class TestConstellixProvider(TestCase):
call('POST', '/', data={'names': ['unit.tests']}), call('POST', '/', data={'names': ['unit.tests']}),
# get all domains to build the cache # get all domains to build the cache
call('GET', '/'), 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={ call('POST', '/123123/records/SRV', data={
'roundRobin': [{ 'roundRobin': [{
'priority': 10, 'priority': 10,


+ 16
- 2
tests/test_octodns_provider_digitalocean.py View File

@ -10,6 +10,7 @@ from mock import Mock, call
from os.path import dirname, join from os.path import dirname, join
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Record from octodns.record import Record
@ -50,7 +51,7 @@ class TestDigitalOceanProvider(TestCase):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals('Unauthorized', ctx.exception.message)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# General error # General error
with requests_mock() as mock: with requests_mock() as mock:
@ -175,7 +176,20 @@ class TestDigitalOceanProvider(TestCase):
call('GET', '/domains/unit.tests/records', {'page': 1}), call('GET', '/domains/unit.tests/records', {'page': 1}),
# delete the initial A record # delete the initial A record
call('DELETE', '/domains/unit.tests/records/11189877'), 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={ call('POST', '/domains/unit.tests/records', data={
'name': '_srv._tcp', 'name': '_srv._tcp',
'weight': 20, 'weight': 20,


+ 28
- 2
tests/test_octodns_provider_dnsimple.py View File

@ -9,6 +9,7 @@ from mock import Mock, call
from os.path import dirname, join from os.path import dirname, join
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Record from octodns.record import Record
@ -47,7 +48,7 @@ class TestDnsimpleProvider(TestCase):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals('Unauthorized', ctx.exception.message)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# General error # General error
with requests_mock() as mock: with requests_mock() as mock:
@ -138,7 +139,32 @@ class TestDnsimpleProvider(TestCase):
provider._client._request.assert_has_calls([ provider._client._request.assert_has_calls([
# created the domain # created the domain
call('POST', '/domains', data={'name': 'unit.tests'}), 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={ call('POST', '/zones/unit.tests/records', data={
'content': '20 30 foo-1.unit.tests.', 'content': '20 30 foo-1.unit.tests.',
'priority': 10, 'priority': 10,


+ 24
- 3
tests/test_octodns_provider_dnsmadeeasy.py View File

@ -10,6 +10,7 @@ from mock import Mock, call
from os.path import dirname, join from os.path import dirname, join
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Record from octodns.record import Record
@ -65,7 +66,7 @@ class TestDnsMadeEasyProvider(TestCase):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals('Unauthorized', ctx.exception.message)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# Bad request # Bad request
with requests_mock() as mock: with requests_mock() as mock:
@ -76,7 +77,7 @@ class TestDnsMadeEasyProvider(TestCase):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertEquals('\n - Rate limit exceeded', self.assertEquals('\n - Rate limit exceeded',
ctx.exception.message)
text_type(ctx.exception))
# General error # General error
with requests_mock() as mock: with requests_mock() as mock:
@ -148,7 +149,27 @@ class TestDnsMadeEasyProvider(TestCase):
call('POST', '/', data={'name': 'unit.tests'}), call('POST', '/', data={'name': 'unit.tests'}),
# get all domains to build the cache # get all domains to build the cache
call('GET', '/'), 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={ call('POST', '/123123/records', data={
'name': '_srv._tcp', 'name': '_srv._tcp',
'weight': 20, 'weight': 20,


+ 2
- 2
tests/test_octodns_provider_dyn.py View File

@ -670,8 +670,8 @@ class TestDynProviderGeo(TestCase):
tds = provider.traffic_directors tds = provider.traffic_directors
self.assertEquals(set(['unit.tests.', 'geo.unit.tests.']), self.assertEquals(set(['unit.tests.', 'geo.unit.tests.']),
set(tds.keys())) 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 " provider.log.warn.assert_called_with("Unsupported TrafficDirector "
"'%s'", 'something else') "'%s'", 'something else')


+ 2
- 1
tests/test_octodns_provider_fastdns.py View File

@ -9,6 +9,7 @@ from __future__ import absolute_import, division, print_function, \
from os.path import dirname, join from os.path import dirname, join
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Record from octodns.record import Record
@ -147,4 +148,4 @@ class TestFastdnsProvider(TestCase):
changes = provider.apply(plan) changes = provider.apply(plan)
except NameError as e: except NameError as e:
expected = "contractId not specified to create zone" expected = "contractId not specified to create zone"
self.assertEquals(e.message, expected)
self.assertEquals(text_type(e), expected)

+ 7
- 2
tests/test_octodns_provider_googlecloud.py View File

@ -193,8 +193,13 @@ class DummyIterator:
def __iter__(self): def __iter__(self):
return self return self
# python2
def next(self): def next(self):
return self.iterable.next()
return next(self.iterable)
# python3
def __next__(self):
return next(self.iterable)
class TestGoogleCloudProvider(TestCase): class TestGoogleCloudProvider(TestCase):
@ -247,7 +252,7 @@ class TestGoogleCloudProvider(TestCase):
return_values_for_status = iter( return_values_for_status = iter(
["pending"] * 11 + ['done', 'done']) ["pending"] * 11 + ['done', 'done'])
type(status_mock).status = PropertyMock( 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) gcloud_zone_mock.changes = Mock(return_value=status_mock)
provider = self._get_provider() provider = self._get_provider()


+ 16
- 16
tests/test_octodns_provider_mythicbeasts.py View File

@ -8,6 +8,7 @@ from __future__ import absolute_import, division, print_function, \
from os.path import dirname, join from os.path import dirname, join
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.provider.mythicbeasts import MythicBeastsProvider, \ from octodns.provider.mythicbeasts import MythicBeastsProvider, \
@ -31,12 +32,12 @@ class TestMythicBeastsProvider(TestCase):
with self.assertRaises(AssertionError) as err: with self.assertRaises(AssertionError) as err:
add_trailing_dot('unit.tests.') add_trailing_dot('unit.tests.')
self.assertEquals('Value already has trailing dot', self.assertEquals('Value already has trailing dot',
err.exception.message)
text_type(err.exception))
with self.assertRaises(AssertionError) as err: with self.assertRaises(AssertionError) as err:
remove_trailing_dot('unit.tests') remove_trailing_dot('unit.tests')
self.assertEquals('Value already missing trailing dot', 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(add_trailing_dot('unit.tests'), 'unit.tests.')
self.assertEquals(remove_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}]} {'raw_values': [{'value': '', 'ttl': 0}]}
) )
self.assertEquals('Unable to parse MX data', self.assertEquals('Unable to parse MX data',
err.exception.message)
text_type(err.exception))
def test_data_for_CNAME(self): def test_data_for_CNAME(self):
test_data = { test_data = {
@ -129,7 +130,7 @@ class TestMythicBeastsProvider(TestCase):
{'raw_values': [{'value': '', 'ttl': 0}]} {'raw_values': [{'value': '', 'ttl': 0}]}
) )
self.assertEquals('Unable to parse SRV data', self.assertEquals('Unable to parse SRV data',
err.exception.message)
text_type(err.exception))
def test_data_for_SSHFP(self): def test_data_for_SSHFP(self):
test_data = { test_data = {
@ -149,7 +150,7 @@ class TestMythicBeastsProvider(TestCase):
{'raw_values': [{'value': '', 'ttl': 0}]} {'raw_values': [{'value': '', 'ttl': 0}]}
) )
self.assertEquals('Unable to parse SSHFP data', self.assertEquals('Unable to parse SSHFP data',
err.exception.message)
text_type(err.exception))
def test_data_for_CAA(self): def test_data_for_CAA(self):
test_data = { test_data = {
@ -166,7 +167,7 @@ class TestMythicBeastsProvider(TestCase):
{'raw_values': [{'value': '', 'ttl': 0}]} {'raw_values': [{'value': '', 'ttl': 0}]}
) )
self.assertEquals('Unable to parse CAA data', self.assertEquals('Unable to parse CAA data',
err.exception.message)
text_type(err.exception))
def test_command_generation(self): def test_command_generation(self):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
@ -312,7 +313,7 @@ class TestMythicBeastsProvider(TestCase):
with self.assertRaises(AssertionError) as err: with self.assertRaises(AssertionError) as err:
provider = MythicBeastsProvider('test', None) provider = MythicBeastsProvider('test', None)
self.assertEquals('Passwords must be a dictionary', self.assertEquals('Passwords must be a dictionary',
err.exception.message)
text_type(err.exception))
# Missing password # Missing password
with requests_mock() as mock: with requests_mock() as mock:
@ -324,7 +325,7 @@ class TestMythicBeastsProvider(TestCase):
provider.populate(zone) provider.populate(zone)
self.assertEquals( self.assertEquals(
'Missing password for domain: unit.tests', 'Missing password for domain: unit.tests',
err.exception.message)
text_type(err.exception))
# Failed authentication # Failed authentication
with requests_mock() as mock: with requests_mock() as mock:
@ -413,8 +414,7 @@ class TestMythicBeastsProvider(TestCase):
provider.apply(plan) provider.apply(plan)
self.assertEquals( self.assertEquals(
'Mythic Beasts could not action command: unit.tests ' '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 # Check deleting and adding/changing test record
existing = 'prawf 300 TXT prawf prawf prawf\ndileu 300 TXT dileu' existing = 'prawf 300 TXT prawf prawf prawf\ndileu 300 TXT dileu'
@ -441,11 +441,11 @@ class TestMythicBeastsProvider(TestCase):
plan = provider.plan(wanted) plan = provider.plan(wanted)
# Octo ignores NS records (15-1) # 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.assertEquals(16, provider.apply(plan))
self.assertTrue(plan.exists) self.assertTrue(plan.exists)

+ 10
- 5
tests/test_octodns_provider_ns1.py View File

@ -5,6 +5,7 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from collections import defaultdict
from mock import Mock, call, patch from mock import Mock, call, patch
from ns1.rest.errors import AuthException, RateLimitException, \ from ns1.rest.errors import AuthException, RateLimitException, \
ResourceException ResourceException
@ -373,8 +374,12 @@ class TestNs1Provider(TestCase):
load_mock.side_effect = [nsone_zone, nsone_zone] load_mock.side_effect = [nsone_zone, nsone_zone]
plan = provider.plan(desired) plan = provider.plan(desired)
self.assertEquals(3, len(plan.changes)) 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 # 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 # the update and delete targets, we can add our side effects to that to
# trigger rate limit handling # trigger rate limit handling
@ -395,8 +400,8 @@ class TestNs1Provider(TestCase):
self.assertEquals(3, got_n) self.assertEquals(3, got_n)
nsone_zone.loadRecord.assert_has_calls([ nsone_zone.loadRecord.assert_has_calls([
call('unit.tests', u'A'), call('unit.tests', u'A'),
call('geo', u'A'),
call('delete-me', u'A'), call('delete-me', u'A'),
call('geo', u'A'),
]) ])
mock_record.assert_has_calls([ mock_record.assert_has_calls([
call.update(answers=[{'answer': [u'1.2.3.4'], 'meta': {}}], 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': {}}], call.update(answers=[{u'answer': [u'1.2.3.4'], u'meta': {}}],
filters=[], filters=[],
ttl=32), ttl=32),
call.delete(),
call.delete(),
call.update( call.update(
answers=[ answers=[
{u'answer': [u'101.102.103.104'], u'meta': {}}, {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}}, {u'filter': u'select_first_n', u'config': {u'N': 1}},
], ],
ttl=34), ttl=34),
call.delete(),
call.delete()
]) ])
def test_escaping(self): def test_escaping(self):


+ 47
- 48
tests/test_octodns_provider_ovh.py View File

@ -382,64 +382,63 @@ class TestOvhProvider(TestCase):
get_mock.side_effect = [[100], [101], [102], [103]] get_mock.side_effect = [[100], [101], [102], [103]]
provider.apply(plan) provider.apply(plan)
wanted_calls = [ 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', 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', 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:info@bar.exam'
'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) post_mock.assert_has_calls(wanted_calls)
# Get for delete calls # Get for delete calls
wanted_get_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'DKIM',
subDomain='dkim'),
call(u'/domain/zone/unit.tests/record', fieldType=u'A', call(u'/domain/zone/unit.tests/record', fieldType=u'A',
subDomain=u''), subDomain=u''),
call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM',
subDomain='dkim'),
call(u'/domain/zone/unit.tests/record', fieldType=u'A', 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) get_mock.assert_has_calls(wanted_get_calls)
# 4 delete calls for update and delete # 4 delete calls for update and delete
delete_mock.assert_has_calls( delete_mock.assert_has_calls(


+ 2
- 1
tests/test_octodns_provider_powerdns.py View File

@ -9,6 +9,7 @@ from json import loads, dumps
from os.path import dirname, join from os.path import dirname, join
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import Record from octodns.record import Record
@ -52,7 +53,7 @@ class TestPowerDnsProvider(TestCase):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
provider.populate(zone) provider.populate(zone)
self.assertTrue('unauthorized' in ctx.exception.message)
self.assertTrue('unauthorized' in text_type(ctx.exception))
# General error # General error
with requests_mock() as mock: with requests_mock() as mock:


+ 7
- 7
tests/test_octodns_provider_rackspace.py View File

@ -7,8 +7,9 @@ from __future__ import absolute_import, division, print_function, \
import json import json
import re import re
from six import text_type
from six.moves.urllib.parse import urlparse
from unittest import TestCase from unittest import TestCase
from urlparse import urlparse
from requests import HTTPError from requests import HTTPError
from requests_mock import ANY, mock as requests_mock from requests_mock import ANY, mock as requests_mock
@ -39,7 +40,6 @@ with open('./tests/fixtures/rackspace-sample-recordset-page2.json') as fh:
class TestRackspaceProvider(TestCase): class TestRackspaceProvider(TestCase):
def setUp(self): def setUp(self):
self.maxDiff = 1000
with requests_mock() as mock: with requests_mock() as mock:
mock.post(ANY, status_code=200, text=AUTH_RESPONSE) mock.post(ANY, status_code=200, text=AUTH_RESPONSE)
self.provider = RackspaceProvider('identity', 'test', 'api-key', self.provider = RackspaceProvider('identity', 'test', 'api-key',
@ -53,7 +53,7 @@ class TestRackspaceProvider(TestCase):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
self.provider.populate(zone) self.provider.populate(zone)
self.assertTrue('unauthorized' in ctx.exception.message)
self.assertTrue('unauthorized' in text_type(ctx.exception))
self.assertTrue(mock.called_once) self.assertTrue(mock.called_once)
def test_server_error(self): def test_server_error(self):
@ -792,13 +792,13 @@ class TestRackspaceProvider(TestCase):
ExpectedUpdates = { ExpectedUpdates = {
"records": [{ "records": [{
"name": "unit.tests", "name": "unit.tests",
"id": "A-222222",
"data": "1.2.3.5",
"id": "A-111111",
"data": "1.2.3.4",
"ttl": 3600 "ttl": 3600
}, { }, {
"name": "unit.tests", "name": "unit.tests",
"id": "A-111111",
"data": "1.2.3.4",
"id": "A-222222",
"data": "1.2.3.5",
"ttl": 3600 "ttl": 3600
}, { }, {
"name": "unit.tests", "name": "unit.tests",


+ 160
- 115
tests/test_octodns_provider_route53.py View File

@ -7,6 +7,7 @@ from __future__ import absolute_import, division, print_function, \
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
from botocore.stub import ANY, Stubber from botocore.stub import ANY, Stubber
from six import text_type
from unittest import TestCase from unittest import TestCase
from mock import patch from mock import patch
@ -1881,10 +1882,10 @@ class TestRoute53Provider(TestCase):
@patch('octodns.provider.route53.Route53Provider._really_apply') @patch('octodns.provider.route53.Route53Provider._really_apply')
def test_apply_3(self, really_apply_mock, _): 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, plan = self._get_test_plan(7)
provider.apply(plan) 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._load_records')
@patch('octodns.provider.route53.Route53Provider._really_apply') @patch('octodns.provider.route53.Route53Provider._really_apply')
@ -1903,7 +1904,7 @@ class TestRoute53Provider(TestCase):
provider, plan = self._get_test_plan(1) provider, plan = self._get_test_plan(1)
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
provider.apply(plan) provider.apply(plan)
self.assertTrue('modifications' in ctx.exception.message)
self.assertTrue('modifications' in text_type(ctx.exception))
def test_semicolon_fixup(self): def test_semicolon_fixup(self):
provider = Route53Provider('test', 'abc', '123') provider = Route53Provider('test', 'abc', '123')
@ -2090,6 +2091,58 @@ class TestRoute53Records(TestCase):
e.__repr__() e.__repr__()
f.__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): def test_dynamic_value_delete(self):
provider = DummyProvider() provider = DummyProvider()
geo = _Route53DynamicValue(provider, self.record_a, 'iad', '2.2.2.2', geo = _Route53DynamicValue(provider, self.record_a, 'iad', '2.2.2.2',
@ -2206,70 +2259,112 @@ class TestRoute53Records(TestCase):
creating=True) creating=True)
self.assertEquals(18, len(route53_records)) 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 # Convert the route53_records into mods
self.assertEquals([{ self.assertEquals([{
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'HealthCheckId': 'hc42', 'HealthCheckId': 'hc42',
'Name': '_octodns-ap-southeast-1-value.unit.tests.', '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, 'TTL': 60,
'Type': 'A', 'Type': 'A',
'Weight': 2
}
'Weight': 2}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'HealthCheckId': 'hc42', 'HealthCheckId': 'hc42',
'Name': '_octodns-ap-southeast-1-value.unit.tests.', '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, 'TTL': 60,
'Type': 'A', 'Type': 'A',
'Weight': 2
}
'Weight': 2}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'Name': '_octodns-default-pool.unit.tests.',
'ResourceRecords': [
{'Value': '1.1.2.1'},
{'Value': '1.1.2.2'}],
'TTL': 60,
'Type': 'A'}
}, {
'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': {
'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-us-east-1-value.unit.tests.',
'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', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
'DNSName': '_octodns-ap-southeast-1-value.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'
},
'GeoLocation': {
'CountryCode': 'JP'},
'Name': 'unit.tests.',
'SetIdentifier': '0-ap-southeast-1-AS-JP',
'Type': 'A'
}
'HostedZoneId': 'z45'},
'Failover': 'PRIMARY',
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
'SetIdentifier': 'ap-southeast-1-Primary',
'Type': 'A'}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
'DNSName': '_octodns-eu-central-1-value.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'GeoLocation': {
'CountryCode': 'US',
'SubdivisionCode': 'FL',
},
'Name': 'unit.tests.',
'SetIdentifier': '1-eu-central-1-NA-US-FL',
'Failover': 'PRIMARY',
'Name': '_octodns-eu-central-1-pool.unit.tests.',
'SetIdentifier': 'eu-central-1-Primary',
'Type': 'A'} 'Type': 'A'}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
'DNSName': '_octodns-us-east-1-value.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'GeoLocation': {
'CountryCode': '*'},
'Name': 'unit.tests.',
'SetIdentifier': '2-us-east-1-None',
'Failover': 'PRIMARY',
'Name': '_octodns-us-east-1-pool.unit.tests.',
'SetIdentifier': 'us-east-1-Primary',
'Type': 'A'} 'Type': 'A'}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
@ -2286,123 +2381,72 @@ class TestRoute53Records(TestCase):
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'GeoLocation': {
'CountryCode': 'CN'},
'Name': 'unit.tests.',
'SetIdentifier': '0-ap-southeast-1-AS-CN',
'Failover': 'SECONDARY',
'Name': '_octodns-eu-central-1-pool.unit.tests.',
'SetIdentifier': 'eu-central-1-Secondary-us-east-1',
'Type': 'A'} 'Type': 'A'}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-us-east-1-value.unit.tests.',
'DNSName': '_octodns-default-pool.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'Failover': 'PRIMARY',
'Failover': 'SECONDARY',
'Name': '_octodns-us-east-1-pool.unit.tests.', 'Name': '_octodns-us-east-1-pool.unit.tests.',
'SetIdentifier': 'us-east-1-Primary',
'SetIdentifier': 'us-east-1-Secondary-default',
'Type': 'A'} 'Type': 'A'}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'GeoLocation': { 'GeoLocation': {
'ContinentCode': 'EU'},
'CountryCode': 'CN'},
'Name': 'unit.tests.', 'Name': 'unit.tests.',
'SetIdentifier': '1-eu-central-1-EU',
'SetIdentifier': '0-ap-southeast-1-AS-CN',
'Type': 'A'} 'Type': 'A'}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-eu-central-1-value.unit.tests.',
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'Failover': 'PRIMARY',
'Name': '_octodns-eu-central-1-pool.unit.tests.',
'SetIdentifier': 'eu-central-1-Primary',
'Type': 'A'}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'Name': '_octodns-default-pool.unit.tests.',
'ResourceRecords': [{
'Value': '1.1.2.1'},
{
'Value': '1.1.2.2'}],
'TTL': 60,
'GeoLocation': {
'CountryCode': 'JP'},
'Name': 'unit.tests.',
'SetIdentifier': '0-ap-southeast-1-AS-JP',
'Type': 'A'} 'Type': 'A'}
}, {
'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', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-default-pool.unit.tests.',
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'Failover': 'SECONDARY',
'Name': '_octodns-us-east-1-pool.unit.tests.',
'SetIdentifier': 'us-east-1-Secondary-default',
'GeoLocation': {
'ContinentCode': 'EU'},
'Name': 'unit.tests.',
'SetIdentifier': '1-eu-central-1-EU',
'Type': 'A'} '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'}],
'SetIdentifier': 'us-east-1-000',
'TTL': 60,
'Type': 'A',
'Weight': 1}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
'ResourceRecordSet': { 'ResourceRecordSet': {
'AliasTarget': { 'AliasTarget': {
'DNSName': '_octodns-ap-southeast-1-value.unit.tests.',
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'Failover': 'PRIMARY',
'Name': '_octodns-ap-southeast-1-pool.unit.tests.',
'SetIdentifier': 'ap-southeast-1-Primary',
'GeoLocation': {
'CountryCode': 'US',
'SubdivisionCode': 'FL'},
'Name': 'unit.tests.',
'SetIdentifier': '1-eu-central-1-NA-US-FL',
'Type': 'A'} 'Type': 'A'}
}, { }, {
'Action': 'CREATE', 'Action': 'CREATE',
@ -2411,11 +2455,12 @@ class TestRoute53Records(TestCase):
'DNSName': '_octodns-us-east-1-pool.unit.tests.', 'DNSName': '_octodns-us-east-1-pool.unit.tests.',
'EvaluateTargetHealth': True, 'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'}, 'HostedZoneId': 'z45'},
'Failover': 'SECONDARY',
'Name': '_octodns-eu-central-1-pool.unit.tests.',
'SetIdentifier': 'eu-central-1-Secondary-us-east-1',
'GeoLocation': {
'CountryCode': '*'},
'Name': 'unit.tests.',
'SetIdentifier': '2-us-east-1-None',
'Type': 'A'} 'Type': 'A'}
}], [r.mod('CREATE', []) for r in route53_records])
}], expected_mods)
for route53_record in route53_records: for route53_record in route53_records:
# Smoke test stringification # Smoke test stringification


+ 2
- 1
tests/test_octodns_provider_selectel.py View File

@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from unittest import TestCase from unittest import TestCase
from six import text_type
import requests_mock import requests_mock
@ -288,7 +289,7 @@ class TestSelectelProvider(TestCase):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
SelectelProvider(123, 'fail_token') SelectelProvider(123, 'fail_token')
self.assertEquals(ctx.exception.message,
self.assertEquals(text_type(ctx.exception),
'Authorization failed. Invalid or empty token.') 'Authorization failed. Invalid or empty token.')
@requests_mock.Mocker() @requests_mock.Mocker()


+ 2
- 2
tests/test_octodns_provider_transip.py View File

@ -5,8 +5,8 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
# from mock import Mock, call
from os.path import dirname, join from os.path import dirname, join
from six import text_type
from suds import WebFault from suds import WebFault
@ -176,7 +176,7 @@ N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
self.assertEquals( self.assertEquals(
'populate: (102) Transip used as target' + 'populate: (102) Transip used as target' +
' for non-existing zone: notfound.unit.tests.', ' for non-existing zone: notfound.unit.tests.',
ctx.exception.message)
text_type(ctx.exception))
# Happy Plan - Zone does not exists # Happy Plan - Zone does not exists
# Won't trigger an exception if provider is NOT used as a target for a # Won't trigger an exception if provider is NOT used as a target for a


+ 23
- 25
tests/test_octodns_provider_yaml.py View File

@ -8,6 +8,7 @@ from __future__ import absolute_import, division, print_function, \
from os import makedirs from os import makedirs
from os.path import basename, dirname, isdir, isfile, join from os.path import basename, dirname, isdir, isfile, join
from unittest import TestCase from unittest import TestCase
from six import text_type
from yaml import safe_load from yaml import safe_load
from yaml.constructor import ConstructorError from yaml.constructor import ConstructorError
@ -57,8 +58,8 @@ class TestYamlProvider(TestCase):
# We add everything # We add everything
plan = target.plan(zone) 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)) self.assertFalse(isfile(yaml_file))
# Now actually do it # Now actually do it
@ -67,8 +68,8 @@ class TestYamlProvider(TestCase):
# Dynamic plan # Dynamic plan
plan = target.plan(dynamic_zone) 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)) self.assertFalse(isfile(dynamic_yaml_file))
# Apply it # Apply it
self.assertEquals(5, target.apply(plan)) self.assertEquals(5, target.apply(plan))
@ -79,16 +80,15 @@ class TestYamlProvider(TestCase):
target.populate(reloaded) target.populate(reloaded)
self.assertDictEqual( self.assertDictEqual(
{'included': ['test']}, {'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)) self.assertFalse(zone.changes(reloaded, target=source))
# A 2nd sync should still create everything # A 2nd sync should still create everything
plan = target.plan(zone) 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: with open(yaml_file) as fh:
data = safe_load(fh.read()) data = safe_load(fh.read())
@ -116,7 +116,7 @@ class TestYamlProvider(TestCase):
self.assertTrue('value' in data.pop('www.sub')) self.assertTrue('value' in data.pop('www.sub'))
# make sure nothing is left # make sure nothing is left
self.assertEquals([], data.keys())
self.assertEquals([], list(data.keys()))
with open(dynamic_yaml_file) as fh: with open(dynamic_yaml_file) as fh:
data = safe_load(fh.read()) data = safe_load(fh.read())
@ -145,7 +145,7 @@ class TestYamlProvider(TestCase):
# self.assertTrue('dynamic' in dyna) # self.assertTrue('dynamic' in dyna)
# make sure nothing is left # make sure nothing is left
self.assertEquals([], data.keys())
self.assertEquals([], list(data.keys()))
def test_empty(self): def test_empty(self):
source = YamlProvider('test', join(dirname(__file__), 'config')) source = YamlProvider('test', join(dirname(__file__), 'config'))
@ -178,7 +178,7 @@ class TestYamlProvider(TestCase):
with self.assertRaises(SubzoneRecordException) as ctx: with self.assertRaises(SubzoneRecordException) as ctx:
source.populate(zone) source.populate(zone)
self.assertEquals('Record www.sub.unit.tests. is under a managed ' self.assertEquals('Record www.sub.unit.tests. is under a managed '
'subzone', ctx.exception.message)
'subzone', text_type(ctx.exception))
class TestSplitYamlProvider(TestCase): class TestSplitYamlProvider(TestCase):
@ -201,9 +201,8 @@ class TestSplitYamlProvider(TestCase):
# This isn't great, but given the variable nature of the temp dir # This isn't great, but given the variable nature of the temp dir
# names, it's necessary. # 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): def test_zone_directory(self):
source = SplitYamlProvider( source = SplitYamlProvider(
@ -252,8 +251,8 @@ class TestSplitYamlProvider(TestCase):
# We add everything # We add everything
plan = target.plan(zone) 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)) self.assertFalse(isdir(zone_dir))
# Now actually do it # Now actually do it
@ -261,8 +260,8 @@ class TestSplitYamlProvider(TestCase):
# Dynamic plan # Dynamic plan
plan = target.plan(dynamic_zone) 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)) self.assertFalse(isdir(dynamic_zone_dir))
# Apply it # Apply it
self.assertEquals(5, target.apply(plan)) self.assertEquals(5, target.apply(plan))
@ -273,16 +272,15 @@ class TestSplitYamlProvider(TestCase):
target.populate(reloaded) target.populate(reloaded)
self.assertDictEqual( self.assertDictEqual(
{'included': ['test']}, {'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)) self.assertFalse(zone.changes(reloaded, target=source))
# A 2nd sync should still create everything # A 2nd sync should still create everything
plan = target.plan(zone) 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') yaml_file = join(zone_dir, '$unit.tests.yaml')
self.assertTrue(isfile(yaml_file)) self.assertTrue(isfile(yaml_file))
@ -371,4 +369,4 @@ class TestSplitYamlProvider(TestCase):
with self.assertRaises(SubzoneRecordException) as ctx: with self.assertRaises(SubzoneRecordException) as ctx:
source.populate(zone) source.populate(zone)
self.assertEquals('Record www.sub.unit.tests. is under a managed ' self.assertEquals('Record www.sub.unit.tests. is under a managed '
'subzone', ctx.exception.message)
'subzone', text_type(ctx.exception))

+ 391
- 30
tests/test_octodns_record.py View File

@ -5,12 +5,14 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \ 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 octodns.zone import Zone
from helpers import DynamicProvider, GeoProvider, SimpleProvider from helpers import DynamicProvider, GeoProvider, SimpleProvider
@ -482,113 +484,140 @@ class TestRecord(TestCase):
# full sorting # full sorting
# equivalent # equivalent
b_naptr_value = b.values[0] 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 # by order
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 10, 'order': 10,
'preference': 31, 'preference': 31,
'flags': 'M', 'flags': 'M',
'service': 'N', 'service': 'N',
'regexp': 'O', 'regexp': 'O',
'replacement': 'x', 'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 40, 'order': 40,
'preference': 31, 'preference': 31,
'flags': 'M', 'flags': 'M',
'service': 'N', 'service': 'N',
'regexp': 'O', 'regexp': 'O',
'replacement': 'x', 'replacement': 'x',
})))
}))
# by preference # by preference
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30, 'order': 30,
'preference': 10, 'preference': 10,
'flags': 'M', 'flags': 'M',
'service': 'N', 'service': 'N',
'regexp': 'O', 'regexp': 'O',
'replacement': 'x', 'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30, 'order': 30,
'preference': 40, 'preference': 40,
'flags': 'M', 'flags': 'M',
'service': 'N', 'service': 'N',
'regexp': 'O', 'regexp': 'O',
'replacement': 'x', 'replacement': 'x',
})))
}))
# by flags # by flags
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30, 'order': 30,
'preference': 31, 'preference': 31,
'flags': 'A', 'flags': 'A',
'service': 'N', 'service': 'N',
'regexp': 'O', 'regexp': 'O',
'replacement': 'x', 'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30, 'order': 30,
'preference': 31, 'preference': 31,
'flags': 'Z', 'flags': 'Z',
'service': 'N', 'service': 'N',
'regexp': 'O', 'regexp': 'O',
'replacement': 'x', 'replacement': 'x',
})))
}))
# by service # by service
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30, 'order': 30,
'preference': 31, 'preference': 31,
'flags': 'M', 'flags': 'M',
'service': 'A', 'service': 'A',
'regexp': 'O', 'regexp': 'O',
'replacement': 'x', 'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30, 'order': 30,
'preference': 31, 'preference': 31,
'flags': 'M', 'flags': 'M',
'service': 'Z', 'service': 'Z',
'regexp': 'O', 'regexp': 'O',
'replacement': 'x', 'replacement': 'x',
})))
}))
# by regexp # by regexp
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30, 'order': 30,
'preference': 31, 'preference': 31,
'flags': 'M', 'flags': 'M',
'service': 'N', 'service': 'N',
'regexp': 'A', 'regexp': 'A',
'replacement': 'x', 'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30, 'order': 30,
'preference': 31, 'preference': 31,
'flags': 'M', 'flags': 'M',
'service': 'N', 'service': 'N',
'regexp': 'Z', 'regexp': 'Z',
'replacement': 'x', 'replacement': 'x',
})))
}))
# by replacement # by replacement
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30, 'order': 30,
'preference': 31, 'preference': 31,
'flags': 'M', 'flags': 'M',
'service': 'N', 'service': 'N',
'regexp': 'O', 'regexp': 'O',
'replacement': 'a', 'replacement': 'a',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30, 'order': 30,
'preference': 31, 'preference': 31,
'flags': 'M', 'flags': 'M',
'service': 'N', 'service': 'N',
'regexp': 'O', 'regexp': 'O',
'replacement': 'z', 'replacement': 'z',
})))
}))
# __repr__ doesn't blow up # __repr__ doesn't blow up
a.__repr__() 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): def test_ns(self):
a_values = ['5.6.7.8.', '6.7.8.9.', '7.8.9.0.'] a_values = ['5.6.7.8.', '6.7.8.9.', '7.8.9.0.']
a_data = {'ttl': 30, 'values': a_values} a_data = {'ttl': 30, 'values': a_values}
@ -758,14 +787,14 @@ class TestRecord(TestCase):
# Missing type # Missing type
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
Record.new(self.zone, 'unknown', {}) Record.new(self.zone, 'unknown', {})
self.assertTrue('missing type' in ctx.exception.message)
self.assertTrue('missing type' in text_type(ctx.exception))
# Unknown type # Unknown type
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
Record.new(self.zone, 'unknown', { Record.new(self.zone, 'unknown', {
'type': 'XXX', '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): def test_change(self):
existing = Record.new(self.zone, 'txt', { existing = Record.new(self.zone, 'txt', {
@ -796,6 +825,38 @@ class TestRecord(TestCase):
self.assertEquals(values, geo.values) self.assertEquals(values, geo.values)
self.assertEquals(['NA-US', 'NA'], list(geo.parents)) 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): def test_healthcheck(self):
new = Record.new(self.zone, 'a', { new = Record.new(self.zone, 'a', {
'ttl': 44, 'ttl': 44,
@ -851,6 +912,306 @@ class TestRecord(TestCase):
}) })
self.assertFalse(new.ignored) 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): class TestRecordValidation(TestCase):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])


+ 3
- 2
tests/test_octodns_source_axfr.py View File

@ -9,6 +9,7 @@ import dns.zone
from dns.exception import DNSException from dns.exception import DNSException
from mock import patch from mock import patch
from six import text_type
from unittest import TestCase from unittest import TestCase
from octodns.source.axfr import AxfrSource, AxfrSourceZoneTransferFailed, \ from octodns.source.axfr import AxfrSource, AxfrSourceZoneTransferFailed, \
@ -38,7 +39,7 @@ class TestAxfrSource(TestCase):
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
self.source.populate(zone) self.source.populate(zone)
self.assertEquals('Unable to Perform Zone Transfer', self.assertEquals('Unable to Perform Zone Transfer',
ctx.exception.message)
text_type(ctx.exception))
class TestZoneFileSource(TestCase): class TestZoneFileSource(TestCase):
@ -68,4 +69,4 @@ class TestZoneFileSource(TestCase):
zone = Zone('invalid.zone.', []) zone = Zone('invalid.zone.', [])
self.source.populate(zone) self.source.populate(zone)
self.assertEquals('The DNS zone has no NS RRset at its origin.', self.assertEquals('The DNS zone has no NS RRset at its origin.',
ctx.exception.message)
text_type(ctx.exception))

+ 1
- 1
tests/test_octodns_yaml.py View File

@ -5,7 +5,7 @@
from __future__ import absolute_import, division, print_function, \ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from StringIO import StringIO
from six import StringIO
from unittest import TestCase from unittest import TestCase
from yaml.constructor import ConstructorError from yaml.constructor import ConstructorError


+ 6
- 5
tests/test_octodns_zone.py View File

@ -6,6 +6,7 @@ from __future__ import absolute_import, division, print_function, \
unicode_literals unicode_literals
from unittest import TestCase from unittest import TestCase
from six import text_type
from octodns.record import ARecord, AaaaRecord, Create, Delete, Record, Update from octodns.record import ARecord, AaaaRecord, Create, Delete, Record, Update
from octodns.zone import DuplicateRecordException, InvalidNodeException, \ from octodns.zone import DuplicateRecordException, InvalidNodeException, \
@ -47,7 +48,7 @@ class TestZone(TestCase):
with self.assertRaises(DuplicateRecordException) as ctx: with self.assertRaises(DuplicateRecordException) as ctx:
zone.add_record(a) zone.add_record(a)
self.assertEquals('Duplicate record a.unit.tests., type A', self.assertEquals('Duplicate record a.unit.tests., type A',
ctx.exception.message)
text_type(ctx.exception))
self.assertEquals(zone.records, set([a])) self.assertEquals(zone.records, set([a]))
# can add duplicate with replace=True # can add duplicate with replace=True
@ -137,7 +138,7 @@ class TestZone(TestCase):
def test_missing_dot(self): def test_missing_dot(self):
with self.assertRaises(Exception) as ctx: with self.assertRaises(Exception) as ctx:
Zone('not.allowed', []) 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): def test_sub_zones(self):
@ -160,7 +161,7 @@ class TestZone(TestCase):
}) })
with self.assertRaises(SubzoneRecordException) as ctx: with self.assertRaises(SubzoneRecordException) as ctx:
zone.add_record(record) 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 # Can add it w/lenient
zone.add_record(record, lenient=True) zone.add_record(record, lenient=True)
self.assertEquals(set([record]), zone.records) self.assertEquals(set([record]), zone.records)
@ -174,7 +175,7 @@ class TestZone(TestCase):
}) })
with self.assertRaises(SubzoneRecordException) as ctx: with self.assertRaises(SubzoneRecordException) as ctx:
zone.add_record(record) 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 # Can add it w/lenient
zone.add_record(record, lenient=True) zone.add_record(record, lenient=True)
self.assertEquals(set([record]), zone.records) self.assertEquals(set([record]), zone.records)
@ -188,7 +189,7 @@ class TestZone(TestCase):
}) })
with self.assertRaises(SubzoneRecordException) as ctx: with self.assertRaises(SubzoneRecordException) as ctx:
zone.add_record(record) 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 # Can add it w/lenient
zone.add_record(record, lenient=True) zone.add_record(record, lenient=True)
self.assertEquals(set([record]), zone.records) self.assertEquals(set([record]), zone.records)


Loading…
Cancel
Save