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
python:
- 2.7
matrix:
include:
- python: 2.7
- python: 3.7
before_install: pip install --upgrade pip
script: ./script/cibuild
notifications:
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
* No material changes


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

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


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


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

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


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

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


+ 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
# to get a consistent _gen_data result that matches what
# went in to new_contents
data = self._gen_data(r).next()
data = next(self._gen_data(r))
# Record the record_id and data for this existing record
key = self._gen_key(data)


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

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


+ 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
values = sorted(values, key=_dynamic_value_sort_key)
# Ensure that weight is included and if not use the default
values = map(lambda v: {
values = [{
'value': v['value'],
'weight': v.get('weight', 1),
}, values)
} for v in values]
# Walk through our existing pools looking for a match we can use
for pool in pools:


+ 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 akamai.edgegrid import EdgeGridAuth
from urlparse import urljoin
from six.moves.urllib.parse import urljoin
from collections import defaultdict
from logging import getLogger


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

@ -328,7 +328,7 @@ class MythicBeastsProvider(BaseProvider):
exists = True
for line in resp.content.splitlines():
match = MythicBeastsProvider.RE_POPLINE.match(line)
match = MythicBeastsProvider.RE_POPLINE.match(line.decode("utf-8"))
if match is None:
self.log.debug('failed to match line: %s', line)


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

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


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

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


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

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

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

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

@ -8,17 +8,19 @@ from __future__ import absolute_import, division, print_function, \
from boto3 import client
from botocore.config import Config
from collections import defaultdict
from incf.countryutils.transformations import cca_to_ctca2
from ipaddress import AddressValueError, ip_address
from pycountry_convert import country_alpha2_to_continent_code
from uuid import uuid4
import logging
import re
from six import text_type
from ..equality import EqualityTupleMixin
from ..record import Record, Update
from ..record.geo import GeoCodes
from .base import BaseProvider
octal_re = re.compile(r'\\(\d\d\d)')
@ -28,7 +30,7 @@ def _octal_replace(s):
return octal_re.sub(lambda m: chr(int(m.group(1), 8)), s)
class _Route53Record(object):
class _Route53Record(EqualityTupleMixin):
@classmethod
def _new_dynamic(cls, provider, record, hosted_zone_id, creating):
@ -147,7 +149,7 @@ class _Route53Record(object):
}
}
# NOTE: we're using __hash__ and __cmp__ methods that consider
# NOTE: we're using __hash__ and ordering methods that consider
# _Route53Records equivalent if they have the same class, fqdn, and _type.
# Values are ignored. This is useful when computing diffs/changes.
@ -155,17 +157,10 @@ class _Route53Record(object):
'sub-classes should never use this method'
return '{}:{}'.format(self.fqdn, self._type).__hash__()
def __cmp__(self, other):
'''sub-classes should call up to this and return its value if non-zero.
When it's zero they should compute their own __cmp__'''
if self.__class__ != other.__class__:
return cmp(self.__class__, other.__class__)
elif self.fqdn != other.fqdn:
return cmp(self.fqdn, other.fqdn)
elif self._type != other._type:
return cmp(self._type, other._type)
# We're ignoring ttl, it's not an actual differentiator
return 0
def _equality_tuple(self):
'''Sub-classes should call up to this and return its value and add
any additional fields they need to hav considered.'''
return (self.__class__.__name__, self.fqdn, self._type)
def __repr__(self):
return '_Route53Record<{} {} {} {}>'.format(self.fqdn, self._type,
@ -506,11 +501,9 @@ class _Route53GeoRecord(_Route53Record):
return '{}:{}:{}'.format(self.fqdn, self._type,
self.geo.code).__hash__()
def __cmp__(self, other):
ret = super(_Route53GeoRecord, self).__cmp__(other)
if ret != 0:
return ret
return cmp(self.geo.code, other.geo.code)
def _equality_tuple(self):
return super(_Route53GeoRecord, self)._equality_tuple() + \
(self.geo.code,)
def __repr__(self):
return '_Route53GeoRecord<{} {} {} {} {}>'.format(self.fqdn,
@ -553,7 +546,10 @@ def _mod_keyer(mod):
if rrset.get('GeoLocation', False):
unique_id = rrset['SetIdentifier']
else:
unique_id = rrset['Name']
if 'SetIdentifier' in rrset:
unique_id = '{}-{}'.format(rrset['Name'], rrset['SetIdentifier'])
else:
unique_id = rrset['Name']
# Prioritise within the action_priority, ensuring targets come first.
if rrset.get('GeoLocation', False):
@ -700,7 +696,7 @@ class Route53Provider(BaseProvider):
if cc == '*':
# This is the default
return
cn = cca_to_ctca2(cc)
cn = country_alpha2_to_continent_code(cc)
try:
return '{}-{}-{}'.format(cn, cc, loc['SubdivisionCode'])
except KeyError:
@ -1037,8 +1033,8 @@ class Route53Provider(BaseProvider):
# ip_address's returned object for equivalence
# E.g 2001:4860:4860::8842 -> 2001:4860:4860:0:0:0:0:8842
if value:
value = ip_address(unicode(value))
config_ip_address = ip_address(unicode(config['IPAddress']))
value = ip_address(text_type(value))
config_ip_address = ip_address(text_type(config['IPAddress']))
else:
# No value so give this a None to match value's
config_ip_address = None
@ -1059,7 +1055,7 @@ class Route53Provider(BaseProvider):
fqdn, record._type, value)
try:
ip_address(unicode(value))
ip_address(text_type(value))
# We're working with an IP, host is the Host header
healthcheck_host = record.healthcheck_host
except (AddressValueError, ValueError):


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

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


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

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


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

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


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

@ -67,7 +67,8 @@ class TinyDnsBaseSource(BaseSource):
values = []
for record in records:
new_value = record[0].decode('unicode-escape').replace(";", "\\;")
new_value = record[0].encode('latin1').decode('unicode-escape') \
.replace(";", "\\;")
values.append(new_value)
try:
@ -252,7 +253,7 @@ class TinyDnsFileSource(TinyDnsBaseSource):
# Ignore hidden files
continue
with open(join(self.directory, filename), 'r') as fh:
lines += filter(lambda l: l, fh.read().split('\n'))
lines += [l for l in fh.read().split('\n') if l]
self._cache = lines


+ 1
- 2
octodns/yaml.py View File

@ -49,8 +49,7 @@ class SortingDumper(SafeDumper):
'''
def _representer(self, data):
data = data.items()
data.sort(key=lambda d: _natsort_key(d[0]))
data = sorted(data.items(), key=lambda d: _natsort_key(d[0]))
return self.represent_mapping(self.DEFAULT_MAPPING_TAG, data)


+ 5
- 3
octodns/zone.py View File

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


+ 0
- 2
requirements-dev.txt View File

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


+ 7
- 6
requirements.txt View File

@ -1,25 +1,26 @@
PyYaml==4.2b1
azure-common==1.1.18
azure-mgmt-dns==2.1.0
azure-common==1.1.23
azure-mgmt-dns==3.0.0
boto3==1.7.5
botocore==1.10.5
dnspython==1.15.0
docutils==0.14
dyn==1.8.1
edgegrid-python==1.1.1
futures==3.2.0
futures==3.2.0; python_version < '3.0'
google-cloud-core==0.28.1
google-cloud-dns==0.29.0
incf.countryutils==1.0
ipaddress==1.0.22
jmespath==0.9.3
msrestazure==0.6.0
msrestazure==0.6.2
natsort==5.5.0
ns1-python==0.12.0
ovh==0.4.8
pycountry-convert==0.7.2
pycountry==19.8.18
python-dateutil==2.6.1
requests==2.22.0
s3transfer==0.1.13
six==1.11.0
setuptools==38.5.2
six==1.12.0
transip==2.0.0

+ 1
- 1
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 html
coverage xml
coverage report
coverage report --show-missing
coverage report | grep ^TOTAL | grep -qv 100% && {
echo "Incomplete code coverage" >&2
exit 1


+ 7
- 3
setup.py View File

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


+ 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:
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

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


+ 2
- 2
tests/test_octodns_plan.py View File

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


+ 17
- 16
tests/test_octodns_provider_base.py View File

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

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


+ 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 requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record
@ -65,7 +66,7 @@ class TestConstellixProvider(TestCase):
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Unauthorized', ctx.exception.message)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# Bad request
with requests_mock() as mock:
@ -77,7 +78,7 @@ class TestConstellixProvider(TestCase):
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('\n - "unittests" is not a valid domain name',
ctx.exception.message)
text_type(ctx.exception))
# General error
with requests_mock() as mock:
@ -148,6 +149,11 @@ class TestConstellixProvider(TestCase):
call('POST', '/', data={'names': ['unit.tests']}),
# get all domains to build the cache
call('GET', '/'),
])
# These two checks are broken up so that ordering doesn't break things.
# Python3 doesn't make the calls in a consistent order so different
# things follow the GET / on different runs
provider._client._request.assert_has_calls([
call('POST', '/123123/records/SRV', data={
'roundRobin': [{
'priority': 10,


+ 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 requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record
@ -50,7 +51,7 @@ class TestDigitalOceanProvider(TestCase):
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Unauthorized', ctx.exception.message)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# General error
with requests_mock() as mock:
@ -175,7 +176,20 @@ class TestDigitalOceanProvider(TestCase):
call('GET', '/domains/unit.tests/records', {'page': 1}),
# delete the initial A record
call('DELETE', '/domains/unit.tests/records/11189877'),
# created at least one of the record with expected data
# created at least some of the record with expected data
call('POST', '/domains/unit.tests/records', data={
'data': '1.2.3.4',
'name': '@',
'ttl': 300, 'type': 'A'}),
call('POST', '/domains/unit.tests/records', data={
'data': '1.2.3.5',
'name': '@',
'ttl': 300, 'type': 'A'}),
call('POST', '/domains/unit.tests/records', data={
'data': 'ca.unit.tests.',
'flags': 0, 'name': '@',
'tag': 'issue',
'ttl': 3600, 'type': 'CAA'}),
call('POST', '/domains/unit.tests/records', data={
'name': '_srv._tcp',
'weight': 20,


+ 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 requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record
@ -47,7 +48,7 @@ class TestDnsimpleProvider(TestCase):
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Unauthorized', ctx.exception.message)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# General error
with requests_mock() as mock:
@ -138,7 +139,32 @@ class TestDnsimpleProvider(TestCase):
provider._client._request.assert_has_calls([
# created the domain
call('POST', '/domains', data={'name': 'unit.tests'}),
# created at least one of the record with expected data
# created at least some of the record with expected data
call('POST', '/zones/unit.tests/records', data={
'content': '1.2.3.4',
'type': 'A',
'name': '',
'ttl': 300}),
call('POST', '/zones/unit.tests/records', data={
'content': '1.2.3.5',
'type': 'A',
'name': '',
'ttl': 300}),
call('POST', '/zones/unit.tests/records', data={
'content': '0 issue "ca.unit.tests"',
'type': 'CAA',
'name': '',
'ttl': 3600}),
call('POST', '/zones/unit.tests/records', data={
'content': '1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49',
'type': 'SSHFP',
'name': '',
'ttl': 3600}),
call('POST', '/zones/unit.tests/records', data={
'content': '1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73',
'type': 'SSHFP',
'name': '',
'ttl': 3600}),
call('POST', '/zones/unit.tests/records', data={
'content': '20 30 foo-1.unit.tests.',
'priority': 10,


+ 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 requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record
@ -65,7 +66,7 @@ class TestDnsMadeEasyProvider(TestCase):
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Unauthorized', ctx.exception.message)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# Bad request
with requests_mock() as mock:
@ -76,7 +77,7 @@ class TestDnsMadeEasyProvider(TestCase):
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('\n - Rate limit exceeded',
ctx.exception.message)
text_type(ctx.exception))
# General error
with requests_mock() as mock:
@ -148,7 +149,27 @@ class TestDnsMadeEasyProvider(TestCase):
call('POST', '/', data={'name': 'unit.tests'}),
# get all domains to build the cache
call('GET', '/'),
# created at least one of the record with expected data
# created at least some of the record with expected data
call('POST', '/123123/records', data={
'type': 'A',
'name': '',
'value': '1.2.3.4',
'ttl': 300}),
call('POST', '/123123/records', data={
'type': 'A',
'name': '',
'value': '1.2.3.5',
'ttl': 300}),
call('POST', '/123123/records', data={
'type': 'ANAME',
'name': '',
'value': 'aname.unit.tests.',
'ttl': 1800}),
call('POST', '/123123/records', data={
'name': '',
'value': 'ca.unit.tests',
'issuerCritical': 0, 'caaType': 'issue',
'ttl': 3600, 'type': 'CAA'}),
call('POST', '/123123/records', data={
'name': '_srv._tcp',
'weight': 20,


+ 2
- 2
tests/test_octodns_provider_dyn.py View File

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


+ 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 requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record
@ -147,4 +148,4 @@ class TestFastdnsProvider(TestCase):
changes = provider.apply(plan)
except NameError as e:
expected = "contractId not specified to create zone"
self.assertEquals(e.message, expected)
self.assertEquals(text_type(e), expected)

+ 7
- 2
tests/test_octodns_provider_googlecloud.py View File

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


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

+ 10
- 5
tests/test_octodns_provider_ns1.py View File

@ -5,6 +5,7 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from collections import defaultdict
from mock import Mock, call, patch
from ns1.rest.errors import AuthException, RateLimitException, \
ResourceException
@ -373,8 +374,12 @@ class TestNs1Provider(TestCase):
load_mock.side_effect = [nsone_zone, nsone_zone]
plan = provider.plan(desired)
self.assertEquals(3, len(plan.changes))
self.assertIsInstance(plan.changes[0], Update)
self.assertIsInstance(plan.changes[2], Delete)
# Shouldn't rely on order so just count classes
classes = defaultdict(lambda: 0)
for change in plan.changes:
classes[change.__class__] += 1
self.assertEquals(1, classes[Delete])
self.assertEquals(2, classes[Update])
# ugh, we need a mock record that can be returned from loadRecord for
# the update and delete targets, we can add our side effects to that to
# trigger rate limit handling
@ -395,8 +400,8 @@ class TestNs1Provider(TestCase):
self.assertEquals(3, got_n)
nsone_zone.loadRecord.assert_has_calls([
call('unit.tests', u'A'),
call('geo', u'A'),
call('delete-me', u'A'),
call('geo', u'A'),
])
mock_record.assert_has_calls([
call.update(answers=[{'answer': [u'1.2.3.4'], 'meta': {}}],
@ -405,6 +410,8 @@ class TestNs1Provider(TestCase):
call.update(answers=[{u'answer': [u'1.2.3.4'], u'meta': {}}],
filters=[],
ttl=32),
call.delete(),
call.delete(),
call.update(
answers=[
{u'answer': [u'101.102.103.104'], u'meta': {}},
@ -422,8 +429,6 @@ class TestNs1Provider(TestCase):
{u'filter': u'select_first_n', u'config': {u'N': 1}},
],
ttl=34),
call.delete(),
call.delete()
])
def test_escaping(self):


+ 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]]
provider.apply(plan)
wanted_calls = [
call(u'/domain/zone/unit.tests/record', fieldType=u'TXT',
subDomain='txt', target=u'TXT text', ttl=1400),
call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM',
subDomain='dkim', target=self.valid_dkim_key,
ttl=1300),
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
subDomain=u'', target=u'1.2.3.4', ttl=100),
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
call('/domain/zone/unit.tests/record', fieldType='A',
subDomain='', target='1.2.3.4', ttl=100),
call('/domain/zone/unit.tests/record', fieldType='AAAA',
subDomain='', target='1:1ec:1::1', ttl=200),
call('/domain/zone/unit.tests/record', fieldType='MX',
subDomain='', target='10 mx1.unit.tests.', ttl=400),
call('/domain/zone/unit.tests/record', fieldType='SPF',
subDomain='',
target='v=spf1 include:unit.texts.redirect ~all',
ttl=1000),
call('/domain/zone/unit.tests/record', fieldType='SSHFP',
subDomain='',
target='1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73',
ttl=1100),
call('/domain/zone/unit.tests/record', fieldType='PTR',
subDomain='4', target='unit.tests.', ttl=900),
call('/domain/zone/unit.tests/record', fieldType='SRV',
subDomain='_srv._tcp',
target=u'10 20 30 foo-1.unit.tests.', ttl=800),
call(u'/domain/zone/unit.tests/record', fieldType=u'SRV',
target='10 20 30 foo-1.unit.tests.', ttl=800),
call('/domain/zone/unit.tests/record', fieldType='SRV',
subDomain='_srv._tcp',
target=u'40 50 60 foo-2.unit.tests.', ttl=800),
call(u'/domain/zone/unit.tests/record', fieldType=u'PTR',
subDomain='4', target=u'unit.tests.', ttl=900),
call(u'/domain/zone/unit.tests/record', fieldType=u'NS',
subDomain='www3', target=u'ns3.unit.tests.', ttl=700),
call(u'/domain/zone/unit.tests/record', fieldType=u'NS',
subDomain='www3', target=u'ns4.unit.tests.', ttl=700),
call(u'/domain/zone/unit.tests/record',
fieldType=u'SSHFP', subDomain=u'', ttl=1100,
target=u'1 1 bf6b6825d2977c511a475bbefb88a'
u'ad54'
u'a92ac73',
),
call(u'/domain/zone/unit.tests/record', fieldType=u'AAAA',
subDomain=u'', target=u'1:1ec:1::1', ttl=200),
call(u'/domain/zone/unit.tests/record', fieldType=u'MX',
subDomain=u'', target=u'10 mx1.unit.tests.', ttl=400),
call(u'/domain/zone/unit.tests/record', fieldType=u'CNAME',
subDomain='www2', target=u'unit.tests.', ttl=300),
call(u'/domain/zone/unit.tests/record', fieldType=u'SPF',
subDomain=u'', ttl=1000,
target=u'v=spf1 include:unit.texts.'
u'redirect ~all',
),
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
subDomain='sub', target=u'1.2.3.4', ttl=200),
call(u'/domain/zone/unit.tests/record', fieldType=u'NAPTR',
subDomain='naptr', ttl=500,
target=u'10 100 "S" "SIP+D2U" "!^.*$!sip:'
u'info@bar'
u'.example.com!" .'
),
call(u'/domain/zone/unit.tests/refresh')]
target='40 50 60 foo-2.unit.tests.', ttl=800),
call('/domain/zone/unit.tests/record', fieldType='DKIM',
subDomain='dkim',
target='p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCxLaG'
'16G4SaEcXVdiIxTg7gKSGbHKQLm30CHib1h9FzS9nkcyvQSyQj1r'
'MFyqC//tft3ohx3nvJl+bGCWxdtLYDSmir9PW54e5CTdxEh8MWRk'
'BO3StF6QG/tAh3aTGDmkqhIJGLb87iHvpmVKqURmEUzJPv5KPJfW'
'LofADI+q9lQIDAQAB', ttl=1300),
call('/domain/zone/unit.tests/record', fieldType='NAPTR',
subDomain='naptr',
target='10 100 "S" "SIP+D2U" "!^.*$!sip: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)
# Get for delete calls
wanted_get_calls = [
call(u'/domain/zone/unit.tests/record', fieldType=u'TXT',
subDomain='txt'),
call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM',
subDomain='dkim'),
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
subDomain=u''),
call(u'/domain/zone/unit.tests/record', fieldType=u'DKIM',
subDomain='dkim'),
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
subDomain='fake')]
subDomain='fake'),
call(u'/domain/zone/unit.tests/record', fieldType=u'TXT',
subDomain='txt')]
get_mock.assert_has_calls(wanted_get_calls)
# 4 delete calls for update and delete
delete_mock.assert_has_calls(


+ 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 requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record
@ -52,7 +53,7 @@ class TestPowerDnsProvider(TestCase):
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertTrue('unauthorized' in ctx.exception.message)
self.assertTrue('unauthorized' in text_type(ctx.exception))
# General error
with requests_mock() as mock:


+ 7
- 7
tests/test_octodns_provider_rackspace.py View File

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


+ 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.stub import ANY, Stubber
from six import text_type
from unittest import TestCase
from mock import patch
@ -1881,10 +1882,10 @@ class TestRoute53Provider(TestCase):
@patch('octodns.provider.route53.Route53Provider._really_apply')
def test_apply_3(self, really_apply_mock, _):
# with a max of seven modifications, four calls
# with a max of seven modifications, three calls
provider, plan = self._get_test_plan(7)
provider.apply(plan)
self.assertEquals(4, really_apply_mock.call_count)
self.assertEquals(3, really_apply_mock.call_count)
@patch('octodns.provider.route53.Route53Provider._load_records')
@patch('octodns.provider.route53.Route53Provider._really_apply')
@ -1903,7 +1904,7 @@ class TestRoute53Provider(TestCase):
provider, plan = self._get_test_plan(1)
with self.assertRaises(Exception) as ctx:
provider.apply(plan)
self.assertTrue('modifications' in ctx.exception.message)
self.assertTrue('modifications' in text_type(ctx.exception))
def test_semicolon_fixup(self):
provider = Route53Provider('test', 'abc', '123')
@ -2090,6 +2091,58 @@ class TestRoute53Records(TestCase):
e.__repr__()
f.__repr__()
def test_route53_record_ordering(self):
# Matches
a = _Route53Record(None, self.record_a, False)
b = _Route53Record(None, self.record_a, False)
self.assertTrue(a == b)
self.assertFalse(a != b)
self.assertFalse(a < b)
self.assertTrue(a <= b)
self.assertFalse(a > b)
self.assertTrue(a >= b)
# Change the fqdn is greater
fqdn = _Route53Record(None, self.record_a, False,
fqdn_override='other')
self.assertFalse(a == fqdn)
self.assertTrue(a != fqdn)
self.assertFalse(a < fqdn)
self.assertFalse(a <= fqdn)
self.assertTrue(a > fqdn)
self.assertTrue(a >= fqdn)
provider = DummyProvider()
geo_a = _Route53GeoRecord(provider, self.record_a, 'NA-US',
self.record_a.geo['NA-US'], False)
geo_b = _Route53GeoRecord(provider, self.record_a, 'NA-US',
self.record_a.geo['NA-US'], False)
self.assertTrue(geo_a == geo_b)
self.assertFalse(geo_a != geo_b)
self.assertFalse(geo_a < geo_b)
self.assertTrue(geo_a <= geo_b)
self.assertFalse(geo_a > geo_b)
self.assertTrue(geo_a >= geo_b)
# Other base
geo_fqdn = _Route53GeoRecord(provider, self.record_a, 'NA-US',
self.record_a.geo['NA-US'], False)
geo_fqdn.fqdn = 'other'
self.assertFalse(geo_a == geo_fqdn)
self.assertTrue(geo_a != geo_fqdn)
self.assertFalse(geo_a < geo_fqdn)
self.assertFalse(geo_a <= geo_fqdn)
self.assertTrue(geo_a > geo_fqdn)
self.assertTrue(geo_a >= geo_fqdn)
# Other class
self.assertFalse(a == geo_a)
self.assertTrue(a != geo_a)
self.assertFalse(a < geo_a)
self.assertFalse(a <= geo_a)
self.assertTrue(a > geo_a)
self.assertTrue(a >= geo_a)
def test_dynamic_value_delete(self):
provider = DummyProvider()
geo = _Route53DynamicValue(provider, self.record_a, 'iad', '2.2.2.2',
@ -2206,70 +2259,112 @@ class TestRoute53Records(TestCase):
creating=True)
self.assertEquals(18, len(route53_records))
expected_mods = [r.mod('CREATE', []) for r in route53_records]
# Sort so that we get a consistent order and don't rely on set ordering
expected_mods.sort(key=_mod_keyer)
# Convert the route53_records into mods
self.assertEquals([{
'Action': 'CREATE',
'ResourceRecordSet': {
'HealthCheckId': 'hc42',
'Name': '_octodns-ap-southeast-1-value.unit.tests.',
'ResourceRecords': [{
'Value': '1.4.1.2'}],
'SetIdentifier': 'ap-southeast-1-001',
'ResourceRecords': [{'Value': '1.4.1.1'}],
'SetIdentifier': 'ap-southeast-1-000',
'TTL': 60,
'Type': 'A',
'Weight': 2
}
'Weight': 2}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'HealthCheckId': 'hc42',
'Name': '_octodns-ap-southeast-1-value.unit.tests.',
'ResourceRecords': [{
'Value': '1.4.1.1'}],
'SetIdentifier': 'ap-southeast-1-000',
'ResourceRecords': [{'Value': '1.4.1.2'}],
'SetIdentifier': 'ap-southeast-1-001',
'TTL': 60,
'Type': 'A',
'Weight': 2
}
'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',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
'DNSName': '_octodns-ap-southeast-1-value.unit.tests.',
'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',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
'DNSName': '_octodns-eu-central-1-value.unit.tests.',
'EvaluateTargetHealth': True,
'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'}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
'DNSName': '_octodns-us-east-1-value.unit.tests.',
'EvaluateTargetHealth': True,
'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'}
}, {
'Action': 'CREATE',
@ -2286,123 +2381,72 @@ class TestRoute53Records(TestCase):
'Action': 'CREATE',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
'EvaluateTargetHealth': True,
'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'}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-us-east-1-value.unit.tests.',
'DNSName': '_octodns-default-pool.unit.tests.',
'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'},
'Failover': 'PRIMARY',
'Failover': 'SECONDARY',
'Name': '_octodns-us-east-1-pool.unit.tests.',
'SetIdentifier': 'us-east-1-Primary',
'SetIdentifier': 'us-east-1-Secondary-default',
'Type': 'A'}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'},
'GeoLocation': {
'ContinentCode': 'EU'},
'CountryCode': 'CN'},
'Name': 'unit.tests.',
'SetIdentifier': '1-eu-central-1-EU',
'SetIdentifier': '0-ap-southeast-1-AS-CN',
'Type': 'A'}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-eu-central-1-value.unit.tests.',
'DNSName': '_octodns-ap-southeast-1-pool.unit.tests.',
'EvaluateTargetHealth': True,
'HostedZoneId': 'z45'},
'Failover': 'PRIMARY',
'Name': '_octodns-eu-central-1-pool.unit.tests.',
'SetIdentifier': 'eu-central-1-Primary',
'Type': 'A'}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'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'}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'HealthCheckId': 'hc42',
'Name': '_octodns-eu-central-1-value.unit.tests.',
'ResourceRecords': [{
'Value': '1.3.1.2'}],
'SetIdentifier': 'eu-central-1-001',
'TTL': 60,
'Type': 'A',
'Weight': 1}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'HealthCheckId': 'hc42',
'Name': '_octodns-eu-central-1-value.unit.tests.',
'ResourceRecords': [{
'Value': '1.3.1.1'}],
'SetIdentifier': 'eu-central-1-000',
'TTL': 60,
'Type': 'A',
'Weight': 1}
}, {
'Action': 'CREATE',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-default-pool.unit.tests.',
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
'EvaluateTargetHealth': True,
'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'}
}, {
'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',
'ResourceRecordSet': {
'AliasTarget': {
'DNSName': '_octodns-ap-southeast-1-value.unit.tests.',
'DNSName': '_octodns-eu-central-1-pool.unit.tests.',
'EvaluateTargetHealth': True,
'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'}
}, {
'Action': 'CREATE',
@ -2411,11 +2455,12 @@ class TestRoute53Records(TestCase):
'DNSName': '_octodns-us-east-1-pool.unit.tests.',
'EvaluateTargetHealth': True,
'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'}
}], [r.mod('CREATE', []) for r in route53_records])
}], expected_mods)
for route53_record in route53_records:
# 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
from unittest import TestCase
from six import text_type
import requests_mock
@ -288,7 +289,7 @@ class TestSelectelProvider(TestCase):
with self.assertRaises(Exception) as ctx:
SelectelProvider(123, 'fail_token')
self.assertEquals(ctx.exception.message,
self.assertEquals(text_type(ctx.exception),
'Authorization failed. Invalid or empty token.')
@requests_mock.Mocker()


+ 2
- 2
tests/test_octodns_provider_transip.py View File

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


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

+ 391
- 30
tests/test_octodns_record.py View File

@ -5,12 +5,14 @@
from __future__ import absolute_import, division, print_function, \
unicode_literals
from six import text_type
from unittest import TestCase
from octodns.record import ARecord, AaaaRecord, AliasRecord, CaaRecord, \
CnameRecord, Create, Delete, GeoValue, MxRecord, NaptrRecord, NaptrValue, \
NsRecord, PtrRecord, Record, SshfpRecord, SpfRecord, SrvRecord, \
TxtRecord, Update, ValidationError, _Dynamic, _DynamicPool, _DynamicRule
CaaValue, CnameRecord, Create, Delete, GeoValue, MxRecord, MxValue, \
NaptrRecord, NaptrValue, NsRecord, PtrRecord, Record, SshfpRecord, \
SshfpValue, SpfRecord, SrvRecord, SrvValue, TxtRecord, Update, \
ValidationError, _Dynamic, _DynamicPool, _DynamicRule
from octodns.zone import Zone
from helpers import DynamicProvider, GeoProvider, SimpleProvider
@ -482,113 +484,140 @@ class TestRecord(TestCase):
# full sorting
# equivalent
b_naptr_value = b.values[0]
self.assertEquals(0, b_naptr_value.__cmp__(b_naptr_value))
self.assertTrue(b_naptr_value == b_naptr_value)
self.assertFalse(b_naptr_value != b_naptr_value)
self.assertTrue(b_naptr_value <= b_naptr_value)
self.assertTrue(b_naptr_value >= b_naptr_value)
# by order
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 10,
'preference': 31,
'flags': 'M',
'service': 'N',
'regexp': 'O',
'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 40,
'preference': 31,
'flags': 'M',
'service': 'N',
'regexp': 'O',
'replacement': 'x',
})))
}))
# by preference
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30,
'preference': 10,
'flags': 'M',
'service': 'N',
'regexp': 'O',
'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30,
'preference': 40,
'flags': 'M',
'service': 'N',
'regexp': 'O',
'replacement': 'x',
})))
}))
# by flags
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30,
'preference': 31,
'flags': 'A',
'service': 'N',
'regexp': 'O',
'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30,
'preference': 31,
'flags': 'Z',
'service': 'N',
'regexp': 'O',
'replacement': 'x',
})))
}))
# by service
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30,
'preference': 31,
'flags': 'M',
'service': 'A',
'regexp': 'O',
'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30,
'preference': 31,
'flags': 'M',
'service': 'Z',
'regexp': 'O',
'replacement': 'x',
})))
}))
# by regexp
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30,
'preference': 31,
'flags': 'M',
'service': 'N',
'regexp': 'A',
'replacement': 'x',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30,
'preference': 31,
'flags': 'M',
'service': 'N',
'regexp': 'Z',
'replacement': 'x',
})))
}))
# by replacement
self.assertEquals(1, b_naptr_value.__cmp__(NaptrValue({
self.assertTrue(b_naptr_value > NaptrValue({
'order': 30,
'preference': 31,
'flags': 'M',
'service': 'N',
'regexp': 'O',
'replacement': 'a',
})))
self.assertEquals(-1, b_naptr_value.__cmp__(NaptrValue({
}))
self.assertTrue(b_naptr_value < NaptrValue({
'order': 30,
'preference': 31,
'flags': 'M',
'service': 'N',
'regexp': 'O',
'replacement': 'z',
})))
}))
# __repr__ doesn't blow up
a.__repr__()
# Hash
v = NaptrValue({
'order': 30,
'preference': 31,
'flags': 'M',
'service': 'N',
'regexp': 'O',
'replacement': 'z',
})
o = NaptrValue({
'order': 30,
'preference': 32,
'flags': 'M',
'service': 'N',
'regexp': 'O',
'replacement': 'z',
})
values = set()
values.add(v)
self.assertTrue(v in values)
self.assertFalse(o in values)
values.add(o)
self.assertTrue(o in values)
def test_ns(self):
a_values = ['5.6.7.8.', '6.7.8.9.', '7.8.9.0.']
a_data = {'ttl': 30, 'values': a_values}
@ -758,14 +787,14 @@ class TestRecord(TestCase):
# Missing type
with self.assertRaises(Exception) as ctx:
Record.new(self.zone, 'unknown', {})
self.assertTrue('missing type' in ctx.exception.message)
self.assertTrue('missing type' in text_type(ctx.exception))
# Unknown type
with self.assertRaises(Exception) as ctx:
Record.new(self.zone, 'unknown', {
'type': 'XXX',
})
self.assertTrue('Unknown record type' in ctx.exception.message)
self.assertTrue('Unknown record type' in text_type(ctx.exception))
def test_change(self):
existing = Record.new(self.zone, 'txt', {
@ -796,6 +825,38 @@ class TestRecord(TestCase):
self.assertEquals(values, geo.values)
self.assertEquals(['NA-US', 'NA'], list(geo.parents))
a = GeoValue('NA-US-CA', values)
b = GeoValue('AP-JP', values)
c = GeoValue('NA-US-CA', ['2.3.4.5'])
self.assertEqual(a, a)
self.assertEqual(b, b)
self.assertEqual(c, c)
self.assertNotEqual(a, b)
self.assertNotEqual(a, c)
self.assertNotEqual(b, a)
self.assertNotEqual(b, c)
self.assertNotEqual(c, a)
self.assertNotEqual(c, b)
self.assertTrue(a > b)
self.assertTrue(a < c)
self.assertTrue(b < a)
self.assertTrue(b < c)
self.assertTrue(c > a)
self.assertTrue(c > b)
self.assertTrue(a >= a)
self.assertTrue(a >= b)
self.assertTrue(a <= c)
self.assertTrue(b <= a)
self.assertTrue(b <= b)
self.assertTrue(b <= c)
self.assertTrue(c > a)
self.assertTrue(c > b)
self.assertTrue(c >= b)
def test_healthcheck(self):
new = Record.new(self.zone, 'a', {
'ttl': 44,
@ -851,6 +912,306 @@ class TestRecord(TestCase):
})
self.assertFalse(new.ignored)
def test_ordering_functions(self):
a = Record.new(self.zone, 'a', {
'ttl': 44,
'type': 'A',
'value': '1.2.3.4',
})
b = Record.new(self.zone, 'b', {
'ttl': 44,
'type': 'A',
'value': '1.2.3.4',
})
c = Record.new(self.zone, 'c', {
'ttl': 44,
'type': 'A',
'value': '1.2.3.4',
})
aaaa = Record.new(self.zone, 'a', {
'ttl': 44,
'type': 'AAAA',
'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
})
self.assertEquals(a, a)
self.assertEquals(b, b)
self.assertEquals(c, c)
self.assertEquals(aaaa, aaaa)
self.assertNotEqual(a, b)
self.assertNotEqual(a, c)
self.assertNotEqual(a, aaaa)
self.assertNotEqual(b, a)
self.assertNotEqual(b, c)
self.assertNotEqual(b, aaaa)
self.assertNotEqual(c, a)
self.assertNotEqual(c, b)
self.assertNotEqual(c, aaaa)
self.assertNotEqual(aaaa, a)
self.assertNotEqual(aaaa, b)
self.assertNotEqual(aaaa, c)
self.assertTrue(a < b)
self.assertTrue(a < c)
self.assertTrue(a < aaaa)
self.assertTrue(b > a)
self.assertTrue(b < c)
self.assertTrue(b > aaaa)
self.assertTrue(c > a)
self.assertTrue(c > b)
self.assertTrue(c > aaaa)
self.assertTrue(aaaa > a)
self.assertTrue(aaaa < b)
self.assertTrue(aaaa < c)
self.assertTrue(a <= a)
self.assertTrue(a <= b)
self.assertTrue(a <= c)
self.assertTrue(a <= aaaa)
self.assertTrue(b >= a)
self.assertTrue(b >= b)
self.assertTrue(b <= c)
self.assertTrue(b >= aaaa)
self.assertTrue(c >= a)
self.assertTrue(c >= b)
self.assertTrue(c >= c)
self.assertTrue(c >= aaaa)
self.assertTrue(aaaa >= a)
self.assertTrue(aaaa <= b)
self.assertTrue(aaaa <= c)
self.assertTrue(aaaa <= aaaa)
def test_caa_value(self):
a = CaaValue({'flags': 0, 'tag': 'a', 'value': 'v'})
b = CaaValue({'flags': 1, 'tag': 'a', 'value': 'v'})
c = CaaValue({'flags': 0, 'tag': 'c', 'value': 'v'})
d = CaaValue({'flags': 0, 'tag': 'a', 'value': 'z'})
self.assertEqual(a, a)
self.assertEqual(b, b)
self.assertEqual(c, c)
self.assertEqual(d, d)
self.assertNotEqual(a, b)
self.assertNotEqual(a, c)
self.assertNotEqual(a, d)
self.assertNotEqual(b, a)
self.assertNotEqual(b, c)
self.assertNotEqual(b, d)
self.assertNotEqual(c, a)
self.assertNotEqual(c, b)
self.assertNotEqual(c, d)
self.assertTrue(a < b)
self.assertTrue(a < c)
self.assertTrue(a < d)
self.assertTrue(b > a)
self.assertTrue(b > c)
self.assertTrue(b > d)
self.assertTrue(c > a)
self.assertTrue(c < b)
self.assertTrue(c > d)
self.assertTrue(d > a)
self.assertTrue(d < b)
self.assertTrue(d < c)
self.assertTrue(a <= b)
self.assertTrue(a <= c)
self.assertTrue(a <= d)
self.assertTrue(a <= a)
self.assertTrue(a >= a)
self.assertTrue(b >= a)
self.assertTrue(b >= c)
self.assertTrue(b >= d)
self.assertTrue(b >= b)
self.assertTrue(b <= b)
self.assertTrue(c >= a)
self.assertTrue(c <= b)
self.assertTrue(c >= d)
self.assertTrue(c >= c)
self.assertTrue(c <= c)
self.assertTrue(d >= a)
self.assertTrue(d <= b)
self.assertTrue(d <= c)
self.assertTrue(d >= d)
self.assertTrue(d <= d)
def test_mx_value(self):
a = MxValue({'preference': 0, 'priority': 'a', 'exchange': 'v',
'value': '1'})
b = MxValue({'preference': 10, 'priority': 'a', 'exchange': 'v',
'value': '2'})
c = MxValue({'preference': 0, 'priority': 'b', 'exchange': 'z',
'value': '3'})
self.assertEqual(a, a)
self.assertEqual(b, b)
self.assertEqual(c, c)
self.assertNotEqual(a, b)
self.assertNotEqual(a, c)
self.assertNotEqual(b, a)
self.assertNotEqual(b, c)
self.assertNotEqual(c, a)
self.assertNotEqual(c, b)
self.assertTrue(a < b)
self.assertTrue(a < c)
self.assertTrue(b > a)
self.assertTrue(b > c)
self.assertTrue(c > a)
self.assertTrue(c < b)
self.assertTrue(a <= b)
self.assertTrue(a <= c)
self.assertTrue(a <= a)
self.assertTrue(a >= a)
self.assertTrue(b >= a)
self.assertTrue(b >= c)
self.assertTrue(b >= b)
self.assertTrue(b <= b)
self.assertTrue(c >= a)
self.assertTrue(c <= b)
self.assertTrue(c >= c)
self.assertTrue(c <= c)
def test_sshfp_value(self):
a = SshfpValue({'algorithm': 0, 'fingerprint_type': 0,
'fingerprint': 'abcd'})
b = SshfpValue({'algorithm': 1, 'fingerprint_type': 0,
'fingerprint': 'abcd'})
c = SshfpValue({'algorithm': 0, 'fingerprint_type': 1,
'fingerprint': 'abcd'})
d = SshfpValue({'algorithm': 0, 'fingerprint_type': 0,
'fingerprint': 'bcde'})
self.assertEqual(a, a)
self.assertEqual(b, b)
self.assertEqual(c, c)
self.assertEqual(d, d)
self.assertNotEqual(a, b)
self.assertNotEqual(a, c)
self.assertNotEqual(a, d)
self.assertNotEqual(b, a)
self.assertNotEqual(b, c)
self.assertNotEqual(b, d)
self.assertNotEqual(c, a)
self.assertNotEqual(c, b)
self.assertNotEqual(c, d)
self.assertNotEqual(d, a)
self.assertNotEqual(d, b)
self.assertNotEqual(d, c)
self.assertTrue(a < b)
self.assertTrue(a < c)
self.assertTrue(b > a)
self.assertTrue(b > c)
self.assertTrue(c > a)
self.assertTrue(c < b)
self.assertTrue(a <= b)
self.assertTrue(a <= c)
self.assertTrue(a <= a)
self.assertTrue(a >= a)
self.assertTrue(b >= a)
self.assertTrue(b >= c)
self.assertTrue(b >= b)
self.assertTrue(b <= b)
self.assertTrue(c >= a)
self.assertTrue(c <= b)
self.assertTrue(c >= c)
self.assertTrue(c <= c)
# Hash
values = set()
values.add(a)
self.assertTrue(a in values)
self.assertFalse(b in values)
values.add(b)
self.assertTrue(b in values)
def test_srv_value(self):
a = SrvValue({'priority': 0, 'weight': 0, 'port': 0, 'target': 'foo.'})
b = SrvValue({'priority': 1, 'weight': 0, 'port': 0, 'target': 'foo.'})
c = SrvValue({'priority': 0, 'weight': 2, 'port': 0, 'target': 'foo.'})
d = SrvValue({'priority': 0, 'weight': 0, 'port': 3, 'target': 'foo.'})
e = SrvValue({'priority': 0, 'weight': 0, 'port': 0, 'target': 'mmm.'})
self.assertEqual(a, a)
self.assertEqual(b, b)
self.assertEqual(c, c)
self.assertEqual(d, d)
self.assertEqual(e, e)
self.assertNotEqual(a, b)
self.assertNotEqual(a, c)
self.assertNotEqual(a, d)
self.assertNotEqual(a, e)
self.assertNotEqual(b, a)
self.assertNotEqual(b, c)
self.assertNotEqual(b, d)
self.assertNotEqual(b, e)
self.assertNotEqual(c, a)
self.assertNotEqual(c, b)
self.assertNotEqual(c, d)
self.assertNotEqual(c, e)
self.assertNotEqual(d, a)
self.assertNotEqual(d, b)
self.assertNotEqual(d, c)
self.assertNotEqual(d, e)
self.assertNotEqual(e, a)
self.assertNotEqual(e, b)
self.assertNotEqual(e, c)
self.assertNotEqual(e, d)
self.assertTrue(a < b)
self.assertTrue(a < c)
self.assertTrue(b > a)
self.assertTrue(b > c)
self.assertTrue(c > a)
self.assertTrue(c < b)
self.assertTrue(a <= b)
self.assertTrue(a <= c)
self.assertTrue(a <= a)
self.assertTrue(a >= a)
self.assertTrue(b >= a)
self.assertTrue(b >= c)
self.assertTrue(b >= b)
self.assertTrue(b <= b)
self.assertTrue(c >= a)
self.assertTrue(c <= b)
self.assertTrue(c >= c)
self.assertTrue(c <= c)
# Hash
values = set()
values.add(a)
self.assertTrue(a in values)
self.assertFalse(b in values)
values.add(b)
self.assertTrue(b in values)
class TestRecordValidation(TestCase):
zone = Zone('unit.tests.', [])


+ 3
- 2
tests/test_octodns_source_axfr.py View File

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

+ 1
- 1
tests/test_octodns_yaml.py View File

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


+ 6
- 5
tests/test_octodns_zone.py View File

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


Loading…
Cancel
Save