mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
Merge branch 'master' into dynamic-pool-validation
This commit is contained in:
@@ -177,7 +177,7 @@ The above command pulled the existing data out of Route53 and placed the results
|
||||
| Provider | Requirements | Record Support | Dynamic | Notes |
|
||||
|--|--|--|--|--|
|
||||
| [AzureProvider](/octodns/provider/azuredns.py) | azure-mgmt-dns | A, AAAA, CAA, CNAME, MX, NS, PTR, SRV, TXT | No | |
|
||||
| [Akamai](/octodns/provider/fastdns.py) | edgegrid-python | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TXT | No | |
|
||||
| [Akamai](/octodns/provider/edgedns.py) | edgegrid-python | A, AAAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, SSHFP, TXT | No | |
|
||||
| [CloudflareProvider](/octodns/provider/cloudflare.py) | | A, AAAA, ALIAS, CAA, CNAME, MX, NS, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [ConstellixProvider](/octodns/provider/constellix.py) | | A, AAAA, ALIAS (ANAME), CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | CAA tags restricted |
|
||||
| [DigitalOceanProvider](/octodns/provider/digitalocean.py) | | A, AAAA, CAA, CNAME, MX, NS, TXT, SRV | No | CAA tags restricted |
|
||||
|
525
octodns/provider/edgedns.py
Normal file
525
octodns/provider/edgedns.py
Normal file
@@ -0,0 +1,525 @@
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from requests import Session
|
||||
from akamai.edgegrid import EdgeGridAuth
|
||||
from six.moves.urllib.parse import urljoin
|
||||
from collections import defaultdict
|
||||
|
||||
from logging import getLogger
|
||||
from ..record import Record
|
||||
from .base import BaseProvider
|
||||
|
||||
|
||||
class AkamaiClientNotFound(Exception):
|
||||
|
||||
def __init__(self, resp):
|
||||
message = "404: Resource not found"
|
||||
super(AkamaiClientNotFound, self).__init__(message)
|
||||
|
||||
|
||||
class AkamaiClient(object):
|
||||
'''
|
||||
Client for making calls to Akamai Fast DNS API using Python Requests
|
||||
|
||||
Fast DNS Zone Management API V2, found here:
|
||||
developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html
|
||||
|
||||
Info on Python Requests library:
|
||||
https://2.python-requests.org/en/master/
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, client_secret, host, access_token, client_token):
|
||||
|
||||
self.base = "https://" + host + "/config-dns/v2/"
|
||||
|
||||
sess = Session()
|
||||
sess.auth = EdgeGridAuth(
|
||||
client_token=client_token,
|
||||
client_secret=client_secret,
|
||||
access_token=access_token
|
||||
)
|
||||
self._sess = sess
|
||||
|
||||
def _request(self, method, path, params=None, data=None, v1=False):
|
||||
|
||||
url = urljoin(self.base, path)
|
||||
resp = self._sess.request(method, url, params=params, json=data)
|
||||
|
||||
if resp.status_code == 404:
|
||||
raise AkamaiClientNotFound(resp)
|
||||
resp.raise_for_status()
|
||||
|
||||
return resp
|
||||
|
||||
def record_create(self, zone, name, record_type, content):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('POST', path, data=content)
|
||||
|
||||
return result
|
||||
|
||||
def record_delete(self, zone, name, record_type):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('DELETE', path)
|
||||
|
||||
return result
|
||||
|
||||
def record_replace(self, zone, name, record_type, content):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('PUT', path, data=content)
|
||||
|
||||
return result
|
||||
|
||||
def zone_get(self, zone):
|
||||
path = 'zones/{}'.format(zone)
|
||||
result = self._request('GET', path)
|
||||
|
||||
return result
|
||||
|
||||
def zone_create(self, contractId, params, gid=None):
|
||||
path = 'zones?contractId={}'.format(contractId)
|
||||
|
||||
if gid is not None:
|
||||
path += '&gid={}'.format(gid)
|
||||
|
||||
result = self._request('POST', path, data=params)
|
||||
|
||||
return result
|
||||
|
||||
def zone_recordset_get(self, zone, page=None, pageSize=None, search=None,
|
||||
showAll="true", sortBy="name", types=None):
|
||||
|
||||
params = {
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'search': search,
|
||||
'showAll': showAll,
|
||||
'sortBy': sortBy,
|
||||
'types': types
|
||||
}
|
||||
|
||||
path = 'zones/{}/recordsets'.format(zone)
|
||||
result = self._request('GET', path, params=params)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class AkamaiProvider(BaseProvider):
|
||||
|
||||
'''
|
||||
Akamai Fast DNS Provider
|
||||
|
||||
edgedns.py:
|
||||
|
||||
Example config file with variables:
|
||||
"
|
||||
---
|
||||
providers:
|
||||
config:
|
||||
class: octodns.provider.yaml.YamlProvider
|
||||
directory: ./config (example path to directory of zone files)
|
||||
edgedns:
|
||||
class: octodns.provider.edgedns.AkamaiProvider
|
||||
client_secret: env/AKAMAI_CLIENT_SECRET
|
||||
host: env/AKAMAI_HOST
|
||||
access_token: env/AKAMAI_ACCESS_TOKEN
|
||||
client_token: env/AKAMAI_CLIENT_TOKEN
|
||||
contract_id: env/AKAMAI_CONTRACT_ID (optional)
|
||||
|
||||
zones:
|
||||
example.com.:
|
||||
sources:
|
||||
- config
|
||||
targets:
|
||||
- edgedns
|
||||
"
|
||||
|
||||
The first four variables above can be hidden in environment variables
|
||||
and octoDNS will automatically search for them in the shell. It is
|
||||
possible to also hard-code into the config file: eg, contract_id.
|
||||
|
||||
The first four values can be found by generating credentials:
|
||||
https://control.akamai.com/
|
||||
Configure > Organization > Manage APIs > New API Client for me
|
||||
Select appropriate group, and fill relevant fields.
|
||||
For API Service Name, select DNS-Zone Record Management
|
||||
and then set appropriate Access level (Read-Write to make changes).
|
||||
Then select the "New Credential" button to generate values for above
|
||||
|
||||
The contract_id paramater is optional, and only required for creating
|
||||
a new zone. If the zone being managed already exists in Akamai for the
|
||||
user in question, then this paramater is not needed.
|
||||
|
||||
'''
|
||||
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
|
||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SPF',
|
||||
'SRV', 'SSHFP', 'TXT'))
|
||||
|
||||
def __init__(self, id, client_secret, host, access_token, client_token,
|
||||
contract_id=None, gid=None, *args, **kwargs):
|
||||
|
||||
self.log = getLogger('AkamaiProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, ')
|
||||
super(AkamaiProvider, self).__init__(id, *args, **kwargs)
|
||||
|
||||
self._dns_client = AkamaiClient(client_secret, host, access_token,
|
||||
client_token)
|
||||
|
||||
self._zone_records = {}
|
||||
self._contractId = contract_id
|
||||
self._gid = gid
|
||||
|
||||
def zone_records(self, zone):
|
||||
""" returns records for a zone, looks for it if not present, or
|
||||
returns empty [] if can't find a match
|
||||
"""
|
||||
if zone.name not in self._zone_records:
|
||||
try:
|
||||
name = zone.name[:-1]
|
||||
response = self._dns_client.zone_recordset_get(name)
|
||||
self._zone_records[zone.name] = response.json()["recordsets"]
|
||||
|
||||
except (AkamaiClientNotFound, KeyError):
|
||||
return []
|
||||
|
||||
return self._zone_records[zone.name]
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s', zone.name)
|
||||
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in self.zone_records(zone):
|
||||
|
||||
_type = record.get('type')
|
||||
# Akamai sends down prefix.zonename., while octodns expects prefix
|
||||
_name = record.get('name').split("." + zone.name[:-1], 1)[0]
|
||||
if _name == zone.name[:-1]:
|
||||
_name = '' # root / @
|
||||
|
||||
if _type not in self.SUPPORTS:
|
||||
continue
|
||||
values[_name][_type].append(record)
|
||||
|
||||
before = len(zone.records)
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||
record = Record.new(zone, name, data_for(_type, records[0]),
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record, lenient=lenient)
|
||||
|
||||
exists = zone.name in self._zone_records
|
||||
found = len(zone.records) - before
|
||||
self.log.info('populate: found %s records, exists=%s', found, exists)
|
||||
|
||||
return exists
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
changes = plan.changes
|
||||
self.log.debug('apply: zone=%s, chnges=%d', desired.name, len(changes))
|
||||
|
||||
zone_name = desired.name[:-1]
|
||||
try:
|
||||
self._dns_client.zone_get(zone_name)
|
||||
|
||||
except AkamaiClientNotFound:
|
||||
self.log.info("zone not found, creating zone")
|
||||
params = self._build_zone_config(zone_name)
|
||||
self._dns_client.zone_create(self._contractId, params, self._gid)
|
||||
|
||||
for change in changes:
|
||||
class_name = change.__class__.__name__
|
||||
getattr(self, '_apply_{}'.format(class_name))(change)
|
||||
|
||||
# Clear out the cache if any
|
||||
self._zone_records.pop(desired.name, None)
|
||||
|
||||
def _apply_Create(self, change):
|
||||
|
||||
new = change.new
|
||||
record_type = new._type
|
||||
|
||||
params_for = getattr(self, '_params_for_{}'.format(record_type))
|
||||
values = self._get_values(new.data)
|
||||
rdata = params_for(values)
|
||||
|
||||
zone = new.zone.name[:-1]
|
||||
name = self._set_full_name(new.name, zone)
|
||||
|
||||
content = {
|
||||
"name": name,
|
||||
"type": record_type,
|
||||
"ttl": new.ttl,
|
||||
"rdata": rdata
|
||||
}
|
||||
|
||||
self._dns_client.record_create(zone, name, record_type, content)
|
||||
|
||||
return
|
||||
|
||||
def _apply_Delete(self, change):
|
||||
|
||||
zone = change.existing.zone.name[:-1]
|
||||
name = self._set_full_name(change.existing.name, zone)
|
||||
record_type = change.existing._type
|
||||
|
||||
self._dns_client.record_delete(zone, name, record_type)
|
||||
|
||||
return
|
||||
|
||||
def _apply_Update(self, change):
|
||||
|
||||
new = change.new
|
||||
record_type = new._type
|
||||
|
||||
params_for = getattr(self, '_params_for_{}'.format(record_type))
|
||||
values = self._get_values(new.data)
|
||||
rdata = params_for(values)
|
||||
|
||||
zone = new.zone.name[:-1]
|
||||
name = self._set_full_name(new.name, zone)
|
||||
|
||||
content = {
|
||||
"name": name,
|
||||
"type": record_type,
|
||||
"ttl": new.ttl,
|
||||
"rdata": rdata
|
||||
}
|
||||
|
||||
self._dns_client.record_replace(zone, name, record_type, content)
|
||||
|
||||
return
|
||||
|
||||
def _data_for_multiple(self, _type, records):
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': [r for r in records['rdata']]
|
||||
}
|
||||
|
||||
_data_for_A = _data_for_multiple
|
||||
_data_for_AAAA = _data_for_multiple
|
||||
_data_for_NS = _data_for_multiple
|
||||
_data_for_SPF = _data_for_multiple
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
value = records['rdata'][0]
|
||||
if (value[-1] != '.'):
|
||||
value = '{}.'.format(value)
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'value': value
|
||||
}
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
preference, exchange = r.split(" ", 1)
|
||||
values.append({
|
||||
'preference': preference,
|
||||
'exchange': exchange
|
||||
})
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_NAPTR(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
order, preference, flags, service, regexp, repl = r.split(' ', 5)
|
||||
|
||||
values.append({
|
||||
'flags': flags[1:-1],
|
||||
'order': order,
|
||||
'preference': preference,
|
||||
'regexp': regexp[1:-1],
|
||||
'replacement': repl,
|
||||
'service': service[1:-1]
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_PTR(self, _type, records):
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'value': records['rdata'][0]
|
||||
}
|
||||
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
priority, weight, port, target = r.split(' ', 3)
|
||||
values.append({
|
||||
'port': port,
|
||||
'priority': priority,
|
||||
'target': target,
|
||||
'weight': weight
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_SSHFP(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
algorithm, fp_type, fingerprint = r.split(' ', 2)
|
||||
values.append({
|
||||
'algorithm': algorithm,
|
||||
'fingerprint': fingerprint.lower(),
|
||||
'fingerprint_type': fp_type
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_TXT(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
r = r[1:-1]
|
||||
values.append(r.replace(';', '\\;'))
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _params_for_multiple(self, values):
|
||||
return [r for r in values]
|
||||
|
||||
def _params_for_single(self, values):
|
||||
return values
|
||||
|
||||
_params_for_A = _params_for_multiple
|
||||
_params_for_AAAA = _params_for_multiple
|
||||
_params_for_NS = _params_for_multiple
|
||||
|
||||
_params_for_CNAME = _params_for_single
|
||||
_params_for_PTR = _params_for_single
|
||||
|
||||
def _params_for_MX(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
preference = r['preference']
|
||||
exchange = r['exchange']
|
||||
|
||||
record = '{} {}'.format(preference, exchange)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_NAPTR(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
ordr = r['order']
|
||||
prf = r['preference']
|
||||
flg = "\"" + r['flags'] + "\""
|
||||
srvc = "\"" + r['service'] + "\""
|
||||
rgx = "\"" + r['regexp'] + "\""
|
||||
rpl = r['replacement']
|
||||
|
||||
record = '{} {} {} {} {} {}'.format(ordr, prf, flg, srvc, rgx, rpl)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SPF(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
txt = "\"" + r.replace('\\;', ';') + "\""
|
||||
rdata.append(txt)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SRV(self, values):
|
||||
rdata = []
|
||||
for r in values:
|
||||
priority = r['priority']
|
||||
weight = r['weight']
|
||||
port = r['port']
|
||||
target = r['target']
|
||||
|
||||
record = '{} {} {} {}'.format(priority, weight, port, target)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SSHFP(self, values):
|
||||
rdata = []
|
||||
for r in values:
|
||||
algorithm = r['algorithm']
|
||||
fp_type = r['fingerprint_type']
|
||||
fp = r['fingerprint']
|
||||
|
||||
record = '{} {} {}'.format(algorithm, fp_type, fp)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_TXT(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
txt = "\"" + r.replace('\\;', ';') + "\""
|
||||
rdata.append(txt)
|
||||
|
||||
return rdata
|
||||
|
||||
def _build_zone_config(self, zone, _type="primary", comment=None,
|
||||
masters=[]):
|
||||
|
||||
if self._contractId is None:
|
||||
raise NameError("contractId not specified to create zone")
|
||||
|
||||
return {
|
||||
"zone": zone,
|
||||
"type": _type,
|
||||
"comment": comment,
|
||||
"masters": masters
|
||||
}
|
||||
|
||||
def _get_values(self, data):
|
||||
|
||||
try:
|
||||
vals = data['values']
|
||||
except KeyError:
|
||||
vals = [data['value']]
|
||||
|
||||
return vals
|
||||
|
||||
def _set_full_name(self, name, zone):
|
||||
name = name + '.' + zone
|
||||
|
||||
# octodns's name for root is ''
|
||||
if (name[0] == '.'):
|
||||
name = name[1:]
|
||||
|
||||
return name
|
@@ -5,521 +5,12 @@
|
||||
from __future__ import absolute_import, division, print_function, \
|
||||
unicode_literals
|
||||
|
||||
from requests import Session
|
||||
from akamai.edgegrid import EdgeGridAuth
|
||||
from six.moves.urllib.parse import urljoin
|
||||
from collections import defaultdict
|
||||
|
||||
from .edgedns import AkamaiProvider
|
||||
from logging import getLogger
|
||||
from ..record import Record
|
||||
from .base import BaseProvider
|
||||
|
||||
# Quell unused warning
|
||||
AkamaiProvider
|
||||
|
||||
class AkamaiClientNotFound(Exception):
|
||||
|
||||
def __init__(self, resp):
|
||||
message = "404: Resource not found"
|
||||
super(AkamaiClientNotFound, self).__init__(message)
|
||||
|
||||
|
||||
class AkamaiClient(object):
|
||||
'''
|
||||
Client for making calls to Akamai Fast DNS API using Python Requests
|
||||
|
||||
Fast DNS Zone Management API V2, found here:
|
||||
developer.akamai.com/api/web_performance/fast_dns_zone_management/v2.html
|
||||
|
||||
Info on Python Requests library:
|
||||
https://2.python-requests.org/en/master/
|
||||
|
||||
'''
|
||||
|
||||
def __init__(self, client_secret, host, access_token, client_token):
|
||||
|
||||
self.base = "https://" + host + "/config-dns/v2/"
|
||||
|
||||
sess = Session()
|
||||
sess.auth = EdgeGridAuth(
|
||||
client_token=client_token,
|
||||
client_secret=client_secret,
|
||||
access_token=access_token
|
||||
)
|
||||
self._sess = sess
|
||||
|
||||
def _request(self, method, path, params=None, data=None, v1=False):
|
||||
|
||||
url = urljoin(self.base, path)
|
||||
resp = self._sess.request(method, url, params=params, json=data)
|
||||
|
||||
if resp.status_code == 404:
|
||||
raise AkamaiClientNotFound(resp)
|
||||
resp.raise_for_status()
|
||||
|
||||
return resp
|
||||
|
||||
def record_create(self, zone, name, record_type, content):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('POST', path, data=content)
|
||||
|
||||
return result
|
||||
|
||||
def record_delete(self, zone, name, record_type):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('DELETE', path)
|
||||
|
||||
return result
|
||||
|
||||
def record_replace(self, zone, name, record_type, content):
|
||||
path = 'zones/{}/names/{}/types/{}'.format(zone, name, record_type)
|
||||
result = self._request('PUT', path, data=content)
|
||||
|
||||
return result
|
||||
|
||||
def zone_get(self, zone):
|
||||
path = 'zones/{}'.format(zone)
|
||||
result = self._request('GET', path)
|
||||
|
||||
return result
|
||||
|
||||
def zone_create(self, contractId, params, gid=None):
|
||||
path = 'zones?contractId={}'.format(contractId)
|
||||
|
||||
if gid is not None:
|
||||
path += '&gid={}'.format(gid)
|
||||
|
||||
result = self._request('POST', path, data=params)
|
||||
|
||||
return result
|
||||
|
||||
def zone_recordset_get(self, zone, page=None, pageSize=None, search=None,
|
||||
showAll="true", sortBy="name", types=None):
|
||||
|
||||
params = {
|
||||
'page': page,
|
||||
'pageSize': pageSize,
|
||||
'search': search,
|
||||
'showAll': showAll,
|
||||
'sortBy': sortBy,
|
||||
'types': types
|
||||
}
|
||||
|
||||
path = 'zones/{}/recordsets'.format(zone)
|
||||
result = self._request('GET', path, params=params)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class AkamaiProvider(BaseProvider):
|
||||
|
||||
'''
|
||||
Akamai Fast DNS Provider
|
||||
|
||||
fastdns.py:
|
||||
|
||||
Example config file with variables:
|
||||
"
|
||||
---
|
||||
providers:
|
||||
config:
|
||||
class: octodns.provider.yaml.YamlProvider
|
||||
directory: ./config (example path to directory of zone files)
|
||||
fastdns:
|
||||
class: octodns.provider.fastdns.AkamaiProvider
|
||||
client_secret: env/AKAMAI_CLIENT_SECRET
|
||||
host: env/AKAMAI_HOST
|
||||
access_token: env/AKAMAI_ACCESS_TOKEN
|
||||
client_token: env/AKAMAI_CLIENT_TOKEN
|
||||
contract_id: env/AKAMAI_CONTRACT_ID (optional)
|
||||
|
||||
zones:
|
||||
example.com.:
|
||||
sources:
|
||||
- config
|
||||
targets:
|
||||
- fastdns
|
||||
"
|
||||
|
||||
The first four variables above can be hidden in environment variables
|
||||
and octoDNS will automatically search for them in the shell. It is
|
||||
possible to also hard-code into the config file: eg, contract_id.
|
||||
|
||||
The first four values can be found by generating credentials:
|
||||
https://control.akamai.com/
|
||||
Configure > Organization > Manage APIs > New API Client for me
|
||||
Select appropriate group, and fill relevant fields.
|
||||
For API Service Name, select DNS-Zone Record Management
|
||||
and then set appropriate Access level (Read-Write to make changes).
|
||||
Then select the "New Credential" button to generate values for above
|
||||
|
||||
The contract_id paramater is optional, and only required for creating
|
||||
a new zone. If the zone being managed already exists in Akamai for the
|
||||
user in question, then this paramater is not needed.
|
||||
|
||||
'''
|
||||
|
||||
SUPPORTS_GEO = False
|
||||
SUPPORTS_DYNAMIC = False
|
||||
|
||||
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NAPTR', 'NS', 'PTR', 'SPF',
|
||||
'SRV', 'SSHFP', 'TXT'))
|
||||
|
||||
def __init__(self, id, client_secret, host, access_token, client_token,
|
||||
contract_id=None, gid=None, *args, **kwargs):
|
||||
|
||||
self.log = getLogger('AkamaiProvider[{}]'.format(id))
|
||||
self.log.debug('__init__: id=%s, ')
|
||||
super(AkamaiProvider, self).__init__(id, *args, **kwargs)
|
||||
|
||||
self._dns_client = AkamaiClient(client_secret, host, access_token,
|
||||
client_token)
|
||||
|
||||
self._zone_records = {}
|
||||
self._contractId = contract_id
|
||||
self._gid = gid
|
||||
|
||||
def zone_records(self, zone):
|
||||
""" returns records for a zone, looks for it if not present, or
|
||||
returns empty [] if can't find a match
|
||||
"""
|
||||
if zone.name not in self._zone_records:
|
||||
try:
|
||||
name = zone.name[:-1]
|
||||
response = self._dns_client.zone_recordset_get(name)
|
||||
self._zone_records[zone.name] = response.json()["recordsets"]
|
||||
|
||||
except (AkamaiClientNotFound, KeyError):
|
||||
return []
|
||||
|
||||
return self._zone_records[zone.name]
|
||||
|
||||
def populate(self, zone, target=False, lenient=False):
|
||||
self.log.debug('populate: name=%s', zone.name)
|
||||
|
||||
values = defaultdict(lambda: defaultdict(list))
|
||||
for record in self.zone_records(zone):
|
||||
|
||||
_type = record.get('type')
|
||||
# Akamai sends down prefix.zonename., while octodns expects prefix
|
||||
_name = record.get('name').split("." + zone.name[:-1], 1)[0]
|
||||
if _name == zone.name[:-1]:
|
||||
_name = '' # root / @
|
||||
|
||||
if _type not in self.SUPPORTS:
|
||||
continue
|
||||
values[_name][_type].append(record)
|
||||
|
||||
before = len(zone.records)
|
||||
for name, types in values.items():
|
||||
for _type, records in types.items():
|
||||
data_for = getattr(self, '_data_for_{}'.format(_type))
|
||||
record = Record.new(zone, name, data_for(_type, records[0]),
|
||||
source=self, lenient=lenient)
|
||||
zone.add_record(record, lenient=lenient)
|
||||
|
||||
exists = zone.name in self._zone_records
|
||||
found = len(zone.records) - before
|
||||
self.log.info('populate: found %s records, exists=%s', found, exists)
|
||||
|
||||
return exists
|
||||
|
||||
def _apply(self, plan):
|
||||
desired = plan.desired
|
||||
changes = plan.changes
|
||||
self.log.debug('apply: zone=%s, chnges=%d', desired.name, len(changes))
|
||||
|
||||
zone_name = desired.name[:-1]
|
||||
try:
|
||||
self._dns_client.zone_get(zone_name)
|
||||
|
||||
except AkamaiClientNotFound:
|
||||
self.log.info("zone not found, creating zone")
|
||||
params = self._build_zone_config(zone_name)
|
||||
self._dns_client.zone_create(self._contractId, params, self._gid)
|
||||
|
||||
for change in changes:
|
||||
class_name = change.__class__.__name__
|
||||
getattr(self, '_apply_{}'.format(class_name))(change)
|
||||
|
||||
# Clear out the cache if any
|
||||
self._zone_records.pop(desired.name, None)
|
||||
|
||||
def _apply_Create(self, change):
|
||||
|
||||
new = change.new
|
||||
record_type = new._type
|
||||
|
||||
params_for = getattr(self, '_params_for_{}'.format(record_type))
|
||||
values = self._get_values(new.data)
|
||||
rdata = params_for(values)
|
||||
|
||||
zone = new.zone.name[:-1]
|
||||
name = self._set_full_name(new.name, zone)
|
||||
|
||||
content = {
|
||||
"name": name,
|
||||
"type": record_type,
|
||||
"ttl": new.ttl,
|
||||
"rdata": rdata
|
||||
}
|
||||
|
||||
self._dns_client.record_create(zone, name, record_type, content)
|
||||
|
||||
return
|
||||
|
||||
def _apply_Delete(self, change):
|
||||
|
||||
zone = change.existing.zone.name[:-1]
|
||||
name = self._set_full_name(change.existing.name, zone)
|
||||
record_type = change.existing._type
|
||||
|
||||
self._dns_client.record_delete(zone, name, record_type)
|
||||
|
||||
return
|
||||
|
||||
def _apply_Update(self, change):
|
||||
|
||||
new = change.new
|
||||
record_type = new._type
|
||||
|
||||
params_for = getattr(self, '_params_for_{}'.format(record_type))
|
||||
values = self._get_values(new.data)
|
||||
rdata = params_for(values)
|
||||
|
||||
zone = new.zone.name[:-1]
|
||||
name = self._set_full_name(new.name, zone)
|
||||
|
||||
content = {
|
||||
"name": name,
|
||||
"type": record_type,
|
||||
"ttl": new.ttl,
|
||||
"rdata": rdata
|
||||
}
|
||||
|
||||
self._dns_client.record_replace(zone, name, record_type, content)
|
||||
|
||||
return
|
||||
|
||||
def _data_for_multiple(self, _type, records):
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': [r for r in records['rdata']]
|
||||
}
|
||||
|
||||
_data_for_A = _data_for_multiple
|
||||
_data_for_AAAA = _data_for_multiple
|
||||
_data_for_NS = _data_for_multiple
|
||||
_data_for_SPF = _data_for_multiple
|
||||
|
||||
def _data_for_CNAME(self, _type, records):
|
||||
value = records['rdata'][0]
|
||||
if (value[-1] != '.'):
|
||||
value = '{}.'.format(value)
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'value': value
|
||||
}
|
||||
|
||||
def _data_for_MX(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
preference, exchange = r.split(" ", 1)
|
||||
values.append({
|
||||
'preference': preference,
|
||||
'exchange': exchange
|
||||
})
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_NAPTR(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
order, preference, flags, service, regexp, repl = r.split(' ', 5)
|
||||
|
||||
values.append({
|
||||
'flags': flags[1:-1],
|
||||
'order': order,
|
||||
'preference': preference,
|
||||
'regexp': regexp[1:-1],
|
||||
'replacement': repl,
|
||||
'service': service[1:-1]
|
||||
})
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_PTR(self, _type, records):
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'value': records['rdata'][0]
|
||||
}
|
||||
|
||||
def _data_for_SRV(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
priority, weight, port, target = r.split(' ', 3)
|
||||
values.append({
|
||||
'port': port,
|
||||
'priority': priority,
|
||||
'target': target,
|
||||
'weight': weight
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_SSHFP(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
algorithm, fp_type, fingerprint = r.split(' ', 2)
|
||||
values.append({
|
||||
'algorithm': algorithm,
|
||||
'fingerprint': fingerprint.lower(),
|
||||
'fingerprint_type': fp_type
|
||||
})
|
||||
|
||||
return {
|
||||
'type': _type,
|
||||
'ttl': records['ttl'],
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _data_for_TXT(self, _type, records):
|
||||
values = []
|
||||
for r in records['rdata']:
|
||||
r = r[1:-1]
|
||||
values.append(r.replace(';', '\\;'))
|
||||
|
||||
return {
|
||||
'ttl': records['ttl'],
|
||||
'type': _type,
|
||||
'values': values
|
||||
}
|
||||
|
||||
def _params_for_multiple(self, values):
|
||||
return [r for r in values]
|
||||
|
||||
def _params_for_single(self, values):
|
||||
return values
|
||||
|
||||
_params_for_A = _params_for_multiple
|
||||
_params_for_AAAA = _params_for_multiple
|
||||
_params_for_NS = _params_for_multiple
|
||||
|
||||
_params_for_CNAME = _params_for_single
|
||||
_params_for_PTR = _params_for_single
|
||||
|
||||
def _params_for_MX(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
preference = r['preference']
|
||||
exchange = r['exchange']
|
||||
|
||||
record = '{} {}'.format(preference, exchange)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_NAPTR(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
ordr = r['order']
|
||||
prf = r['preference']
|
||||
flg = "\"" + r['flags'] + "\""
|
||||
srvc = "\"" + r['service'] + "\""
|
||||
rgx = "\"" + r['regexp'] + "\""
|
||||
rpl = r['replacement']
|
||||
|
||||
record = '{} {} {} {} {} {}'.format(ordr, prf, flg, srvc, rgx, rpl)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SPF(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
txt = "\"" + r.replace('\\;', ';') + "\""
|
||||
rdata.append(txt)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SRV(self, values):
|
||||
rdata = []
|
||||
for r in values:
|
||||
priority = r['priority']
|
||||
weight = r['weight']
|
||||
port = r['port']
|
||||
target = r['target']
|
||||
|
||||
record = '{} {} {} {}'.format(priority, weight, port, target)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_SSHFP(self, values):
|
||||
rdata = []
|
||||
for r in values:
|
||||
algorithm = r['algorithm']
|
||||
fp_type = r['fingerprint_type']
|
||||
fp = r['fingerprint']
|
||||
|
||||
record = '{} {} {}'.format(algorithm, fp_type, fp)
|
||||
rdata.append(record)
|
||||
|
||||
return rdata
|
||||
|
||||
def _params_for_TXT(self, values):
|
||||
rdata = []
|
||||
|
||||
for r in values:
|
||||
txt = "\"" + r.replace('\\;', ';') + "\""
|
||||
rdata.append(txt)
|
||||
|
||||
return rdata
|
||||
|
||||
def _build_zone_config(self, zone, _type="primary", comment=None,
|
||||
masters=[]):
|
||||
|
||||
if self._contractId is None:
|
||||
raise NameError("contractId not specified to create zone")
|
||||
|
||||
return {
|
||||
"zone": zone,
|
||||
"type": _type,
|
||||
"comment": comment,
|
||||
"masters": masters
|
||||
}
|
||||
|
||||
def _get_values(self, data):
|
||||
|
||||
try:
|
||||
vals = data['values']
|
||||
except KeyError:
|
||||
vals = [data['value']]
|
||||
|
||||
return vals
|
||||
|
||||
def _set_full_name(self, name, zone):
|
||||
name = name + '.' + zone
|
||||
|
||||
# octodns's name for root is ''
|
||||
if (name[0] == '.'):
|
||||
name = name[1:]
|
||||
|
||||
return name
|
||||
log = getLogger('octodns.provider.fastdns.AkamaiProvider')
|
||||
log.warn('DEPRECATION NOTICE: AkamaiProvider has been moved to '
|
||||
'octodns.provider.fastdns.AkamaiProvider')
|
||||
|
@@ -13,12 +13,13 @@ from six import text_type
|
||||
from unittest import TestCase
|
||||
|
||||
from octodns.record import Record
|
||||
from octodns.provider.fastdns import AkamaiProvider
|
||||
from octodns.provider.edgedns import AkamaiProvider
|
||||
from octodns.provider.fastdns import AkamaiProvider as LegacyAkamaiProvider
|
||||
from octodns.provider.yaml import YamlProvider
|
||||
from octodns.zone import Zone
|
||||
|
||||
|
||||
class TestFastdnsProvider(TestCase):
|
||||
class TestEdgeDnsProvider(TestCase):
|
||||
expected = Zone('unit.tests.', [])
|
||||
source = YamlProvider('test', join(dirname(__file__), 'config'))
|
||||
source.populate(expected)
|
||||
@@ -71,7 +72,7 @@ class TestFastdnsProvider(TestCase):
|
||||
# No diffs == no changes
|
||||
with requests_mock() as mock:
|
||||
|
||||
with open('tests/fixtures/fastdns-records.json') as fh:
|
||||
with open('tests/fixtures/edgedns-records.json') as fh:
|
||||
mock.get(ANY, text=fh.read())
|
||||
|
||||
zone = Zone('unit.tests.', [])
|
||||
@@ -95,7 +96,7 @@ class TestFastdnsProvider(TestCase):
|
||||
# tests create update delete through previous state config json
|
||||
with requests_mock() as mock:
|
||||
|
||||
with open('tests/fixtures/fastdns-records-prev.json') as fh:
|
||||
with open('tests/fixtures/edgedns-records-prev.json') as fh:
|
||||
mock.get(ANY, text=fh.read())
|
||||
|
||||
plan = provider.plan(self.expected)
|
||||
@@ -108,7 +109,7 @@ class TestFastdnsProvider(TestCase):
|
||||
|
||||
# Test against a zone that doesn't exist yet
|
||||
with requests_mock() as mock:
|
||||
with open('tests/fixtures/fastdns-records-prev-other.json') as fh:
|
||||
with open('tests/fixtures/edgedns-records-prev-other.json') as fh:
|
||||
mock.get(ANY, status_code=404)
|
||||
|
||||
plan = provider.plan(self.expected)
|
||||
@@ -121,7 +122,7 @@ class TestFastdnsProvider(TestCase):
|
||||
|
||||
# Test against a zone that doesn't exist yet, but gid not provided
|
||||
with requests_mock() as mock:
|
||||
with open('tests/fixtures/fastdns-records-prev-other.json') as fh:
|
||||
with open('tests/fixtures/edgedns-records-prev-other.json') as fh:
|
||||
mock.get(ANY, status_code=404)
|
||||
provider = AkamaiProvider("test", "s", "akam.com", "atok", "ctok",
|
||||
"cid")
|
||||
@@ -149,3 +150,9 @@ class TestFastdnsProvider(TestCase):
|
||||
except NameError as e:
|
||||
expected = "contractId not specified to create zone"
|
||||
self.assertEquals(text_type(e), expected)
|
||||
|
||||
|
||||
class TestDeprecatedAkamaiProvider(TestCase):
|
||||
|
||||
def test_equivilent(self):
|
||||
self.assertEquals(LegacyAkamaiProvider, AkamaiProvider)
|
Reference in New Issue
Block a user