1
0
mirror of https://github.com/github/octodns.git synced 2024-05-11 05:55:00 +00:00
Files
github-octodns/octodns/provider/edgedns.py
2020-04-10 13:26:41 -04:00

526 lines
15 KiB
Python

#
#
#
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
Edge DNS Zone Management API V2, found here:
https://developer.akamai.com/api/cloud_security/edge_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 Edge 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