mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge branch 'master' into patch-1
This commit is contained in:
@@ -60,7 +60,7 @@ better in the future :fingers_crossed:
|
||||
|
||||
#### Miscellaneous
|
||||
|
||||
* Use a 3rd party lib for nautrual sorting of keys, rather than my old
|
||||
* Use a 3rd party lib for natural sorting of keys, rather than my old
|
||||
implementation. Sorting can be disabled in the YamlProvider with
|
||||
`enforce_order: False`.
|
||||
* Semi-colon/escaping fixes and improvements.
|
||||
|
||||
@@ -53,11 +53,11 @@ The geo labels breakdown based on:
|
||||
|
||||
1.
|
||||
- 'AF': 14, # Continental Africa
|
||||
- 'AN': 17, # Continental Antartica
|
||||
- 'AS': 15, # Contentinal Asia
|
||||
- 'EU': 13, # Contentinal Europe
|
||||
- 'AN': 17, # Continental Antarctica
|
||||
- 'AS': 15, # Continental Asia
|
||||
- 'EU': 13, # Continental Europe
|
||||
- 'NA': 11, # Continental North America
|
||||
- 'OC': 16, # Contentinal Austrailia/Oceania
|
||||
- 'OC': 16, # Continental Australia/Oceania
|
||||
- 'SA': 12, # Continental South America
|
||||
|
||||
2. ISO Country Code https://en.wikipedia.org/wiki/ISO_3166-2
|
||||
|
||||
@@ -26,7 +26,7 @@ def main():
|
||||
help='Limit sync to the specified zone(s)')
|
||||
|
||||
# --sources isn't an option here b/c filtering sources out would be super
|
||||
# dangerous since you could eaily end up with an empty zone and delete
|
||||
# dangerous since you could easily end up with an empty zone and delete
|
||||
# everything, or even just part of things when there are multiple sources
|
||||
|
||||
parser.add_argument('--target', default=[], action='append',
|
||||
|
||||
@@ -51,7 +51,7 @@ class MakeThreadFuture(object):
|
||||
|
||||
class MainThreadExecutor(object):
|
||||
'''
|
||||
Dummy executor that runs things on the main thread during the involcation
|
||||
Dummy executor that runs things on the main thread during the invocation
|
||||
of submit, but still returns a future object with the result. This allows
|
||||
code to be written to handle async, even in the case where we don't want to
|
||||
use multiple threads/workers and would prefer that things flow as if
|
||||
@@ -361,7 +361,7 @@ class Manager(object):
|
||||
|
||||
plan = target.plan(zone)
|
||||
if plan is None:
|
||||
plan = Plan(zone, zone, [])
|
||||
plan = Plan(zone, zone, [], False)
|
||||
target.apply(plan)
|
||||
|
||||
def validate_configs(self):
|
||||
|
||||
@@ -39,7 +39,7 @@ class _AzureRecord(object):
|
||||
}
|
||||
|
||||
def __init__(self, resource_group, record, delete=False):
|
||||
'''Contructor for _AzureRecord.
|
||||
'''Constructor for _AzureRecord.
|
||||
|
||||
Notes on Azure records: An Azure record set has the form
|
||||
RecordSet(name=<...>, type=<...>, arecords=[...], aaaa_records, ..)
|
||||
@@ -222,7 +222,7 @@ class AzureProvider(BaseProvider):
|
||||
azuredns:
|
||||
class: octodns.provider.azuredns.AzureProvider
|
||||
client_id: env/AZURE_APPLICATION_ID
|
||||
key: env/AZURE_AUTHENICATION_KEY
|
||||
key: env/AZURE_AUTHENTICATION_KEY
|
||||
directory_id: env/AZURE_DIRECTORY_ID
|
||||
sub_id: env/AZURE_SUBSCRIPTION_ID
|
||||
resource_group: 'TestResource1'
|
||||
@@ -322,6 +322,8 @@ class AzureProvider(BaseProvider):
|
||||
:type return: void
|
||||
'''
|
||||
self.log.debug('populate: name=%s', zone.name)
|
||||
|
||||
exists = False
|
||||
before = len(zone.records)
|
||||
|
||||
zone_name = zone.name[:len(zone.name) - 1]
|
||||
@@ -331,6 +333,7 @@ class AzureProvider(BaseProvider):
|
||||
_records = set()
|
||||
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)
|
||||
@@ -344,7 +347,9 @@ class AzureProvider(BaseProvider):
|
||||
record = Record.new(zone, record_name, data, source=self)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records', len(zone.records) - before)
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _data_for_A(self, azrecord):
|
||||
return {'values': [ar.ipv4_address for ar in azrecord.arecords]}
|
||||
|
||||
@@ -30,14 +30,14 @@ class BaseProvider(BaseSource):
|
||||
def _include_change(self, change):
|
||||
'''
|
||||
An opportunity for providers to filter out false positives due to
|
||||
pecularities in their implementation. E.g. minimum TTLs.
|
||||
peculiarities in their implementation. E.g. minimum TTLs.
|
||||
'''
|
||||
return True
|
||||
|
||||
def _extra_changes(self, existing, changes):
|
||||
'''
|
||||
An opportunity for providers to add extra changes to the plan that are
|
||||
necessary to update ancilary record data or configure the zone. E.g.
|
||||
necessary to update ancillary record data or configure the zone. E.g.
|
||||
base NS records.
|
||||
'''
|
||||
return []
|
||||
@@ -46,7 +46,12 @@ class BaseProvider(BaseSource):
|
||||
self.log.info('plan: desired=%s', desired.name)
|
||||
|
||||
existing = Zone(desired.name, desired.sub_zones)
|
||||
self.populate(existing, target=True, lenient=True)
|
||||
exists = self.populate(existing, target=True, lenient=True)
|
||||
if exists is None:
|
||||
# If your code gets this warning see Source.populate for more
|
||||
# information
|
||||
self.log.warn('Provider %s used in target mode did not return '
|
||||
'exists', self.id)
|
||||
|
||||
# compute the changes at the zone/record level
|
||||
changes = existing.changes(desired, self)
|
||||
@@ -66,7 +71,7 @@ class BaseProvider(BaseSource):
|
||||
changes += extra
|
||||
|
||||
if changes:
|
||||
plan = Plan(existing, desired, changes,
|
||||
plan = Plan(existing, desired, changes, exists,
|
||||
self.update_pcent_threshold,
|
||||
self.delete_pcent_threshold)
|
||||
self.log.info('plan: %s', plan)
|
||||
|
||||
@@ -228,9 +228,11 @@ class CloudflareProvider(BaseProvider):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
|
||||
exists = False
|
||||
before = len(zone.records)
|
||||
records = self.zone_records(zone)
|
||||
if records:
|
||||
exists = True
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in records:
|
||||
name = zone.hostname_from_fqdn(record['name'])
|
||||
@@ -253,8 +255,9 @@ class CloudflareProvider(BaseProvider):
|
||||
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _include_change(self, change):
|
||||
if isinstance(change, Update):
|
||||
@@ -394,7 +397,7 @@ class CloudflareProvider(BaseProvider):
|
||||
for record in self.zone_records(zone):
|
||||
name = zone.hostname_from_fqdn(record['name'])
|
||||
# Use the _record_for so that we include all of standard
|
||||
# converstion logic
|
||||
# conversion logic
|
||||
r = self._record_for(zone, name, record['type'], [record], True)
|
||||
if hostname == r.name and _type == r._type:
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ class DigitalOceanClient(object):
|
||||
self._request('POST', '/domains', data={'name': name,
|
||||
'ip_address': '192.0.2.1'})
|
||||
|
||||
# After the zone is created, immeadiately delete the record
|
||||
# After the zone is created, immediately delete the record
|
||||
records = self.records(name)
|
||||
for record in records:
|
||||
if record['name'] == '' and record['type'] == 'A':
|
||||
@@ -232,8 +232,10 @@ class DigitalOceanProvider(BaseProvider):
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
exists = zone.name in self._zone_records
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _params_for_multiple(self, record):
|
||||
for value in record.values:
|
||||
|
||||
@@ -160,7 +160,7 @@ class DnsimpleProvider(BaseProvider):
|
||||
record['content'].split(' ', 5)
|
||||
except ValueError:
|
||||
# their api will let you create invalid records, this
|
||||
# essnetially handles that by ignoring them for values
|
||||
# essentially handles that by ignoring them for values
|
||||
# purposes. That will cause updates to happen to delete them if
|
||||
# they shouldn't exist or update them if they're wrong
|
||||
continue
|
||||
@@ -272,8 +272,10 @@ class DnsimpleProvider(BaseProvider):
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
exists = zone.name in self._zone_records
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _params_for_multiple(self, record):
|
||||
for value in record.values:
|
||||
|
||||
@@ -40,7 +40,7 @@ class _CachingDynZone(DynZone):
|
||||
cls.log.debug('get: fetched')
|
||||
except DynectGetError:
|
||||
if not create:
|
||||
cls.log.debug("get: does't exist")
|
||||
cls.log.debug("get: doesn't exist")
|
||||
return None
|
||||
# this value shouldn't really matter, it's not tied to
|
||||
# whois or anything
|
||||
@@ -129,11 +129,11 @@ class DynProvider(BaseProvider):
|
||||
REGION_CODES = {
|
||||
'NA': 11, # Continental North America
|
||||
'SA': 12, # Continental South America
|
||||
'EU': 13, # Contentinal Europe
|
||||
'EU': 13, # Continental Europe
|
||||
'AF': 14, # Continental Africa
|
||||
'AS': 15, # Contentinal Asia
|
||||
'OC': 16, # Contentinal Austrailia/Oceania
|
||||
'AN': 17, # Continental Antartica
|
||||
'AS': 15, # Continental Asia
|
||||
'OC': 16, # Continental Australia/Oceania
|
||||
'AN': 17, # Continental Antarctica
|
||||
}
|
||||
|
||||
_sess_create_lock = Lock()
|
||||
@@ -166,7 +166,7 @@ class DynProvider(BaseProvider):
|
||||
if DynectSession.get_session() is None:
|
||||
# We need to create a new session for this thread and DynectSession
|
||||
# creation is not thread-safe so we have to do the locking. If we
|
||||
# don't and multiple sessions start creattion before the the first
|
||||
# don't and multiple sessions start creation before the the first
|
||||
# has finished (long time b/c it makes http calls) the subsequent
|
||||
# creates will blow away DynectSession._instances, potentially
|
||||
# multiple times if there are multiple creates in flight. Only the
|
||||
@@ -291,7 +291,7 @@ class DynProvider(BaseProvider):
|
||||
try:
|
||||
fqdn, _type = td.label.split(':', 1)
|
||||
except ValueError as e:
|
||||
self.log.warn("Failed to load TraficDirector '%s': %s",
|
||||
self.log.warn("Failed to load TrafficDirector '%s': %s",
|
||||
td.label, e.message)
|
||||
continue
|
||||
tds[fqdn][_type] = td
|
||||
@@ -353,6 +353,7 @@ class DynProvider(BaseProvider):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
|
||||
exists = False
|
||||
before = len(zone.records)
|
||||
|
||||
self._check_dyn_sess()
|
||||
@@ -360,10 +361,12 @@ class DynProvider(BaseProvider):
|
||||
td_records = set()
|
||||
if self.traffic_directors_enabled:
|
||||
td_records = self._populate_traffic_directors(zone)
|
||||
exists = True
|
||||
|
||||
dyn_zone = _CachingDynZone.get(zone.name[:-1])
|
||||
|
||||
if dyn_zone:
|
||||
exists = True
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for _type, records in dyn_zone.get_all_records().items():
|
||||
if _type == 'soa_records':
|
||||
@@ -382,8 +385,9 @@ class DynProvider(BaseProvider):
|
||||
if record not in td_records:
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _kwargs_for_A(self, record):
|
||||
return [{
|
||||
|
||||
@@ -9,6 +9,7 @@ import shlex
|
||||
import time
|
||||
from logging import getLogger
|
||||
from uuid import uuid4
|
||||
import re
|
||||
|
||||
from google.cloud import dns
|
||||
|
||||
@@ -203,11 +204,14 @@ class GoogleCloudProvider(BaseProvider):
|
||||
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
|
||||
exists = False
|
||||
before = len(zone.records)
|
||||
|
||||
gcloud_zone = self.gcloud_zones.get(zone.name)
|
||||
|
||||
if gcloud_zone:
|
||||
exists = True
|
||||
for gcloud_record in self._get_gcloud_records(gcloud_zone):
|
||||
if gcloud_record.record_type.upper() not in self.SUPPORTS:
|
||||
continue
|
||||
@@ -228,7 +232,9 @@ class GoogleCloudProvider(BaseProvider):
|
||||
record = Record.new(zone, record_name, data, source=self)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records', len(zone.records) - before)
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _data_for_A(self, gcloud_record):
|
||||
return {
|
||||
@@ -270,12 +276,15 @@ class GoogleCloudProvider(BaseProvider):
|
||||
|
||||
_data_for_PTR = _data_for_CNAME
|
||||
|
||||
_fix_semicolons = re.compile(r'(?<!\\);')
|
||||
|
||||
def _data_for_SPF(self, gcloud_record):
|
||||
if len(gcloud_record.rrdatas) > 1:
|
||||
return {
|
||||
'values': gcloud_record.rrdatas}
|
||||
'values': [self._fix_semicolons.sub('\;', rr)
|
||||
for rr in gcloud_record.rrdatas]}
|
||||
return {
|
||||
'value': gcloud_record.rrdatas[0]}
|
||||
'value': self._fix_semicolons.sub('\;', gcloud_record.rrdatas[0])}
|
||||
|
||||
def _data_for_SRV(self, gcloud_record):
|
||||
return {'values': [{
|
||||
|
||||
@@ -190,11 +190,13 @@ class Ns1Provider(BaseProvider):
|
||||
nsone_zone = self._client.loadZone(zone.name[:-1])
|
||||
records = nsone_zone.data['records']
|
||||
geo_records = nsone_zone.search(has_geo=True)
|
||||
exists = True
|
||||
except ResourceException as e:
|
||||
if e.message != self.ZONE_NOT_FOUND_MESSAGE:
|
||||
raise
|
||||
records = []
|
||||
geo_records = []
|
||||
exists = False
|
||||
|
||||
before = len(zone.records)
|
||||
# geo information isn't returned from the main endpoint, so we need
|
||||
@@ -208,8 +210,9 @@ class Ns1Provider(BaseProvider):
|
||||
source=self, lenient=lenient)
|
||||
zone_hash[(_type, name)] = record
|
||||
[zone.add_record(r) for r in zone_hash.values()]
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _params_for_A(self, record):
|
||||
params = {'answers': record.values, 'ttl': record.ttl}
|
||||
|
||||
@@ -11,6 +11,7 @@ import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import ovh
|
||||
from ovh import ResourceNotFoundError
|
||||
|
||||
from octodns.record import Record
|
||||
from .base import BaseProvider
|
||||
@@ -33,6 +34,7 @@ class OvhProvider(BaseProvider):
|
||||
"""
|
||||
|
||||
SUPPORTS_GEO = False
|
||||
ZONE_NOT_FOUND_MESSAGE = 'This service does not exist'
|
||||
|
||||
# This variable is also used in populate method to filter which OVH record
|
||||
# types are supported by octodns
|
||||
@@ -57,7 +59,14 @@ class OvhProvider(BaseProvider):
|
||||
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
||||
target, lenient)
|
||||
zone_name = zone.name[:-1]
|
||||
records = self.get_records(zone_name=zone_name)
|
||||
try:
|
||||
records = self.get_records(zone_name=zone_name)
|
||||
exists = True
|
||||
except ResourceNotFoundError as e:
|
||||
if e.message != self.ZONE_NOT_FOUND_MESSAGE:
|
||||
raise
|
||||
exists = False
|
||||
records = []
|
||||
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in records:
|
||||
@@ -75,8 +84,9 @@ class OvhProvider(BaseProvider):
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
|
||||
@@ -21,12 +21,13 @@ class Plan(object):
|
||||
MAX_SAFE_DELETE_PCENT = .3
|
||||
MIN_EXISTING_RECORDS = 10
|
||||
|
||||
def __init__(self, existing, desired, changes,
|
||||
def __init__(self, existing, desired, changes, exists,
|
||||
update_pcent_threshold=MAX_SAFE_UPDATE_PCENT,
|
||||
delete_pcent_threshold=MAX_SAFE_DELETE_PCENT):
|
||||
self.existing = existing
|
||||
self.desired = desired
|
||||
self.changes = changes
|
||||
self.exists = exists
|
||||
self.update_pcent_threshold = update_pcent_threshold
|
||||
self.delete_pcent_threshold = delete_pcent_threshold
|
||||
|
||||
@@ -123,6 +124,12 @@ class PlanLogger(_PlanOutput):
|
||||
buf.write(' (')
|
||||
buf.write(target)
|
||||
buf.write(')\n* ')
|
||||
|
||||
if plan.exists is False:
|
||||
buf.write('Create ')
|
||||
buf.write(str(plan.desired))
|
||||
buf.write('\n* ')
|
||||
|
||||
for change in plan.changes:
|
||||
buf.write(change.__repr__(leader='* '))
|
||||
buf.write('\n* ')
|
||||
@@ -168,6 +175,11 @@ class PlanMarkdown(_PlanOutput):
|
||||
fh.write('| Operation | Name | Type | TTL | Value | Source |\n'
|
||||
'|--|--|--|--|--|--|\n')
|
||||
|
||||
if plan.exists is False:
|
||||
fh.write('| Create | ')
|
||||
fh.write(str(plan.desired))
|
||||
fh.write(' | | | | |\n')
|
||||
|
||||
for change in plan.changes:
|
||||
existing = change.existing
|
||||
new = change.new
|
||||
@@ -193,7 +205,8 @@ class PlanMarkdown(_PlanOutput):
|
||||
fh.write(' | ')
|
||||
fh.write(_value_stringifier(new, '; '))
|
||||
fh.write(' | ')
|
||||
fh.write(new.source.id)
|
||||
if new.source:
|
||||
fh.write(new.source.id)
|
||||
fh.write(' |\n')
|
||||
|
||||
fh.write('\nSummary: ')
|
||||
@@ -229,6 +242,11 @@ class PlanHtml(_PlanOutput):
|
||||
</tr>
|
||||
''')
|
||||
|
||||
if plan.exists is False:
|
||||
fh.write(' <tr>\n <td>Create</td>\n <td colspan=5>')
|
||||
fh.write(str(plan.desired))
|
||||
fh.write('</td>\n </tr>\n')
|
||||
|
||||
for change in plan.changes:
|
||||
existing = change.existing
|
||||
new = change.new
|
||||
@@ -256,7 +274,8 @@ class PlanHtml(_PlanOutput):
|
||||
fh.write('</td>\n <td>')
|
||||
fh.write(_value_stringifier(new, '<br/>'))
|
||||
fh.write('</td>\n <td>')
|
||||
fh.write(new.source.id)
|
||||
if new.source:
|
||||
fh.write(new.source.id)
|
||||
fh.write('</td>\n </tr>\n')
|
||||
|
||||
fh.write(' <tr>\n <td colspan=6>Summary: ')
|
||||
|
||||
@@ -178,7 +178,7 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
raise Exception('PowerDNS unauthorized host={}'
|
||||
.format(self.host))
|
||||
elif e.response.status_code == 422:
|
||||
# 422 means powerdns doesn't know anything about the requsted
|
||||
# 422 means powerdns doesn't know anything about the requested
|
||||
# domain. We'll just ignore it here and leave the zone
|
||||
# untouched.
|
||||
pass
|
||||
@@ -187,8 +187,10 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
raise
|
||||
|
||||
before = len(zone.records)
|
||||
exists = False
|
||||
|
||||
if resp:
|
||||
exists = True
|
||||
for rrset in resp.json()['rrsets']:
|
||||
_type = rrset['type']
|
||||
if _type == 'SOA':
|
||||
@@ -199,8 +201,9 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _records_for_multiple(self, record):
|
||||
return [{'content': v, 'disabled': False}
|
||||
@@ -294,8 +297,8 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
return []
|
||||
|
||||
# sorting mostly to make things deterministic for testing, but in
|
||||
# theory it let us find what we're after quickier (though sorting would
|
||||
# ve more exepensive.)
|
||||
# theory it let us find what we're after quicker (though sorting would
|
||||
# be more expensive.)
|
||||
for record in sorted(existing.records):
|
||||
if record == ns:
|
||||
# We've found the top-level NS record, return any changes
|
||||
@@ -341,7 +344,7 @@ class PowerDnsBaseProvider(BaseProvider):
|
||||
e.response.text)
|
||||
raise
|
||||
self.log.info('_apply: creating zone=%s', desired.name)
|
||||
# 422 means powerdns doesn't know anything about the requsted
|
||||
# 422 means powerdns doesn't know anything about the requested
|
||||
# domain. We'll try to create it with the correct records instead
|
||||
# of update. Hopefully all the mods are creates :-)
|
||||
data = {
|
||||
|
||||
@@ -200,7 +200,7 @@ class RackspaceProvider(BaseProvider):
|
||||
raise Exception('Rackspace request unauthorized')
|
||||
elif e.response.status_code == 404:
|
||||
# Zone not found leaves the zone empty instead of failing.
|
||||
return
|
||||
return False
|
||||
raise
|
||||
|
||||
before = len(zone.records)
|
||||
@@ -217,8 +217,9 @@ class RackspaceProvider(BaseProvider):
|
||||
source=self)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
self.log.info('populate: found %s records, exists=True',
|
||||
len(zone.records) - before)
|
||||
return True
|
||||
|
||||
def _group_records(self, all_records):
|
||||
records = defaultdict(lambda: defaultdict(list))
|
||||
|
||||
@@ -61,7 +61,7 @@ class _Route53Record(object):
|
||||
|
||||
# NOTE: we're using __hash__ and __cmp__ methods that consider
|
||||
# _Route53Records equivalent if they have the same class, fqdn, and _type.
|
||||
# Values are ignored. This is usful when computing diffs/changes.
|
||||
# Values are ignored. This is useful when computing diffs/changes.
|
||||
|
||||
def __hash__(self):
|
||||
'sub-classes should never use this method'
|
||||
@@ -451,9 +451,11 @@ class Route53Provider(BaseProvider):
|
||||
target, lenient)
|
||||
|
||||
before = len(zone.records)
|
||||
exists = False
|
||||
|
||||
zone_id = self._get_zone_id(zone.name)
|
||||
if zone_id:
|
||||
exists = True
|
||||
records = defaultdict(lambda: defaultdict(list))
|
||||
for rrset in self._load_records(zone_id):
|
||||
record_name = zone.hostname_from_fqdn(rrset['Name'])
|
||||
@@ -483,8 +485,9 @@ class Route53Provider(BaseProvider):
|
||||
lenient=lenient)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
len(zone.records) - before)
|
||||
self.log.info('populate: found %s records, exists=%s',
|
||||
len(zone.records) - before, exists)
|
||||
return exists
|
||||
|
||||
def _gen_mods(self, action, records):
|
||||
'''
|
||||
@@ -679,7 +682,7 @@ class Route53Provider(BaseProvider):
|
||||
.get('CountryCode', False) == '*':
|
||||
# it's a default record
|
||||
continue
|
||||
# we expect a healtcheck now
|
||||
# we expect a healthcheck now
|
||||
try:
|
||||
health_check_id = rrset['HealthCheckId']
|
||||
caller_ref = \
|
||||
@@ -730,7 +733,7 @@ class Route53Provider(BaseProvider):
|
||||
batch_rs_count)
|
||||
# send the batch
|
||||
self._really_apply(batch, zone_id)
|
||||
# start a new batch with the lefovers
|
||||
# start a new batch with the leftovers
|
||||
batch = mods
|
||||
batch_rs_count = mods_rs_count
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ class YamlProvider(BaseProvider):
|
||||
if target:
|
||||
# When acting as a target we ignore any existing records so that we
|
||||
# create a completely new copy
|
||||
return
|
||||
return False
|
||||
|
||||
before = len(zone.records)
|
||||
filename = join(self.directory, '{}yaml'.format(zone.name))
|
||||
@@ -69,8 +69,9 @@ class YamlProvider(BaseProvider):
|
||||
lenient=lenient)
|
||||
zone.add_record(record)
|
||||
|
||||
self.log.info('populate: found %s records',
|
||||
self.log.info('populate: found %s records, exists=False',
|
||||
len(zone.records) - before)
|
||||
return False
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
|
||||
@@ -151,7 +151,7 @@ class Record(object):
|
||||
|
||||
# NOTE: we're using __hash__ and __cmp__ methods that consider Records
|
||||
# equivalent if they have the same name & _type. Values are ignored. This
|
||||
# is usful when computing diffs/changes.
|
||||
# is useful when computing diffs/changes.
|
||||
|
||||
def __hash__(self):
|
||||
return '{}:{}'.format(self.name, self._type).__hash__()
|
||||
|
||||
@@ -22,7 +22,7 @@ class BaseSource(object):
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
'''
|
||||
Loads all zones the provider knows about
|
||||
Loads all records the provider knows about for the provided zone
|
||||
|
||||
When `target` is True the populate call is being made to load the
|
||||
current state of the provider.
|
||||
@@ -31,6 +31,9 @@ class BaseSource(object):
|
||||
do a "best effort" load of data. That will allow through some common,
|
||||
but not best practices stuff that we otherwise would reject. E.g. no
|
||||
trailing . or mising escapes for ;.
|
||||
|
||||
When target is True (loading current state) this method should return
|
||||
True if the zone exists or False if it does not.
|
||||
'''
|
||||
raise NotImplementedError('Abstract base class, populate method '
|
||||
'missing')
|
||||
|
||||
@@ -37,10 +37,10 @@ class Zone(object):
|
||||
if not name[-1] == '.':
|
||||
raise Exception('Invalid zone name {}, missing ending dot'
|
||||
.format(name))
|
||||
# Force everyting to lowercase just to be safe
|
||||
# Force everything to lowercase just to be safe
|
||||
self.name = unicode(name).lower() if name else name
|
||||
self.sub_zones = sub_zones
|
||||
# We're grouping by node, it allows us to efficently search for
|
||||
# We're grouping by node, it allows us to efficiently search for
|
||||
# duplicates and detect when CNAMEs co-exist with other records
|
||||
self._records = defaultdict(set)
|
||||
# optional leading . to match empty hostname
|
||||
|
||||
@@ -11,4 +11,4 @@ git tag -s v$VERSION -m "Release $VERSION"
|
||||
git push origin v$VERSION
|
||||
echo "Tagged and pushed v$VERSION"
|
||||
python setup.py sdist upload
|
||||
echo "Updloaded $VERSION"
|
||||
echo "Uploaded $VERSION"
|
||||
|
||||
@@ -16,15 +16,6 @@ from octodns.zone import Zone
|
||||
from helpers import SimpleProvider
|
||||
|
||||
|
||||
class TestPlanLogger(TestCase):
|
||||
|
||||
def test_invalid_level(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
PlanLogger('invalid', 'not-a-level')
|
||||
self.assertEquals('Unsupported level: not-a-level',
|
||||
ctx.exception.message)
|
||||
|
||||
|
||||
simple = SimpleProvider()
|
||||
zone = Zone('unit.tests.', [])
|
||||
existing = Record.new(zone, 'a', {
|
||||
@@ -48,15 +39,45 @@ create = Create(Record.new(zone, 'b', {
|
||||
'type': 'CNAME',
|
||||
'value': 'foo.unit.tests.'
|
||||
}, simple))
|
||||
create2 = Create(Record.new(zone, 'c', {
|
||||
'ttl': 60,
|
||||
'type': 'CNAME',
|
||||
'value': 'foo.unit.tests.'
|
||||
}))
|
||||
update = Update(existing, new)
|
||||
delete = Delete(new)
|
||||
changes = [create, delete, update]
|
||||
changes = [create, create2, delete, update]
|
||||
plans = [
|
||||
(simple, Plan(zone, zone, changes)),
|
||||
(simple, Plan(zone, zone, changes)),
|
||||
(simple, Plan(zone, zone, changes, True)),
|
||||
(simple, Plan(zone, zone, changes, False)),
|
||||
]
|
||||
|
||||
|
||||
class TestPlanLogger(TestCase):
|
||||
|
||||
def test_invalid_level(self):
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
PlanLogger('invalid', 'not-a-level')
|
||||
self.assertEquals('Unsupported level: not-a-level',
|
||||
ctx.exception.message)
|
||||
|
||||
def test_create(self):
|
||||
|
||||
class MockLogger(object):
|
||||
|
||||
def __init__(self):
|
||||
self.out = StringIO()
|
||||
|
||||
def log(self, level, msg):
|
||||
self.out.write(msg)
|
||||
|
||||
log = MockLogger()
|
||||
PlanLogger('logger').run(log, plans)
|
||||
out = log.out.getvalue()
|
||||
self.assertTrue('Summary: Creates=2, Updates=1, '
|
||||
'Deletes=1, Existing Records=0' in out)
|
||||
|
||||
|
||||
class TestPlanHtml(TestCase):
|
||||
log = getLogger('TestPlanHtml')
|
||||
|
||||
@@ -69,7 +90,7 @@ class TestPlanHtml(TestCase):
|
||||
out = StringIO()
|
||||
PlanHtml('html').run(plans, fh=out)
|
||||
out = out.getvalue()
|
||||
self.assertTrue(' <td colspan=6>Summary: Creates=1, Updates=1, '
|
||||
self.assertTrue(' <td colspan=6>Summary: Creates=2, Updates=1, '
|
||||
'Deletes=1, Existing Records=0</td>' in out)
|
||||
|
||||
|
||||
|
||||
@@ -302,7 +302,8 @@ class TestAzureDnsProvider(TestCase):
|
||||
record_list = provider._dns_client.record_sets.list_by_dns_zone
|
||||
record_list.return_value = rs
|
||||
|
||||
provider.populate(zone)
|
||||
exists = provider.populate(zone)
|
||||
self.assertTrue(exists)
|
||||
|
||||
self.assertEquals(len(zone.records), 16)
|
||||
|
||||
@@ -338,8 +339,10 @@ class TestAzureDnsProvider(TestCase):
|
||||
changes.append(Create(i))
|
||||
deletes.append(Delete(i))
|
||||
|
||||
self.assertEquals(13, provider.apply(Plan(None, zone, changes)))
|
||||
self.assertEquals(13, provider.apply(Plan(zone, zone, deletes)))
|
||||
self.assertEquals(13, provider.apply(Plan(None, zone,
|
||||
changes, True)))
|
||||
self.assertEquals(13, provider.apply(Plan(zone, zone,
|
||||
deletes, True)))
|
||||
|
||||
def test_create_zone(self):
|
||||
provider = self._get_provider()
|
||||
@@ -354,7 +357,8 @@ class TestAzureDnsProvider(TestCase):
|
||||
_get = provider._dns_client.zones.get
|
||||
_get.side_effect = CloudError(Mock(status=404), err_msg)
|
||||
|
||||
self.assertEquals(13, provider.apply(Plan(None, desired, changes)))
|
||||
self.assertEquals(13, provider.apply(Plan(None, desired, changes,
|
||||
True)))
|
||||
|
||||
def test_check_zone_no_create(self):
|
||||
provider = self._get_provider()
|
||||
@@ -374,6 +378,7 @@ class TestAzureDnsProvider(TestCase):
|
||||
_get = provider._dns_client.zones.get
|
||||
_get.side_effect = CloudError(Mock(status=404), err_msg)
|
||||
|
||||
provider.populate(Zone('unit3.test.', []))
|
||||
exists = provider.populate(Zone('unit3.test.', []))
|
||||
self.assertFalse(exists)
|
||||
|
||||
self.assertEquals(len(zone.records), 0)
|
||||
|
||||
@@ -63,14 +63,14 @@ class TestBaseProvider(TestCase):
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupportsGeo('hassupportesgeo').populate(zone)
|
||||
HasSupportsGeo('hassupportsgeo').populate(zone)
|
||||
self.assertEquals('Abstract base class, SUPPORTS property missing',
|
||||
ctx.exception.message)
|
||||
|
||||
class HasSupports(HasSupportsGeo):
|
||||
SUPPORTS = set(('A',))
|
||||
with self.assertRaises(NotImplementedError) as ctx:
|
||||
HasSupports('hassupportes').populate(zone)
|
||||
HasSupports('hassupports').populate(zone)
|
||||
self.assertEquals('Abstract base class, populate method missing',
|
||||
ctx.exception.message)
|
||||
|
||||
@@ -94,7 +94,7 @@ class TestBaseProvider(TestCase):
|
||||
'value': '1.2.3.4'
|
||||
}))
|
||||
|
||||
self.assertTrue(HasSupports('hassupportesgeo')
|
||||
self.assertTrue(HasSupports('hassupportsgeo')
|
||||
.supports(list(zone.records)[0]))
|
||||
|
||||
plan = HasPopulate('haspopulate').plan(zone)
|
||||
@@ -153,7 +153,7 @@ class TestBaseProvider(TestCase):
|
||||
|
||||
def test_safe_none(self):
|
||||
# No changes is safe
|
||||
Plan(None, None, []).raise_if_unsafe()
|
||||
Plan(None, None, [], True).raise_if_unsafe()
|
||||
|
||||
def test_safe_creates(self):
|
||||
# Creates are safe when existing records is under MIN_EXISTING_RECORDS
|
||||
@@ -164,7 +164,8 @@ class TestBaseProvider(TestCase):
|
||||
'type': 'A',
|
||||
'value': '1.2.3.4',
|
||||
})
|
||||
Plan(zone, zone, [Create(record) for i in range(10)]).raise_if_unsafe()
|
||||
Plan(zone, zone, [Create(record) for i in range(10)], True) \
|
||||
.raise_if_unsafe()
|
||||
|
||||
def test_safe_min_existing_creates(self):
|
||||
# Creates are safe when existing records is over MIN_EXISTING_RECORDS
|
||||
@@ -183,7 +184,8 @@ class TestBaseProvider(TestCase):
|
||||
'value': '2.3.4.5'
|
||||
}))
|
||||
|
||||
Plan(zone, zone, [Create(record) for i in range(10)]).raise_if_unsafe()
|
||||
Plan(zone, zone, [Create(record) for i in range(10)], True) \
|
||||
.raise_if_unsafe()
|
||||
|
||||
def test_safe_no_existing(self):
|
||||
# existing records fewer than MIN_EXISTING_RECORDS is safe
|
||||
@@ -195,7 +197,7 @@ class TestBaseProvider(TestCase):
|
||||
})
|
||||
|
||||
updates = [Update(record, record), Update(record, record)]
|
||||
Plan(zone, zone, updates).raise_if_unsafe()
|
||||
Plan(zone, zone, updates, True).raise_if_unsafe()
|
||||
|
||||
def test_safe_updates_min_existing(self):
|
||||
# MAX_SAFE_UPDATE_PCENT+1 fails when more
|
||||
@@ -219,7 +221,7 @@ class TestBaseProvider(TestCase):
|
||||
Plan.MAX_SAFE_UPDATE_PCENT) + 1)]
|
||||
|
||||
with self.assertRaises(UnsafePlan) as ctx:
|
||||
Plan(zone, zone, changes).raise_if_unsafe()
|
||||
Plan(zone, zone, changes, True).raise_if_unsafe()
|
||||
|
||||
self.assertTrue('Too many updates' in ctx.exception.message)
|
||||
|
||||
@@ -243,7 +245,7 @@ class TestBaseProvider(TestCase):
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS *
|
||||
Plan.MAX_SAFE_UPDATE_PCENT))]
|
||||
|
||||
Plan(zone, zone, changes).raise_if_unsafe()
|
||||
Plan(zone, zone, changes, True).raise_if_unsafe()
|
||||
|
||||
def test_safe_deletes_min_existing(self):
|
||||
# MAX_SAFE_DELETE_PCENT+1 fails when more
|
||||
@@ -267,7 +269,7 @@ class TestBaseProvider(TestCase):
|
||||
Plan.MAX_SAFE_DELETE_PCENT) + 1)]
|
||||
|
||||
with self.assertRaises(UnsafePlan) as ctx:
|
||||
Plan(zone, zone, changes).raise_if_unsafe()
|
||||
Plan(zone, zone, changes, True).raise_if_unsafe()
|
||||
|
||||
self.assertTrue('Too many deletes' in ctx.exception.message)
|
||||
|
||||
@@ -291,7 +293,7 @@ class TestBaseProvider(TestCase):
|
||||
for i in range(int(Plan.MIN_EXISTING_RECORDS *
|
||||
Plan.MAX_SAFE_DELETE_PCENT))]
|
||||
|
||||
Plan(zone, zone, changes).raise_if_unsafe()
|
||||
Plan(zone, zone, changes, True).raise_if_unsafe()
|
||||
|
||||
def test_safe_updates_min_existing_override(self):
|
||||
safe_pcent = .4
|
||||
@@ -316,7 +318,7 @@ class TestBaseProvider(TestCase):
|
||||
safe_pcent) + 1)]
|
||||
|
||||
with self.assertRaises(UnsafePlan) as ctx:
|
||||
Plan(zone, zone, changes,
|
||||
Plan(zone, zone, changes, True,
|
||||
update_pcent_threshold=safe_pcent).raise_if_unsafe()
|
||||
|
||||
self.assertTrue('Too many updates' in ctx.exception.message)
|
||||
@@ -344,7 +346,7 @@ class TestBaseProvider(TestCase):
|
||||
safe_pcent) + 1)]
|
||||
|
||||
with self.assertRaises(UnsafePlan) as ctx:
|
||||
Plan(zone, zone, changes,
|
||||
Plan(zone, zone, changes, True,
|
||||
delete_pcent_threshold=safe_pcent).raise_if_unsafe()
|
||||
|
||||
self.assertTrue('Too many deletes' in ctx.exception.message)
|
||||
|
||||
@@ -166,6 +166,7 @@ class TestCloudflareProvider(TestCase):
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(12, len(plan.changes))
|
||||
self.assertEquals(12, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
# created the domain
|
||||
@@ -285,6 +286,7 @@ class TestCloudflareProvider(TestCase):
|
||||
# only see the delete & ttl update, below min-ttl is filtered out
|
||||
self.assertEquals(2, len(plan.changes))
|
||||
self.assertEquals(2, provider.apply(plan))
|
||||
self.assertTrue(plan.exists)
|
||||
# recreate for update, and deletes for the 2 parts of the other
|
||||
provider._request.assert_has_calls([
|
||||
call('PUT', '/zones/ff12ab34cd5611334422ab3322997650/dns_records/'
|
||||
@@ -366,7 +368,7 @@ class TestCloudflareProvider(TestCase):
|
||||
'values': ['2.2.2.2', '3.3.3.3', '4.4.4.4'],
|
||||
})
|
||||
change = Update(existing, new)
|
||||
plan = Plan(zone, zone, [change])
|
||||
plan = Plan(zone, zone, [change], True)
|
||||
provider._apply(plan)
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
@@ -451,7 +453,7 @@ class TestCloudflareProvider(TestCase):
|
||||
'value': 'ns2.foo.bar.',
|
||||
})
|
||||
change = Update(existing, new)
|
||||
plan = Plan(zone, zone, [change])
|
||||
plan = Plan(zone, zone, [change], True)
|
||||
provider._apply(plan)
|
||||
|
||||
provider._request.assert_has_calls([
|
||||
@@ -599,7 +601,8 @@ class TestCloudflareProvider(TestCase):
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
|
||||
# the two A records get merged into one CNAME record poining to the CDN
|
||||
# the two A records get merged into one CNAME record pointing to
|
||||
# the CDN.
|
||||
self.assertEquals(3, len(zone.records))
|
||||
|
||||
record = list(zone.records)[0]
|
||||
@@ -621,7 +624,7 @@ class TestCloudflareProvider(TestCase):
|
||||
self.assertEquals('a.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 itsself.
|
||||
# never point a Cloudflare record to itself.
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, 'cname', {
|
||||
'ttl': 300,
|
||||
@@ -676,7 +679,7 @@ class TestCloudflareProvider(TestCase):
|
||||
self.assertEquals('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 itsself.
|
||||
# never point a Cloudflare record to itself.
|
||||
wanted = Zone('unit.tests.', [])
|
||||
wanted.add_record(Record.new(wanted, '', {
|
||||
'ttl': 300,
|
||||
|
||||
@@ -165,6 +165,7 @@ class TestDigitalOceanProvider(TestCase):
|
||||
n = len(self.expected.records) - 7
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._client._request.assert_has_calls([
|
||||
# created the domain
|
||||
@@ -225,6 +226,7 @@ class TestDigitalOceanProvider(TestCase):
|
||||
}))
|
||||
|
||||
plan = provider.plan(wanted)
|
||||
self.assertTrue(plan.exists)
|
||||
self.assertEquals(2, len(plan.changes))
|
||||
self.assertEquals(2, provider.apply(plan))
|
||||
# recreate for update, and delete for the 2 parts of the other
|
||||
|
||||
@@ -133,6 +133,7 @@ class TestDnsimpleProvider(TestCase):
|
||||
n = len(self.expected.records) - 3
|
||||
self.assertEquals(n, len(plan.changes))
|
||||
self.assertEquals(n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
provider._client._request.assert_has_calls([
|
||||
# created the domain
|
||||
@@ -186,6 +187,7 @@ class TestDnsimpleProvider(TestCase):
|
||||
}))
|
||||
|
||||
plan = provider.plan(wanted)
|
||||
self.assertTrue(plan.exists)
|
||||
self.assertEquals(2, len(plan.changes))
|
||||
self.assertEquals(2, provider.apply(plan))
|
||||
# recreate for update, and deletes for the 2 parts of the other
|
||||
|
||||
@@ -430,6 +430,7 @@ class TestDynProvider(TestCase):
|
||||
update_mock.assert_not_called()
|
||||
provider.apply(plan)
|
||||
update_mock.assert_called()
|
||||
self.assertFalse(plan.exists)
|
||||
add_mock.assert_called()
|
||||
# Once for each dyn record (8 Records, 2 of which have dual values)
|
||||
self.assertEquals(15, len(add_mock.call_args_list))
|
||||
@@ -474,6 +475,7 @@ class TestDynProvider(TestCase):
|
||||
plan = provider.plan(new)
|
||||
provider.apply(plan)
|
||||
update_mock.assert_called()
|
||||
self.assertTrue(plan.exists)
|
||||
# we expect 4 deletes, 2 from actual deletes and 2 from
|
||||
# updates which delete and recreate
|
||||
self.assertEquals(4, len(delete_mock.call_args_list))
|
||||
@@ -491,7 +493,7 @@ class TestDynProviderGeo(TestCase):
|
||||
traffic_director_response = loads(fh.read())
|
||||
|
||||
@property
|
||||
def traffic_directors_reponse(self):
|
||||
def traffic_directors_response(self):
|
||||
return {
|
||||
'data': [{
|
||||
'active': 'Y',
|
||||
@@ -607,7 +609,7 @@ class TestDynProviderGeo(TestCase):
|
||||
mock.side_effect = [{'data': []}]
|
||||
self.assertEquals({}, provider.traffic_directors)
|
||||
|
||||
# a supported td and an ingored one
|
||||
# a supported td and an ignored one
|
||||
response = {
|
||||
'data': [{
|
||||
'active': 'Y',
|
||||
@@ -650,7 +652,7 @@ class TestDynProviderGeo(TestCase):
|
||||
set(tds.keys()))
|
||||
self.assertEquals(['A'], tds['unit.tests.'].keys())
|
||||
self.assertEquals(['A'], tds['geo.unit.tests.'].keys())
|
||||
provider.log.warn.assert_called_with("Failed to load TraficDirector "
|
||||
provider.log.warn.assert_called_with("Failed to load TrafficDirector "
|
||||
"'%s': %s", 'something else',
|
||||
'need more than 1 value to '
|
||||
'unpack')
|
||||
@@ -758,7 +760,7 @@ class TestDynProviderGeo(TestCase):
|
||||
# only traffic director
|
||||
mock.side_effect = [
|
||||
# get traffic directors
|
||||
self.traffic_directors_reponse,
|
||||
self.traffic_directors_response,
|
||||
# get traffic director
|
||||
self.traffic_director_response,
|
||||
# get zone
|
||||
@@ -809,7 +811,7 @@ class TestDynProviderGeo(TestCase):
|
||||
# both traffic director and regular, regular is ignored
|
||||
mock.side_effect = [
|
||||
# get traffic directors
|
||||
self.traffic_directors_reponse,
|
||||
self.traffic_directors_response,
|
||||
# get traffic director
|
||||
self.traffic_director_response,
|
||||
# get zone
|
||||
@@ -859,7 +861,7 @@ class TestDynProviderGeo(TestCase):
|
||||
# busted traffic director
|
||||
mock.side_effect = [
|
||||
# get traffic directors
|
||||
self.traffic_directors_reponse,
|
||||
self.traffic_directors_response,
|
||||
# get traffic director
|
||||
busted_traffic_director_response,
|
||||
# get zone
|
||||
@@ -913,7 +915,7 @@ class TestDynProviderGeo(TestCase):
|
||||
Delete(geo),
|
||||
Delete(regular),
|
||||
]
|
||||
plan = Plan(None, desired, changes)
|
||||
plan = Plan(None, desired, changes, True)
|
||||
provider._apply(plan)
|
||||
mock.assert_has_calls([
|
||||
call('/Zone/unit.tests/', 'GET', {}),
|
||||
@@ -932,14 +934,14 @@ class TestDynProviderGeo(TestCase):
|
||||
provider = DynProvider('test', 'cust', 'user', 'pass',
|
||||
traffic_directors_enabled=True)
|
||||
|
||||
# will be tested seperately
|
||||
# will be tested separately
|
||||
provider._mod_rulesets = MagicMock()
|
||||
|
||||
mock.side_effect = [
|
||||
# create traffic director
|
||||
self.traffic_director_response,
|
||||
# get traffic directors
|
||||
self.traffic_directors_reponse
|
||||
self.traffic_directors_response
|
||||
]
|
||||
provider._mod_geo_Create(None, Create(self.geo_record))
|
||||
# td now lives in cache
|
||||
|
||||
@@ -263,7 +263,8 @@ class TestGoogleCloudProvider(TestCase):
|
||||
provider.apply(Plan(
|
||||
existing=[update_existing_r, delete_r],
|
||||
desired=desired,
|
||||
changes=changes
|
||||
changes=changes,
|
||||
exists=True
|
||||
))
|
||||
|
||||
calls_mock = gcloud_zone_mock.changes.return_value
|
||||
@@ -295,7 +296,8 @@ class TestGoogleCloudProvider(TestCase):
|
||||
provider.apply(Plan(
|
||||
existing=[update_existing_r, delete_r],
|
||||
desired=desired,
|
||||
changes=changes
|
||||
changes=changes,
|
||||
exists=True
|
||||
))
|
||||
|
||||
unsupported_change = Mock()
|
||||
@@ -357,15 +359,17 @@ class TestGoogleCloudProvider(TestCase):
|
||||
"unit.tests.")
|
||||
|
||||
test_zone = Zone('unit.tests.', [])
|
||||
provider.populate(test_zone)
|
||||
exists = provider.populate(test_zone)
|
||||
self.assertTrue(exists)
|
||||
|
||||
# test_zone gets fed the same records as zone does, except it's in
|
||||
# the format returned by google API, so after populate they should look
|
||||
# excactly the same.
|
||||
# exactly the same.
|
||||
self.assertEqual(test_zone.records, zone.records)
|
||||
|
||||
test_zone2 = Zone('nonexistant.zone.', [])
|
||||
provider.populate(test_zone2, False, False)
|
||||
test_zone2 = Zone('nonexistent.zone.', [])
|
||||
exists = provider.populate(test_zone2, False, False)
|
||||
self.assertFalse(exists)
|
||||
|
||||
self.assertEqual(len(test_zone2.records), 0,
|
||||
msg="Zone should not get records from wrong domain")
|
||||
@@ -401,8 +405,8 @@ class TestGoogleCloudProvider(TestCase):
|
||||
provider.gcloud_client.list_zones = Mock(
|
||||
return_value=DummyIterator([]))
|
||||
|
||||
self.assertIsNone(provider.gcloud_zones.get("nonexistant.xone"),
|
||||
msg="Check that nonexistant zones return None when"
|
||||
self.assertIsNone(provider.gcloud_zones.get("nonexistent.zone"),
|
||||
msg="Check that nonexistent zones return None when"
|
||||
"there's no create=True flag")
|
||||
|
||||
def test__get_rrsets(self):
|
||||
@@ -423,7 +427,7 @@ class TestGoogleCloudProvider(TestCase):
|
||||
provider.gcloud_client.list_zones = Mock(
|
||||
return_value=DummyIterator([]))
|
||||
|
||||
mock_zone = provider._create_gcloud_zone("nonexistant.zone.mock")
|
||||
mock_zone = provider._create_gcloud_zone("nonexistent.zone.mock")
|
||||
|
||||
mock_zone.create.assert_called()
|
||||
provider.gcloud_client.zone.assert_called()
|
||||
@@ -442,3 +446,13 @@ class TestGoogleCloudProvider(TestCase):
|
||||
|
||||
self.assertRegexpMatches(mock_zone.name, '^[a-z][a-z0-9-]*[a-z0-9]$')
|
||||
self.assertEqual(len(mock_zone.name), 63)
|
||||
|
||||
def test_semicolon_fixup(self):
|
||||
provider = self._get_provider()
|
||||
|
||||
self.assertEquals({
|
||||
'values': ['abcd\\; ef\\;g', 'hij\\; klm\\;n']
|
||||
}, provider._data_for_TXT(
|
||||
DummyResourceRecordSet(
|
||||
'unit.tests.', 'TXT', 0, ['abcd; ef;g', 'hij\\; klm\\;n'])
|
||||
))
|
||||
|
||||
@@ -196,9 +196,10 @@ class TestNs1Provider(TestCase):
|
||||
load_mock.side_effect = \
|
||||
ResourceException('server error: zone not found')
|
||||
zone = Zone('unit.tests.', [])
|
||||
provider.populate(zone)
|
||||
exists = provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
self.assertEquals(('unit.tests',), load_mock.call_args[0])
|
||||
self.assertFalse(exists)
|
||||
|
||||
# Existing zone w/o records
|
||||
load_mock.reset_mock()
|
||||
@@ -269,6 +270,7 @@ class TestNs1Provider(TestCase):
|
||||
# everything except the root NS
|
||||
expected_n = len(self.expected) - 1
|
||||
self.assertEquals(expected_n, len(plan.changes))
|
||||
self.assertTrue(plan.exists)
|
||||
|
||||
# Fails, general error
|
||||
load_mock.reset_mock()
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function, \
|
||||
from unittest import TestCase
|
||||
|
||||
from mock import patch, call
|
||||
from ovh import APIError
|
||||
from ovh import APIError, ResourceNotFoundError, InvalidCredential
|
||||
|
||||
from octodns.provider.ovh import OvhProvider
|
||||
from octodns.record import Record
|
||||
@@ -199,14 +199,14 @@ class TestOvhProvider(TestCase):
|
||||
api_record.append({
|
||||
'fieldType': 'SPF',
|
||||
'ttl': 1000,
|
||||
'target': 'v=spf1 include:unit.texts.rerirect ~all',
|
||||
'target': 'v=spf1 include:unit.texts.redirect ~all',
|
||||
'subDomain': '',
|
||||
'id': 13
|
||||
})
|
||||
expected.add(Record.new(zone, '', {
|
||||
'ttl': 1000,
|
||||
'type': 'SPF',
|
||||
'value': 'v=spf1 include:unit.texts.rerirect ~all'
|
||||
'value': 'v=spf1 include:unit.texts.redirect ~all'
|
||||
}))
|
||||
|
||||
# SSHFP
|
||||
@@ -307,18 +307,30 @@ class TestOvhProvider(TestCase):
|
||||
|
||||
with patch.object(provider._client, 'get') as get_mock:
|
||||
zone = Zone('unit.tests.', [])
|
||||
get_mock.side_effect = APIError('boom')
|
||||
get_mock.side_effect = ResourceNotFoundError('boom')
|
||||
with self.assertRaises(APIError) as ctx:
|
||||
provider.populate(zone)
|
||||
self.assertEquals(get_mock.side_effect, ctx.exception)
|
||||
|
||||
with patch.object(provider._client, 'get') as get_mock:
|
||||
get_mock.side_effect = InvalidCredential('boom')
|
||||
with self.assertRaises(APIError) as ctx:
|
||||
provider.populate(zone)
|
||||
self.assertEquals(get_mock.side_effect, ctx.exception)
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
get_mock.side_effect = ResourceNotFoundError('This service does '
|
||||
'not exist')
|
||||
exists = provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
self.assertFalse(exists)
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
get_returns = [[record['id'] for record in self.api_record]]
|
||||
get_returns += self.api_record
|
||||
get_mock.side_effect = get_returns
|
||||
provider.populate(zone)
|
||||
exists = provider.populate(zone)
|
||||
self.assertEquals(self.expected, zone.records)
|
||||
self.assertTrue(exists)
|
||||
|
||||
@patch('ovh.Client')
|
||||
def test_is_valid_dkim(self, client_mock):
|
||||
@@ -404,7 +416,7 @@ class TestOvhProvider(TestCase):
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'SPF',
|
||||
subDomain=u'', ttl=1000,
|
||||
target=u'v=spf1 include:unit.texts.'
|
||||
u'rerirect ~all',
|
||||
u'redirect ~all',
|
||||
),
|
||||
call(u'/domain/zone/unit.tests/record', fieldType=u'A',
|
||||
subDomain='sub', target=u'1.2.3.4', ttl=200),
|
||||
|
||||
@@ -100,12 +100,13 @@ class TestPowerDnsProvider(TestCase):
|
||||
# No existing records -> creates for every record in expected
|
||||
with requests_mock() as mock:
|
||||
mock.get(ANY, status_code=200, text=EMPTY_TEXT)
|
||||
# post 201, is reponse to the create with data
|
||||
# post 201, is response to the create with data
|
||||
mock.patch(ANY, status_code=201, text=assert_rrsets_callback)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
self.assertEquals(expected_n, len(plan.changes))
|
||||
self.assertEquals(expected_n, provider.apply(plan))
|
||||
self.assertTrue(plan.exists)
|
||||
|
||||
# Non-existent zone -> creates for every record in expected
|
||||
# OMG this is fucking ugly, probably better to ditch requests_mocks and
|
||||
@@ -118,12 +119,13 @@ class TestPowerDnsProvider(TestCase):
|
||||
mock.get(ANY, status_code=422, text='')
|
||||
# patch 422's, unknown zone
|
||||
mock.patch(ANY, status_code=422, text=dumps(not_found))
|
||||
# post 201, is reponse to the create with data
|
||||
# post 201, is response to the create with data
|
||||
mock.post(ANY, status_code=201, text=assert_rrsets_callback)
|
||||
|
||||
plan = provider.plan(expected)
|
||||
self.assertEquals(expected_n, len(plan.changes))
|
||||
self.assertEquals(expected_n, provider.apply(plan))
|
||||
self.assertFalse(plan.exists)
|
||||
|
||||
with requests_mock() as mock:
|
||||
# get 422's, unknown zone
|
||||
|
||||
@@ -73,9 +73,10 @@ class TestRackspaceProvider(TestCase):
|
||||
json={'error': "Could not find domain 'unit.tests.'"})
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
self.provider.populate(zone)
|
||||
exists = self.provider.populate(zone)
|
||||
self.assertEquals(set(), zone.records)
|
||||
self.assertTrue(mock.called_once)
|
||||
self.assertFalse(exists)
|
||||
|
||||
def test_multipage_populate(self):
|
||||
with requests_mock() as mock:
|
||||
@@ -109,6 +110,7 @@ class TestRackspaceProvider(TestCase):
|
||||
|
||||
plan = self.provider.plan(expected)
|
||||
self.assertTrue(mock.called)
|
||||
self.assertTrue(plan.exists)
|
||||
|
||||
# OctoDNS does not propagate top-level NS records.
|
||||
self.assertEquals(1, len(plan.changes))
|
||||
|
||||
@@ -331,9 +331,9 @@ class TestRoute53Provider(TestCase):
|
||||
stubber.assert_no_pending_responses()
|
||||
|
||||
# Populate a zone that doesn't exist
|
||||
noexist = Zone('does.not.exist.', [])
|
||||
provider.populate(noexist)
|
||||
self.assertEquals(set(), noexist.records)
|
||||
nonexistent = Zone('does.not.exist.', [])
|
||||
provider.populate(nonexistent)
|
||||
self.assertEquals(set(), nonexistent.records)
|
||||
|
||||
def test_sync(self):
|
||||
provider, stubber = self._get_stubbed_provider()
|
||||
@@ -361,6 +361,7 @@ class TestRoute53Provider(TestCase):
|
||||
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(9, len(plan.changes))
|
||||
self.assertTrue(plan.exists)
|
||||
for change in plan.changes:
|
||||
self.assertIsInstance(change, Create)
|
||||
stubber.assert_no_pending_responses()
|
||||
@@ -593,6 +594,7 @@ class TestRoute53Provider(TestCase):
|
||||
|
||||
plan = provider.plan(self.expected)
|
||||
self.assertEquals(9, len(plan.changes))
|
||||
self.assertFalse(plan.exists)
|
||||
for change in plan.changes:
|
||||
self.assertIsInstance(change, Create)
|
||||
stubber.assert_no_pending_responses()
|
||||
|
||||
@@ -430,7 +430,7 @@ class TestRecord(TestCase):
|
||||
self.assertEqual(change.new, other)
|
||||
|
||||
# full sorting
|
||||
# equivilent
|
||||
# equivalent
|
||||
b_naptr_value = b.values[0]
|
||||
self.assertEquals(0, b_naptr_value.__cmp__(b_naptr_value))
|
||||
# by order
|
||||
@@ -710,7 +710,7 @@ class TestRecord(TestCase):
|
||||
Record.new(self.zone, 'unknown', {})
|
||||
self.assertTrue('missing type' in ctx.exception.message)
|
||||
|
||||
# Unkown type
|
||||
# Unknown type
|
||||
with self.assertRaises(Exception) as ctx:
|
||||
Record.new(self.zone, 'unknown', {
|
||||
'type': 'XXX',
|
||||
|
||||
Reference in New Issue
Block a user