Browse Source

Merge pull request #303 from github/dynamic-records

Base dynamic record support
pull/310/head
Ross McFarland 7 years ago
committed by GitHub
parent
commit
c0730918a6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2662 additions and 302 deletions
  1. +19
    -0
      CHANGELOG.md
  2. +7
    -0
      octodns/manager.py
  3. +1
    -0
      octodns/provider/azuredns.py
  4. +1
    -0
      octodns/provider/cloudflare.py
  5. +1
    -0
      octodns/provider/digitalocean.py
  6. +1
    -0
      octodns/provider/dnsimple.py
  7. +1
    -0
      octodns/provider/dnsmadeeasy.py
  8. +5
    -0
      octodns/provider/dyn.py
  9. +1
    -0
      octodns/provider/etc_hosts.py
  10. +1
    -0
      octodns/provider/googlecloud.py
  11. +1
    -0
      octodns/provider/ns1.py
  12. +1
    -0
      octodns/provider/ovh.py
  13. +1
    -0
      octodns/provider/powerdns.py
  14. +1
    -0
      octodns/provider/rackspace.py
  15. +2
    -0
      octodns/provider/route53.py
  16. +1
    -0
      octodns/provider/yaml.py
  17. +526
    -264
      octodns/record/__init__.py
  18. +35
    -0
      octodns/record/geo.py
  19. +316
    -0
      octodns/record/geo_data.py
  20. +1
    -0
      octodns/source/axfr.py
  21. +3
    -0
      octodns/source/base.py
  22. +1
    -0
      octodns/source/tinydns.py
  23. +2
    -0
      requirements-dev.txt
  24. +64
    -0
      script/generate-geo-data
  25. +197
    -0
      tests/config/dynamic.tests.yaml
  26. +20
    -0
      tests/helpers.py
  27. +8
    -2
      tests/test_octodns_manager.py
  28. +10
    -2
      tests/test_octodns_provider_base.py
  29. +60
    -9
      tests/test_octodns_provider_yaml.py
  30. +1320
    -25
      tests/test_octodns_record.py
  31. +53
    -0
      tests/test_octodns_record_geo.py
  32. +1
    -0
      tests/test_octodns_zone.py

+ 19
- 0
CHANGELOG.md View File

@ -1,3 +1,22 @@
## v0.9.4 - ....-..-.. - Dynamic Records Beta
* Dynamic Records (more powerful/flexible replacement for geo)
* Will support A, AAAA, and CNAME out the gate and include the ability to
weight records. It should provide a foundation for further suppport
if/when needed.
* Major refactoring and improvements to validation of the Record hierarchy,
things are much more consisntely implemented now and error messages should
be more actionable/clear. Both the base values and dynamic values use the
same validatio logic.
* natsort version bump to address setup issues
* DNSSimple TXT record handling fixes, ; it's always ;
* Route53Provider support for sessiom tokens
* Add ALIAS to the list of Cloudflare record types that support proxying
* Fix for TTL bug in Dyn CCA records
* Records updated so that 'octodns' record metadata is persisted through
YamlProvider
* Added --version support to ArguementParser (thus all commands)
## v0.9.3 - 2018-10-29 - Misc. stuff sort of release ## v0.9.3 - 2018-10-29 - Misc. stuff sort of release
* ZoneFile source added * ZoneFile source added


+ 7
- 0
octodns/manager.py View File

@ -37,6 +37,13 @@ class _AggregateTarget(object):
return False return False
return True return True
@property
def SUPPORTS_DYNAMIC(self):
for target in self.targets:
if not target.SUPPORTS_DYNAMIC:
return False
return True
class MakeThreadFuture(object): class MakeThreadFuture(object):


+ 1
- 0
octodns/provider/azuredns.py View File

@ -249,6 +249,7 @@ class AzureProvider(BaseProvider):
possible to also hard-code into the config file: eg, resource_group. possible to also hard-code into the config file: eg, resource_group.
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT')) SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT'))
def __init__(self, id, client_id, key, directory_id, sub_id, def __init__(self, id, client_id, key, directory_id, sub_id,


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

@ -59,6 +59,7 @@ class CloudflareProvider(BaseProvider):
value: 1.2.3.4 value: 1.2.3.4
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SRV', SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SRV',
'SPF', 'TXT')) 'SPF', 'TXT'))


+ 1
- 0
octodns/provider/digitalocean.py View File

@ -116,6 +116,7 @@ class DigitalOceanProvider(BaseProvider):
token: foo token: foo
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV')) SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'TXT', 'SRV'))
def __init__(self, id, token, *args, **kwargs): def __init__(self, id, token, *args, **kwargs):


+ 1
- 0
octodns/provider/dnsimple.py View File

@ -91,6 +91,7 @@ class DnsimpleProvider(BaseProvider):
account: 42 account: 42
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
'PTR', 'SPF', 'SRV', 'SSHFP', 'TXT')) 'PTR', 'SPF', 'SRV', 'SSHFP', 'TXT'))


+ 1
- 0
octodns/provider/dnsmadeeasy.py View File

@ -158,6 +158,7 @@ class DnsMadeEasyProvider(BaseProvider):
sandbox: true sandbox: true
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX',
'NS', 'PTR', 'SPF', 'SRV', 'TXT')) 'NS', 'PTR', 'SPF', 'SRV', 'TXT'))


+ 5
- 0
octodns/provider/dyn.py View File

@ -259,6 +259,11 @@ class DynProvider(BaseProvider):
def SUPPORTS_GEO(self): def SUPPORTS_GEO(self):
return self.traffic_directors_enabled return self.traffic_directors_enabled
@property
def SUPPORTS_DYNAMIC(self):
# TODO: dynamic
return False
def _check_dyn_sess(self): def _check_dyn_sess(self):
# We don't have to worry about locking for the check since the # We don't have to worry about locking for the check since the
# underlying pieces are pre-thread. We can check to see if this thread # underlying pieces are pre-thread. We can check to see if this thread


+ 1
- 0
octodns/provider/etc_hosts.py View File

@ -25,6 +25,7 @@ class EtcHostsProvider(BaseProvider):
directory: ./hosts directory: ./hosts
''' '''
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME')) SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME'))
def __init__(self, id, directory, *args, **kwargs): def __init__(self, id, directory, *args, **kwargs):


+ 1
- 0
octodns/provider/googlecloud.py View File

@ -40,6 +40,7 @@ class GoogleCloudProvider(BaseProvider):
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR',
'NS', 'PTR', 'SPF', 'SRV', 'TXT')) 'NS', 'PTR', 'SPF', 'SRV', 'TXT'))
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
CHANGE_LOOP_WAIT = 5 CHANGE_LOOP_WAIT = 5


+ 1
- 0
octodns/provider/ns1.py View File

@ -26,6 +26,7 @@ class Ns1Provider(BaseProvider):
api_key: env/NS1_API_KEY api_key: env/NS1_API_KEY
''' '''
SUPPORTS_GEO = True SUPPORTS_GEO = True
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR',
'NS', 'PTR', 'SPF', 'SRV', 'TXT')) 'NS', 'PTR', 'SPF', 'SRV', 'TXT'))


+ 1
- 0
octodns/provider/ovh.py View File

@ -34,6 +34,7 @@ class OvhProvider(BaseProvider):
""" """
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
ZONE_NOT_FOUND_MESSAGE = 'This service does not exist' ZONE_NOT_FOUND_MESSAGE = 'This service does not exist'
# This variable is also used in populate method to filter which OVH record # This variable is also used in populate method to filter which OVH record


+ 1
- 0
octodns/provider/powerdns.py View File

@ -14,6 +14,7 @@ from .base import BaseProvider
class PowerDnsBaseProvider(BaseProvider): class PowerDnsBaseProvider(BaseProvider):
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT')) 'PTR', 'SPF', 'SSHFP', 'SRV', 'TXT'))
TIMEOUT = 5 TIMEOUT = 5


+ 1
- 0
octodns/provider/rackspace.py View File

@ -38,6 +38,7 @@ def unescape_semicolon(s):
class RackspaceProvider(BaseProvider): class RackspaceProvider(BaseProvider):
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NS', 'PTR', 'SPF', SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CNAME', 'MX', 'NS', 'PTR', 'SPF',
'TXT')) 'TXT'))
TIMEOUT = 5 TIMEOUT = 5


+ 2
- 0
octodns/provider/route53.py View File

@ -232,6 +232,8 @@ class Route53Provider(BaseProvider):
In general the account used will need full permissions on Route53. In general the account used will need full permissions on Route53.
''' '''
SUPPORTS_GEO = True SUPPORTS_GEO = True
# TODO: dynamic
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', SUPPORTS = set(('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR',
'SPF', 'SRV', 'TXT')) 'SPF', 'SRV', 'TXT'))


+ 1
- 0
octodns/provider/yaml.py View File

@ -31,6 +31,7 @@ class YamlProvider(BaseProvider):
enforce_order: True enforce_order: True
''' '''
SUPPORTS_GEO = True SUPPORTS_GEO = True
SUPPORTS_DYNAMIC = True
SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS', SUPPORTS = set(('A', 'AAAA', 'ALIAS', 'CAA', 'CNAME', 'MX', 'NAPTR', 'NS',
'PTR', 'SSHFP', 'SPF', 'SRV', 'TXT')) 'PTR', 'SSHFP', 'SPF', 'SRV', 'TXT'))


octodns/record.py → octodns/record/__init__.py View File


+ 35
- 0
octodns/record/geo.py View File

@ -0,0 +1,35 @@
#
#
#
from .geo_data import geo_data
class GeoCodes(object):
__COUNTRIES = None
@classmethod
def validate(cls, code, prefix):
'''
Validates an octoDNS geo code making sure that it is a valid and
corresponding:
* continent
* continent & country
* continent, country, & province
'''
reasons = []
pieces = code.split('-')
n = len(pieces)
if n > 3:
reasons.append('{}invalid geo code "{}"'.format(prefix, code))
elif n > 0 and pieces[0] not in geo_data:
reasons.append('{}unknown continent code "{}"'
.format(prefix, code))
elif n > 1 and pieces[1] not in geo_data[pieces[0]]:
reasons.append('{}unknown country code "{}"'.format(prefix, code))
elif n > 2 and \
pieces[2] not in geo_data[pieces[0]][pieces[1]]['provinces']:
reasons.append('{}unknown province code "{}"'.format(prefix, code))
return reasons

+ 316
- 0
octodns/record/geo_data.py View File

@ -0,0 +1,316 @@
#
# -*- coding: utf-8 -*-
#
# This file is generated using
# ./script/generate-geo-data > octodns/record/geo_data.py
# do not modify it directly
#
geo_data = \
{'AF': {'AO': {'name': 'Angola'},
'BF': {'name': 'Burkina Faso'},
'BI': {'name': 'Burundi'},
'BJ': {'name': 'Benin'},
'BW': {'name': 'Botswana'},
'CD': {'name': 'Congo, The Democratic Republic of the'},
'CF': {'name': 'Central African Republic'},
'CG': {'name': 'Congo'},
'CI': {'name': "Côte d'Ivoire"},
'CM': {'name': 'Cameroon'},
'CV': {'name': 'Cabo Verde'},
'DJ': {'name': 'Djibouti'},
'DZ': {'name': 'Algeria'},
'EG': {'name': 'Egypt'},
'EH': {'name': 'Western Sahara'},
'ER': {'name': 'Eritrea'},
'ET': {'name': 'Ethiopia'},
'GA': {'name': 'Gabon'},
'GH': {'name': 'Ghana'},
'GM': {'name': 'Gambia'},
'GN': {'name': 'Guinea'},
'GQ': {'name': 'Equatorial Guinea'},
'GW': {'name': 'Guinea-Bissau'},
'KE': {'name': 'Kenya'},
'KM': {'name': 'Comoros'},
'LR': {'name': 'Liberia'},
'LS': {'name': 'Lesotho'},
'LY': {'name': 'Libya'},
'MA': {'name': 'Morocco'},
'MG': {'name': 'Madagascar'},
'ML': {'name': 'Mali'},
'MR': {'name': 'Mauritania'},
'MU': {'name': 'Mauritius'},
'MW': {'name': 'Malawi'},
'MZ': {'name': 'Mozambique'},
'NA': {'name': 'Namibia'},
'NE': {'name': 'Niger'},
'NG': {'name': 'Nigeria'},
'RE': {'name': 'Réunion'},
'RW': {'name': 'Rwanda'},
'SC': {'name': 'Seychelles'},
'SD': {'name': 'Sudan'},
'SH': {'name': 'Saint Helena, Ascension and Tristan da Cunha'},
'SL': {'name': 'Sierra Leone'},
'SN': {'name': 'Senegal'},
'SO': {'name': 'Somalia'},
'SS': {'name': 'South Sudan'},
'ST': {'name': 'Sao Tome and Principe'},
'SZ': {'name': 'Swaziland'},
'TD': {'name': 'Chad'},
'TG': {'name': 'Togo'},
'TN': {'name': 'Tunisia'},
'TZ': {'name': 'Tanzania, United Republic of'},
'UG': {'name': 'Uganda'},
'YT': {'name': 'Mayotte'},
'ZA': {'name': 'South Africa'},
'ZM': {'name': 'Zambia'},
'ZW': {'name': 'Zimbabwe'}},
'AN': {'AQ': {'name': 'Antarctica'},
'BV': {'name': 'Bouvet Island'},
'HM': {'name': 'Heard Island and McDonald Islands'},
'TF': {'name': 'French Southern Territories'}},
'AS': {'AE': {'name': 'United Arab Emirates'},
'AF': {'name': 'Afghanistan'},
'AM': {'name': 'Armenia'},
'AZ': {'name': 'Azerbaijan'},
'BD': {'name': 'Bangladesh'},
'BH': {'name': 'Bahrain'},
'BN': {'name': 'Brunei Darussalam'},
'BT': {'name': 'Bhutan'},
'CC': {'name': 'Cocos (Keeling) Islands'},
'CN': {'name': 'China'},
'CX': {'name': 'Christmas Island'},
'CY': {'name': 'Cyprus'},
'GE': {'name': 'Georgia'},
'HK': {'name': 'Hong Kong'},
'ID': {'name': 'Indonesia'},
'IL': {'name': 'Israel'},
'IN': {'name': 'India'},
'IO': {'name': 'British Indian Ocean Territory'},
'IQ': {'name': 'Iraq'},
'IR': {'name': 'Iran, Islamic Republic of'},
'JO': {'name': 'Jordan'},
'JP': {'name': 'Japan'},
'KG': {'name': 'Kyrgyzstan'},
'KH': {'name': 'Cambodia'},
'KP': {'name': "Korea, Democratic People's Republic of"},
'KR': {'name': 'Korea, Republic of'},
'KW': {'name': 'Kuwait'},
'KZ': {'name': 'Kazakhstan'},
'LA': {'name': "Lao People's Democratic Republic"},
'LB': {'name': 'Lebanon'},
'LK': {'name': 'Sri Lanka'},
'MM': {'name': 'Myanmar'},
'MN': {'name': 'Mongolia'},
'MO': {'name': 'Macao'},
'MV': {'name': 'Maldives'},
'MY': {'name': 'Malaysia'},
'NP': {'name': 'Nepal'},
'OM': {'name': 'Oman'},
'PH': {'name': 'Philippines'},
'PK': {'name': 'Pakistan'},
'PS': {'name': 'Palestine, State of'},
'QA': {'name': 'Qatar'},
'SA': {'name': 'Saudi Arabia'},
'SG': {'name': 'Singapore'},
'SY': {'name': 'Syrian Arab Republic'},
'TH': {'name': 'Thailand'},
'TJ': {'name': 'Tajikistan'},
'TM': {'name': 'Turkmenistan'},
'TR': {'name': 'Turkey'},
'TW': {'name': 'Taiwan, Province of China'},
'UZ': {'name': 'Uzbekistan'},
'VN': {'name': 'Viet Nam'},
'YE': {'name': 'Yemen'}},
'EU': {'AD': {'name': 'Andorra'},
'AL': {'name': 'Albania'},
'AT': {'name': 'Austria'},
'AX': {'name': 'Åland Islands'},
'BA': {'name': 'Bosnia and Herzegovina'},
'BE': {'name': 'Belgium'},
'BG': {'name': 'Bulgaria'},
'BY': {'name': 'Belarus'},
'CH': {'name': 'Switzerland'},
'CZ': {'name': 'Czechia'},
'DE': {'name': 'Germany'},
'DK': {'name': 'Denmark'},
'EE': {'name': 'Estonia'},
'ES': {'name': 'Spain'},
'FI': {'name': 'Finland'},
'FO': {'name': 'Faroe Islands'},
'FR': {'name': 'France'},
'GB': {'name': 'United Kingdom'},
'GG': {'name': 'Guernsey'},
'GI': {'name': 'Gibraltar'},
'GR': {'name': 'Greece'},
'HR': {'name': 'Croatia'},
'HU': {'name': 'Hungary'},
'IE': {'name': 'Ireland'},
'IM': {'name': 'Isle of Man'},
'IS': {'name': 'Iceland'},
'IT': {'name': 'Italy'},
'JE': {'name': 'Jersey'},
'LI': {'name': 'Liechtenstein'},
'LT': {'name': 'Lithuania'},
'LU': {'name': 'Luxembourg'},
'LV': {'name': 'Latvia'},
'MC': {'name': 'Monaco'},
'MD': {'name': 'Moldova, Republic of'},
'ME': {'name': 'Montenegro'},
'MK': {'name': 'Macedonia, Republic of'},
'MT': {'name': 'Malta'},
'NL': {'name': 'Netherlands'},
'NO': {'name': 'Norway'},
'PL': {'name': 'Poland'},
'PT': {'name': 'Portugal'},
'RO': {'name': 'Romania'},
'RS': {'name': 'Serbia'},
'RU': {'name': 'Russian Federation'},
'SE': {'name': 'Sweden'},
'SI': {'name': 'Slovenia'},
'SJ': {'name': 'Svalbard and Jan Mayen'},
'SK': {'name': 'Slovakia'},
'SM': {'name': 'San Marino'},
'UA': {'name': 'Ukraine'},
'VA': {'name': 'Holy See (Vatican City State)'}},
'ID': {'TL': {'name': 'Timor-Leste'}},
'NA': {'AG': {'name': 'Antigua and Barbuda'},
'AI': {'name': 'Anguilla'},
'AW': {'name': 'Aruba'},
'BB': {'name': 'Barbados'},
'BL': {'name': 'Saint Barthélemy'},
'BM': {'name': 'Bermuda'},
'BQ': {'name': 'Bonaire, Sint Eustatius and Saba'},
'BS': {'name': 'Bahamas'},
'BZ': {'name': 'Belize'},
'CA': {'name': 'Canada'},
'CR': {'name': 'Costa Rica'},
'CU': {'name': 'Cuba'},
'CW': {'name': 'Curaçao'},
'DM': {'name': 'Dominica'},
'DO': {'name': 'Dominican Republic'},
'GD': {'name': 'Grenada'},
'GL': {'name': 'Greenland'},
'GP': {'name': 'Guadeloupe'},
'GT': {'name': 'Guatemala'},
'HN': {'name': 'Honduras'},
'HT': {'name': 'Haiti'},
'JM': {'name': 'Jamaica'},
'KN': {'name': 'Saint Kitts and Nevis'},
'KY': {'name': 'Cayman Islands'},
'LC': {'name': 'Saint Lucia'},
'MF': {'name': 'Saint Martin (French part)'},
'MQ': {'name': 'Martinique'},
'MS': {'name': 'Montserrat'},
'MX': {'name': 'Mexico'},
'NI': {'name': 'Nicaragua'},
'PA': {'name': 'Panama'},
'PM': {'name': 'Saint Pierre and Miquelon'},
'PR': {'name': 'Puerto Rico'},
'SV': {'name': 'El Salvador'},
'SX': {'name': 'Sint Maarten (Dutch part)'},
'TC': {'name': 'Turks and Caicos Islands'},
'TT': {'name': 'Trinidad and Tobago'},
'US': {'name': 'United States',
'provinces': {'AK': {'name': 'Alaska'},
'AL': {'name': 'Alabama'},
'AR': {'name': 'Arkansas'},
'AS': {'name': 'American Samoa'},
'AZ': {'name': 'Arizona'},
'CA': {'name': 'California'},
'CO': {'name': 'Colorado'},
'CT': {'name': 'Connecticut'},
'DC': {'name': 'District of Columbia'},
'DE': {'name': 'Delaware'},
'FL': {'name': 'Florida'},
'GA': {'name': 'Georgia'},
'GU': {'name': 'Guam'},
'HI': {'name': 'Hawaii'},
'IA': {'name': 'Iowa'},
'ID': {'name': 'Idaho'},
'IL': {'name': 'Illinois'},
'IN': {'name': 'Indiana'},
'KS': {'name': 'Kansas'},
'KY': {'name': 'Kentucky'},
'LA': {'name': 'Louisiana'},
'MA': {'name': 'Massachusetts'},
'MD': {'name': 'Maryland'},
'ME': {'name': 'Maine'},
'MI': {'name': 'Michigan'},
'MN': {'name': 'Minnesota'},
'MO': {'name': 'Missouri'},
'MP': {'name': 'Northern Mariana Islands'},
'MS': {'name': 'Mississippi'},
'MT': {'name': 'Montana'},
'NC': {'name': 'North Carolina'},
'ND': {'name': 'North Dakota'},
'NE': {'name': 'Nebraska'},
'NH': {'name': 'New Hampshire'},
'NJ': {'name': 'New Jersey'},
'NM': {'name': 'New Mexico'},
'NV': {'name': 'Nevada'},
'NY': {'name': 'New York'},
'OH': {'name': 'Ohio'},
'OK': {'name': 'Oklahoma'},
'OR': {'name': 'Oregon'},
'PA': {'name': 'Pennsylvania'},
'PR': {'name': 'Puerto Rico'},
'RI': {'name': 'Rhode Island'},
'SC': {'name': 'South Carolina'},
'SD': {'name': 'South Dakota'},
'TN': {'name': 'Tennessee'},
'TX': {'name': 'Texas'},
'UM': {'name': 'United States Minor Outlying '
'Islands'},
'UT': {'name': 'Utah'},
'VA': {'name': 'Virginia'},
'VI': {'name': 'Virgin Islands'},
'VT': {'name': 'Vermont'},
'WA': {'name': 'Washington'},
'WI': {'name': 'Wisconsin'},
'WV': {'name': 'West Virginia'},
'WY': {'name': 'Wyoming'}}},
'VC': {'name': 'Saint Vincent and the Grenadines'},
'VG': {'name': 'Virgin Islands, British'},
'VI': {'name': 'Virgin Islands, U.S.'}},
'OC': {'AS': {'name': 'American Samoa'},
'AU': {'name': 'Australia'},
'CK': {'name': 'Cook Islands'},
'FJ': {'name': 'Fiji'},
'FM': {'name': 'Micronesia, Federated States of'},
'GU': {'name': 'Guam'},
'KI': {'name': 'Kiribati'},
'MH': {'name': 'Marshall Islands'},
'MP': {'name': 'Northern Mariana Islands'},
'NC': {'name': 'New Caledonia'},
'NF': {'name': 'Norfolk Island'},
'NR': {'name': 'Nauru'},
'NU': {'name': 'Niue'},
'NZ': {'name': 'New Zealand'},
'PF': {'name': 'French Polynesia'},
'PG': {'name': 'Papua New Guinea'},
'PN': {'name': 'Pitcairn'},
'PW': {'name': 'Palau'},
'SB': {'name': 'Solomon Islands'},
'TK': {'name': 'Tokelau'},
'TO': {'name': 'Tonga'},
'TV': {'name': 'Tuvalu'},
'UM': {'name': 'United States Minor Outlying Islands'},
'VU': {'name': 'Vanuatu'},
'WF': {'name': 'Wallis and Futuna'},
'WS': {'name': 'Samoa'}},
'SA': {'AR': {'name': 'Argentina'},
'BO': {'name': 'Bolivia, Plurinational State of'},
'BR': {'name': 'Brazil'},
'CL': {'name': 'Chile'},
'CO': {'name': 'Colombia'},
'EC': {'name': 'Ecuador'},
'FK': {'name': 'Falkland Islands (Malvinas)'},
'GF': {'name': 'French Guiana'},
'GS': {'name': 'South Georgia and the South Sandwich Islands'},
'GY': {'name': 'Guyana'},
'PE': {'name': 'Peru'},
'PY': {'name': 'Paraguay'},
'SR': {'name': 'Suriname'},
'UY': {'name': 'Uruguay'},
'VE': {'name': 'Venezuela, Bolivarian Republic of'}}}

+ 1
- 0
octodns/source/axfr.py View File

@ -24,6 +24,7 @@ from .base import BaseSource
class AxfrBaseSource(BaseSource): class AxfrBaseSource(BaseSource):
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SPF', SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR', 'SPF',
'SRV', 'TXT')) 'SRV', 'TXT'))


+ 3
- 0
octodns/source/base.py View File

@ -16,6 +16,9 @@ class BaseSource(object):
if not hasattr(self, 'SUPPORTS_GEO'): if not hasattr(self, 'SUPPORTS_GEO'):
raise NotImplementedError('Abstract base class, SUPPORTS_GEO ' raise NotImplementedError('Abstract base class, SUPPORTS_GEO '
'property missing') 'property missing')
if not hasattr(self, 'SUPPORTS_DYNAMIC'):
raise NotImplementedError('Abstract base class, SUPPORTS_DYNAMIC '
'property missing')
if not hasattr(self, 'SUPPORTS'): if not hasattr(self, 'SUPPORTS'):
raise NotImplementedError('Abstract base class, SUPPORTS ' raise NotImplementedError('Abstract base class, SUPPORTS '
'property missing') 'property missing')


+ 1
- 0
octodns/source/tinydns.py View File

@ -19,6 +19,7 @@ from .base import BaseSource
class TinyDnsBaseSource(BaseSource): class TinyDnsBaseSource(BaseSource):
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A', 'CNAME', 'MX', 'NS')) SUPPORTS = set(('A', 'CNAME', 'MX', 'NS'))
split_re = re.compile(r':+') split_re = re.compile(r':+')


+ 2
- 0
requirements-dev.txt View File

@ -2,6 +2,8 @@ coverage
mock mock
nose nose
pycodestyle==2.4.0 pycodestyle==2.4.0
pycountry>=18.12.8
pycountry_convert>=0.7.2
pyflakes==1.6.0 pyflakes==1.6.0
requests_mock requests_mock
twine==1.11.0 twine==1.11.0

+ 64
- 0
script/generate-geo-data View File

@ -0,0 +1,64 @@
#!/usr/bin/env python
from collections import defaultdict
from pprint import pformat
from pycountry import countries, subdivisions
from pycountry_convert import country_alpha2_to_continent_code
subs = defaultdict(dict)
for subdivision in subdivisions:
# Route53 only supports US states, Dyn supports US states and CA provinces, but for now we'll just do US
if subdivision.country_code not in ('US'):
continue
subs[subdivision.country_code][subdivision.code[3:]] = {
'name': subdivision.name
}
subs = dict(subs)
#pprint(subs)
# These are best guesses at things pycountry_convert doesn't map
continent_backups = {
'AQ': 'AN',
'EH': 'AF',
'PN': 'OC',
'SX': 'NA',
'TF': 'AN',
'TL': 'ID',
'UM': 'OC',
'VA': 'EU',
}
geos = defaultdict(dict)
for country in countries:
try:
continent_code = country_alpha2_to_continent_code(country.alpha_2)
except KeyError:
try:
continent_code = continent_backups[country.alpha_2]
except KeyError:
raise
print('{} {} {}'.format(country.alpha_2, country.name, getattr(country, 'official_name', '')))
geos[continent_code][country.alpha_2] = {
'name': country.name
}
try:
geos[continent_code][country.alpha_2]['provinces'] = subs[country.alpha_2]
except KeyError:
pass
geos = dict(geos)
data = pformat(geos).replace('\n', '\n ')
print('''#
# -*- coding: utf-8 -*-
#
# This file is generated using
# ./script/generate-geo-data > octodns/record/geo_data.py
# do not modify it directly
#
geo_data = \\
{}'''.format(data))

+ 197
- 0
tests/config/dynamic.tests.yaml View File

@ -0,0 +1,197 @@
---
a:
dynamic:
pools:
ams:
# TODO: make value possible
values:
- value: 1.1.1.1
iad:
values:
# TODO: make value optional
- value: 2.2.2.2
- value: 3.3.3.3
lax:
values:
- value: 4.4.4.4
sea:
values:
- value: 6.6.6.6
weight: 10
- value: 5.5.5.5
weight: 25
rules:
- geos:
- EU-GB
pool: iad
- geos:
- EU
pool: ams
- geos:
- NA-US-CA
- NA-US-OR
- NA-US-WA
pool: sea
- pool: iad
type: A
values:
- 2.2.2.2
- 3.3.3.3
aaaa:
dynamic:
pools:
ams:
values:
- value: 2601:642:500:e210:62f8:1dff:feb8:9471
iad:
values:
- value: 2601:642:500:e210:62f8:1dff:feb8:9472
- value: 2601:642:500:e210:62f8:1dff:feb8:9473
lax:
values:
- value: 2601:642:500:e210:62f8:1dff:feb8:9474
sea:
values:
- value: 2601:642:500:e210:62f8:1dff:feb8:9475
weight: 1
- value: 2601:642:500:e210:62f8:1dff:feb8:9476
weight: 2
rules:
- geos:
- EU-GB
pool: iad
- geos:
- EU
pool: ams
- geos:
- NA-US-CA
- NA-US-OR
- NA-US-WA
pool: sea
- pool: iad
type: AAAA
values:
- 2601:642:500:e210:62f8:1dff:feb8:947a
- 2601:644:500:e210:62f8:1dff:feb8:947a
cname:
dynamic:
pools:
ams:
values:
- value: target-ams.unit.tests.
iad:
values:
- value: target-iad.unit.tests.
lax:
values:
- value: target-lax.unit.tests.
sea:
values:
- value: target-sea-1.unit.tests.
weight: 100
- value: target-sea-2.unit.tests.
weight: 175
rules:
- geos:
- EU-GB
pool: iad
- geos:
- EU
pool: ams
- geos:
- NA-US-CA
- NA-US-OR
- NA-US-WA
pool: sea
- pool: iad
type: CNAME
value: target.unit.tests.
real-ish-a:
dynamic:
pools:
ap-southeast-1:
values:
# ap-southeast-1a
- value: 1.4.1.1
weight: 2
- value: 1.4.1.2
weight: 2
# ap-southeast-1b
- value: 1.4.2.1
- value: 1.4.2.2
# ap-southeast-1c
- value: 1.4.3.1
- value: 1.4.3.2
eu-central-1:
values:
# eu-central-1a
- value: 1.3.1.1
- value: 1.3.1.2
# eu-central-1b
- value: 1.3.2.1
- value: 1.3.2.2
# eu-central-1c
- value: 1.3.3.1
- value: 1.3.3.2
us-east-1:
values:
# us-east-1a
- value: 1.1.1.1
- value: 1.1.1.2
# us-east-1b
- value: 1.1.2.1
- value: 1.1.2.2
# us-east-1c
- value: 1.1.3.1
- value: 1.1.3.2
us-west-2:
values:
# us-west-2a
- value: 1.2.1.1
- value: 1.2.1.2
# us-west-2b
- value: 1.2.2.1
- value: 1.2.2.2
# us-west-2c
- value: 1.2.3.1
- value: 1.2.3.2
rules:
- geos:
# TODO: require sorted
- NA-US-CA
- NA-US-OR
- NA-US-WA
pool: us-west-2
- geos:
- AS-CN
pool: ap-southeast-1
- geos:
- AF
- EU
pool: eu-central-1
- pool: us-east-1
type: A
values:
# Generally these should match the values of your "default" rule's pools as
# if everything fails healthchecks they'll fallback to this
- 1.1.1.1
- 1.1.1.2
- 1.1.2.1
- 1.1.2.2
- 1.1.3.1
- 1.1.3.2
simple-weighted:
dynamic:
pools:
default:
values:
- value: one.unit.tests.
weight: 3
- value: two.unit.tests.
weight: 2
rules:
- pool: default
type: CNAME
# CNAMEs don't support health checks (currently) so these will never be used
# on providers with dynamic support
value: default.unit.tests.

+ 20
- 0
tests/helpers.py View File

@ -17,6 +17,7 @@ class SimpleSource(object):
class SimpleProvider(object): class SimpleProvider(object):
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A',)) SUPPORTS = set(('A',))
id = 'test' id = 'test'
@ -35,6 +36,25 @@ class SimpleProvider(object):
class GeoProvider(object): class GeoProvider(object):
SUPPORTS_GEO = True SUPPORTS_GEO = True
SUPPORTS_DYNAMIC = False
id = 'test'
def __init__(self, id='test'):
pass
def populate(self, zone, source=False, lenient=False):
pass
def supports(self, record):
return True
def __repr__(self):
return self.__class__.__name__
class DynamicProvider(object):
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = True
id = 'test' id = 'test'
def __init__(self, id='test'): def __init__(self, id='test'):


+ 8
- 2
tests/test_octodns_manager.py View File

@ -14,8 +14,8 @@ from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager
from octodns.yaml import safe_load from octodns.yaml import safe_load
from octodns.zone import Zone from octodns.zone import Zone
from helpers import GeoProvider, NoSshFpProvider, SimpleProvider, \
TemporaryDirectory
from helpers import DynamicProvider, GeoProvider, NoSshFpProvider, \
SimpleProvider, TemporaryDirectory
config_dir = join(dirname(__file__), 'config') config_dir = join(dirname(__file__), 'config')
@ -187,6 +187,7 @@ class TestManager(TestCase):
def test_aggregate_target(self): def test_aggregate_target(self):
simple = SimpleProvider() simple = SimpleProvider()
geo = GeoProvider() geo = GeoProvider()
dynamic = DynamicProvider()
nosshfp = NoSshFpProvider() nosshfp = NoSshFpProvider()
self.assertFalse(_AggregateTarget([simple, simple]).SUPPORTS_GEO) self.assertFalse(_AggregateTarget([simple, simple]).SUPPORTS_GEO)
@ -194,6 +195,11 @@ class TestManager(TestCase):
self.assertFalse(_AggregateTarget([geo, simple]).SUPPORTS_GEO) self.assertFalse(_AggregateTarget([geo, simple]).SUPPORTS_GEO)
self.assertTrue(_AggregateTarget([geo, geo]).SUPPORTS_GEO) self.assertTrue(_AggregateTarget([geo, geo]).SUPPORTS_GEO)
self.assertFalse(_AggregateTarget([simple, simple]).SUPPORTS_DYNAMIC)
self.assertFalse(_AggregateTarget([simple, dynamic]).SUPPORTS_DYNAMIC)
self.assertFalse(_AggregateTarget([dynamic, simple]).SUPPORTS_DYNAMIC)
self.assertTrue(_AggregateTarget([dynamic, dynamic]).SUPPORTS_DYNAMIC)
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
record = Record.new(zone, 'sshfp', { record = Record.new(zone, 'sshfp', {
'ttl': 60, 'ttl': 60,


+ 10
- 2
tests/test_octodns_provider_base.py View File

@ -61,13 +61,21 @@ class TestBaseProvider(TestCase):
class HasSupportsGeo(HasLog): class HasSupportsGeo(HasLog):
SUPPORTS_GEO = False SUPPORTS_GEO = False
with self.assertRaises(NotImplementedError) as ctx:
HasSupportsGeo('hassupportsgeo')
self.assertEquals('Abstract base class, SUPPORTS_DYNAMIC '
'property missing', ctx.exception.message)
class HasSupportsDyanmic(HasSupportsGeo):
SUPPORTS_DYNAMIC = False
zone = Zone('unit.tests.', ['sub']) zone = Zone('unit.tests.', ['sub'])
with self.assertRaises(NotImplementedError) as ctx: with self.assertRaises(NotImplementedError) as ctx:
HasSupportsGeo('hassupportsgeo').populate(zone)
HasSupportsDyanmic('hassupportsdynamic').populate(zone)
self.assertEquals('Abstract base class, SUPPORTS property missing', self.assertEquals('Abstract base class, SUPPORTS property missing',
ctx.exception.message) ctx.exception.message)
class HasSupports(HasSupportsGeo):
class HasSupports(HasSupportsDyanmic):
SUPPORTS = set(('A',)) SUPPORTS = set(('A',))
with self.assertRaises(NotImplementedError) as ctx: with self.assertRaises(NotImplementedError) as ctx:
HasSupports('hassupports').populate(zone) HasSupports('hassupports').populate(zone)


+ 60
- 9
tests/test_octodns_provider_yaml.py View File

@ -23,6 +23,7 @@ class TestYamlProvider(TestCase):
source = YamlProvider('test', join(dirname(__file__), 'config')) source = YamlProvider('test', join(dirname(__file__), 'config'))
zone = Zone('unit.tests.', []) zone = Zone('unit.tests.', [])
dynamic_zone = Zone('dynamic.tests.', [])
# With target we don't add anything # With target we don't add anything
source.populate(zone, target=source) source.populate(zone, target=source)
@ -32,6 +33,9 @@ class TestYamlProvider(TestCase):
source.populate(zone) source.populate(zone)
self.assertEquals(18, len(zone.records)) self.assertEquals(18, len(zone.records))
source.populate(dynamic_zone)
self.assertEquals(5, len(dynamic_zone.records))
# Assumption here is that a clean round-trip means that everything # Assumption here is that a clean round-trip means that everything
# worked as expected, data that went in came back out and could be # worked as expected, data that went in came back out and could be
# pulled in yet again and still match up. That assumes that the input # pulled in yet again and still match up. That assumes that the input
@ -45,6 +49,7 @@ class TestYamlProvider(TestCase):
# Add some subdirs to make sure that it can create them # Add some subdirs to make sure that it can create them
directory = join(td.dirname, 'sub', 'dir') directory = join(td.dirname, 'sub', 'dir')
yaml_file = join(directory, 'unit.tests.yaml') yaml_file = join(directory, 'unit.tests.yaml')
dynamic_yaml_file = join(directory, 'dynamic.tests.yaml')
target = YamlProvider('test', directory) target = YamlProvider('test', directory)
# We add everything # We add everything
@ -57,6 +62,15 @@ class TestYamlProvider(TestCase):
self.assertEquals(15, target.apply(plan)) self.assertEquals(15, target.apply(plan))
self.assertTrue(isfile(yaml_file)) self.assertTrue(isfile(yaml_file))
# Dynamic plan
plan = target.plan(dynamic_zone)
self.assertEquals(5, len(filter(lambda c: isinstance(c, Create),
plan.changes)))
self.assertFalse(isfile(dynamic_yaml_file))
# Apply it
self.assertEquals(5, target.apply(plan))
self.assertTrue(isfile(dynamic_yaml_file))
# There should be no changes after the round trip # There should be no changes after the round trip
reloaded = Zone('unit.tests.', []) reloaded = Zone('unit.tests.', [])
target.populate(reloaded) target.populate(reloaded)
@ -77,21 +91,58 @@ class TestYamlProvider(TestCase):
data = safe_load(fh.read()) data = safe_load(fh.read())
# '' has some of both # '' has some of both
roots = sorted(data[''], key=lambda r: r['type'])
roots = sorted(data.pop(''), key=lambda r: r['type'])
self.assertTrue('values' in roots[0]) # A self.assertTrue('values' in roots[0]) # A
self.assertTrue('geo' in roots[0]) # geo made the trip
self.assertTrue('value' in roots[1]) # CAA self.assertTrue('value' in roots[1]) # CAA
self.assertTrue('values' in roots[2]) # SSHFP self.assertTrue('values' in roots[2]) # SSHFP
# these are stored as plural 'values' # these are stored as plural 'values'
self.assertTrue('values' in data['mx'])
self.assertTrue('values' in data['naptr'])
self.assertTrue('values' in data['_srv._tcp'])
self.assertTrue('values' in data['txt'])
self.assertTrue('values' in data.pop('_srv._tcp'))
self.assertTrue('values' in data.pop('mx'))
self.assertTrue('values' in data.pop('naptr'))
self.assertTrue('values' in data.pop('sub'))
self.assertTrue('values' in data.pop('txt'))
# these are stored as singular 'value' # these are stored as singular 'value'
self.assertTrue('value' in data['aaaa'])
self.assertTrue('value' in data['ptr'])
self.assertTrue('value' in data['spf'])
self.assertTrue('value' in data['www'])
self.assertTrue('value' in data.pop('aaaa'))
self.assertTrue('value' in data.pop('cname'))
self.assertTrue('value' in data.pop('included'))
self.assertTrue('value' in data.pop('ptr'))
self.assertTrue('value' in data.pop('spf'))
self.assertTrue('value' in data.pop('www'))
self.assertTrue('value' in data.pop('www.sub'))
# make sure nothing is left
self.assertEquals([], data.keys())
with open(dynamic_yaml_file) as fh:
data = safe_load(fh.read())
# make sure new dynamic records made the trip
dyna = data.pop('a')
self.assertTrue('values' in dyna)
# self.assertTrue('dynamic' in dyna)
# TODO:
# make sure new dynamic records made the trip
dyna = data.pop('aaaa')
self.assertTrue('values' in dyna)
# self.assertTrue('dynamic' in dyna)
dyna = data.pop('cname')
self.assertTrue('value' in dyna)
# self.assertTrue('dynamic' in dyna)
dyna = data.pop('real-ish-a')
self.assertTrue('values' in dyna)
# self.assertTrue('dynamic' in dyna)
dyna = data.pop('simple-weighted')
self.assertTrue('value' in dyna)
# self.assertTrue('dynamic' in dyna)
# make sure nothing is left
self.assertEquals([], data.keys())
def test_empty(self): def test_empty(self):
source = YamlProvider('test', join(dirname(__file__), 'config')) source = YamlProvider('test', join(dirname(__file__), 'config'))


+ 1320
- 25
tests/test_octodns_record.py
File diff suppressed because it is too large
View File


+ 53
- 0
tests/test_octodns_record_geo.py View File

@ -0,0 +1,53 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from unittest import TestCase
from octodns.record.geo import GeoCodes
class TestRecordGeoCodes(TestCase):
def test_validate(self):
prefix = 'xyz '
# All valid
self.assertEquals([], GeoCodes.validate('NA', prefix))
self.assertEquals([], GeoCodes.validate('NA-US', prefix))
self.assertEquals([], GeoCodes.validate('NA-US-OR', prefix))
# Just plain bad
self.assertEquals(['xyz invalid geo code "XX-YY-ZZ-AA"'],
GeoCodes.validate('XX-YY-ZZ-AA', prefix))
self.assertEquals(['xyz unknown continent code "X-Y-Z"'],
GeoCodes.validate('X-Y-Z', prefix))
self.assertEquals(['xyz unknown continent code "XXX-Y-Z"'],
GeoCodes.validate('XXX-Y-Z', prefix))
# Bad continent
self.assertEquals(['xyz unknown continent code "XX"'],
GeoCodes.validate('XX', prefix))
# Bad continent good country
self.assertEquals(['xyz unknown continent code "XX-US"'],
GeoCodes.validate('XX-US', prefix))
# Bad continent good country and province
self.assertEquals(['xyz unknown continent code "XX-US-OR"'],
GeoCodes.validate('XX-US-OR', prefix))
# Bad country, good continent
self.assertEquals(['xyz unknown country code "NA-XX"'],
GeoCodes.validate('NA-XX', prefix))
# Bad country, good continent and state
self.assertEquals(['xyz unknown country code "NA-XX-OR"'],
GeoCodes.validate('NA-XX-OR', prefix))
# Good country, good continent, but bad match
self.assertEquals(['xyz unknown country code "NA-GB"'],
GeoCodes.validate('NA-GB', prefix))
# Bad province code, good continent and country
self.assertEquals(['xyz unknown province code "NA-US-XX"'],
GeoCodes.validate('NA-US-XX', prefix))

+ 1
- 0
tests/test_octodns_zone.py View File

@ -111,6 +111,7 @@ class TestZone(TestCase):
class NoAaaaProvider(object): class NoAaaaProvider(object):
id = 'no-aaaa' id = 'no-aaaa'
SUPPORTS_GEO = False SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
def supports(self, record): def supports(self, record):
return record._type != 'AAAA' return record._type != 'AAAA'


Loading…
Cancel
Save