Browse Source

Merge pull request #182 from vanbroup/cloudflare-proxied

Option to handle Cloudflare proxied records
pull/192/head
Ross McFarland 8 years ago
committed by GitHub
parent
commit
43f6ff7210
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 263 additions and 13 deletions
  1. +61
    -12
      octodns/provider/cloudflare.py
  2. +202
    -1
      tests/test_octodns_provider_cloudflare.py

+ 61
- 12
octodns/provider/cloudflare.py View File

@ -14,14 +14,18 @@ from ..record import Record, Update
from .base import BaseProvider
class CloudflareAuthenticationError(Exception):
class CloudflareError(Exception):
def __init__(self, data):
try:
message = data['errors'][0]['message']
except (IndexError, KeyError):
message = 'Authentication error'
super(CloudflareAuthenticationError, self).__init__(message)
message = 'Cloudflare error'
super(CloudflareError, self).__init__(message)
class CloudflareAuthenticationError(CloudflareError):
def __init__(self, data):
CloudflareError.__init__(self, data)
class CloudflareProvider(BaseProvider):
@ -34,6 +38,12 @@ class CloudflareProvider(BaseProvider):
email: dns-manager@example.com
# The api key (required)
token: foo
# Import CDN enabled records as CNAME to {}.cdn.cloudflare.net. Records
# ending at .cdn.cloudflare.net. will be ignored when this provider is
# not used as the source and the cdn option is enabled.
#
# See: https://support.cloudflare.com/hc/en-us/articles/115000830351
cdn: false
'''
SUPPORTS_GEO = False
SUPPORTS = set(('ALIAS', 'A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'SRV',
@ -42,16 +52,18 @@ class CloudflareProvider(BaseProvider):
MIN_TTL = 120
TIMEOUT = 15
def __init__(self, id, email, token, *args, **kwargs):
def __init__(self, id, email, token, cdn=False, *args, **kwargs):
self.log = getLogger('CloudflareProvider[{}]'.format(id))
self.log.debug('__init__: id=%s, email=%s, token=***', id, email)
super(CloudflareProvider, self).__init__(id, *args, **kwargs)
self.log.debug('__init__: id=%s, email=%s, token=***, cdn=%s', id,
email, cdn)
super(CloudflareProvider, self).__init__(id, cdn, *args, **kwargs)
sess = Session()
sess.headers.update({
'X-Auth-Email': email,
'X-Auth-Key': token,
})
self.cdn = cdn
self._sess = sess
self._zones = None
@ -64,8 +76,11 @@ class CloudflareProvider(BaseProvider):
resp = self._sess.request(method, url, params=params, json=data,
timeout=self.TIMEOUT)
self.log.debug('_request: status=%d', resp.status_code)
if resp.status_code == 400:
raise CloudflareError(resp.json())
if resp.status_code == 403:
raise CloudflareAuthenticationError(resp.json())
resp.raise_for_status()
return resp.json()
@ -87,6 +102,18 @@ class CloudflareProvider(BaseProvider):
return self._zones
def _data_for_cdn(self, name, _type, records):
self.log.info('CDN rewrite for %s', records[0]['name'])
_type = "CNAME"
if name == "":
_type = "ALIAS"
return {
'ttl': records[0]['ttl'],
'type': _type,
'value': '{}.cdn.cloudflare.net.'.format(records[0]['name']),
}
def _data_for_multiple(self, _type, records):
return {
'ttl': records[0]['ttl'],
@ -200,14 +227,29 @@ class CloudflareProvider(BaseProvider):
for name, types in values.items():
for _type, records in types.items():
# Cloudflare supports ALIAS semantics with root CNAMEs
if _type == 'CNAME' and name == '':
_type = 'ALIAS'
# rewrite Cloudflare proxied records
if self.cdn and records[0]['proxied']:
data = self._data_for_cdn(name, _type, records)
else:
# Cloudflare supports ALIAS semantics with root CNAMEs
if _type == 'CNAME' and name == '':
_type = 'ALIAS'
data_for = getattr(self, '_data_for_{}'.format(_type))
data = data_for(_type, records)
data_for = getattr(self, '_data_for_{}'.format(_type))
data = data_for(_type, records)
record = Record.new(zone, name, data, source=self,
lenient=lenient)
# only one rewrite is needed for names where the proxy is
# enabled at multiple records with a different type but
# the same name
if (self.cdn and records[0]['proxied'] and
record in zone._records[name]):
self.log.info('CDN rewrite %s already in zone', name)
continue
zone.add_record(record)
self.log.info('populate: found %s records',
@ -220,6 +262,13 @@ class CloudflareProvider(BaseProvider):
new['ttl'] = max(120, new['ttl'])
if new == existing:
return False
# If this is a record to enable Cloudflare CDN don't update as
# we don't know the original values.
if (change.record._type in ('ALIAS', 'CNAME') and
change.record.value.endswith('.cdn.cloudflare.net.')):
return False
return True
def _contents_for_multiple(self, record):


+ 202
- 1
tests/test_octodns_provider_cloudflare.py View File

@ -42,6 +42,20 @@ class TestCloudflareProvider(TestCase):
def test_populate(self):
provider = CloudflareProvider('test', 'email', 'token')
# Bad requests
with requests_mock() as mock:
mock.get(ANY, status_code=400,
text='{"success":false,"errors":[{"code":1101,'
'"message":"request was invalid"}],'
'"messages":[],"result":null}')
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('CloudflareError', type(ctx.exception).__name__)
self.assertEquals('request was invalid', ctx.exception.message)
# Bad auth
with requests_mock() as mock:
mock.get(ANY, status_code=403,
@ -52,6 +66,8 @@ class TestCloudflareProvider(TestCase):
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('CloudflareAuthenticationError',
type(ctx.exception).__name__)
self.assertEquals('Unknown X-Auth-Key or X-Auth-Email',
ctx.exception.message)
@ -62,7 +78,9 @@ class TestCloudflareProvider(TestCase):
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Authentication error', ctx.exception.message)
self.assertEquals('CloudflareAuthenticationError',
type(ctx.exception).__name__)
self.assertEquals('Cloudflare error', ctx.exception.message)
# General error
with requests_mock() as mock:
@ -485,3 +503,186 @@ class TestCloudflareProvider(TestCase):
'ttl': 300,
'type': 'CNAME'
}, list(contents)[0])
def test_cdn(self):
provider = CloudflareProvider('test', 'email', 'token', True)
# A CNAME for us to transform to ALIAS
provider.zone_records = Mock(return_value=[
{
"id": "fc12ab34cd5611334422ab3322997642",
"type": "CNAME",
"name": "cname.unit.tests",
"content": "www.unit.tests",
"proxiable": True,
"proxied": True,
"ttl": 300,
"locked": False,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"modified_on": "2017-03-11T18:01:43.420689Z",
"created_on": "2017-03-11T18:01:43.420689Z",
"meta": {
"auto_added": False
}
},
{
"id": "fc12ab34cd5611334422ab3322997642",
"type": "A",
"name": "a.unit.tests",
"content": "1.1.1.1",
"proxiable": True,
"proxied": True,
"ttl": 300,
"locked": False,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"modified_on": "2017-03-11T18:01:43.420689Z",
"created_on": "2017-03-11T18:01:43.420689Z",
"meta": {
"auto_added": False
}
},
{
"id": "fc12ab34cd5611334422ab3322997642",
"type": "A",
"name": "a.unit.tests",
"content": "1.1.1.2",
"proxiable": True,
"proxied": True,
"ttl": 300,
"locked": False,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"modified_on": "2017-03-11T18:01:43.420689Z",
"created_on": "2017-03-11T18:01:43.420689Z",
"meta": {
"auto_added": False
}
},
{
"id": "fc12ab34cd5611334422ab3322997642",
"type": "A",
"name": "multi.unit.tests",
"content": "1.1.1.3",
"proxiable": True,
"proxied": True,
"ttl": 300,
"locked": False,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"modified_on": "2017-03-11T18:01:43.420689Z",
"created_on": "2017-03-11T18:01:43.420689Z",
"meta": {
"auto_added": False
}
},
{
"id": "fc12ab34cd5611334422ab3322997642",
"type": "AAAA",
"name": "multi.unit.tests",
"content": "::1",
"proxiable": True,
"proxied": True,
"ttl": 300,
"locked": False,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"modified_on": "2017-03-11T18:01:43.420689Z",
"created_on": "2017-03-11T18:01:43.420689Z",
"meta": {
"auto_added": False
}
},
])
zone = Zone('unit.tests.', [])
provider.populate(zone)
# the two A records get merged into one CNAME record poining to the CDN
self.assertEquals(3, len(zone.records))
record = list(zone.records)[0]
self.assertEquals('multi', record.name)
self.assertEquals('multi.unit.tests.', record.fqdn)
self.assertEquals('CNAME', record._type)
self.assertEquals('multi.unit.tests.cdn.cloudflare.net.', record.value)
record = list(zone.records)[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)
self.assertEquals('CNAME', record._type)
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.
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, 'cname', {
'ttl': 300,
'type': 'CNAME',
'value': 'change.unit.tests.cdn.cloudflare.net.'
}))
wanted.add_record(Record.new(wanted, 'new', {
'ttl': 300,
'type': 'CNAME',
'value': 'new.unit.tests.cdn.cloudflare.net.'
}))
wanted.add_record(Record.new(wanted, 'created', {
'ttl': 300,
'type': 'CNAME',
'value': 'www.unit.tests.'
}))
plan = provider.plan(wanted)
self.assertEquals(1, len(plan.changes))
def test_cdn_alias(self):
provider = CloudflareProvider('test', 'email', 'token', True)
# A CNAME for us to transform to ALIAS
provider.zone_records = Mock(return_value=[
{
"id": "fc12ab34cd5611334422ab3322997642",
"type": "CNAME",
"name": "unit.tests",
"content": "www.unit.tests",
"proxiable": True,
"proxied": True,
"ttl": 300,
"locked": False,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"modified_on": "2017-03-11T18:01:43.420689Z",
"created_on": "2017-03-11T18:01:43.420689Z",
"meta": {
"auto_added": False
}
},
])
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(1, len(zone.records))
record = list(zone.records)[0]
self.assertEquals('', record.name)
self.assertEquals('unit.tests.', record.fqdn)
self.assertEquals('ALIAS', record._type)
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.
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, '', {
'ttl': 300,
'type': 'ALIAS',
'value': 'change.unit.tests.cdn.cloudflare.net.'
}))
plan = provider.plan(wanted)
self.assertEquals(False, hasattr(plan, 'changes'))

Loading…
Cancel
Save