mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge pull request #182 from vanbroup/cloudflare-proxied
Option to handle Cloudflare proxied records
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user