mirror of
https://github.com/github/octodns.git
synced 2024-05-11 05:55:00 +00:00
259 lines
8.3 KiB
Python
259 lines
8.3 KiB
Python
from __future__ import (absolute_import, division, print_function,
|
|
unicode_literals)
|
|
|
|
from collections import defaultdict, namedtuple
|
|
from logging import getLogger
|
|
|
|
from transip import TransIP
|
|
from transip.exceptions import TransIPHTTPError
|
|
from transip.v6.objects import DnsEntry
|
|
|
|
from . import ProviderException
|
|
from ..record import Record
|
|
from .base import BaseProvider
|
|
|
|
DNSEntry = namedtuple('DNSEntry', ('name', 'expire', 'type', 'content'))
|
|
|
|
|
|
class TransipException(ProviderException):
|
|
pass
|
|
|
|
|
|
class TransipConfigException(TransipException):
|
|
pass
|
|
|
|
|
|
class TransipNewZoneException(TransipException):
|
|
pass
|
|
|
|
|
|
class TransipProvider(BaseProvider):
|
|
'''
|
|
Transip DNS provider
|
|
|
|
transip:
|
|
class: octodns.provider.transip.TransipProvider
|
|
# Your Transip account name (required)
|
|
account: yourname
|
|
# Path to a private key file (required if key is not used)
|
|
key_file: /path/to/file
|
|
# The api key as string (required if key_file is not used)
|
|
key: |
|
|
\'''
|
|
-----BEGIN PRIVATE KEY-----
|
|
...
|
|
-----END PRIVATE KEY-----
|
|
\'''
|
|
# if both `key_file` and `key` are presented `key_file` is used
|
|
|
|
'''
|
|
|
|
SUPPORTS_GEO = False
|
|
SUPPORTS_DYNAMIC = False
|
|
SUPPORTS = set(('A', 'AAAA', 'CNAME', 'MX', 'NS', 'SRV', 'SPF', 'TXT',
|
|
'SSHFP', 'CAA'))
|
|
# unsupported by OctoDNS: 'TLSA'
|
|
MIN_TTL = 120
|
|
TIMEOUT = 15
|
|
ROOT_RECORD = '@'
|
|
|
|
def __init__(self, id, account, key=None, key_file=None, *args, **kwargs):
|
|
self.log = getLogger('TransipProvider[{}]'.format(id))
|
|
self.log.debug('__init__: id=%s, account=%s, token=***', id,
|
|
account)
|
|
super(TransipProvider, self).__init__(id, *args, **kwargs)
|
|
|
|
if key_file is not None:
|
|
self._client = TransIP(login=account, private_key_file=key_file)
|
|
elif key is not None:
|
|
self._client = TransIP(login=account, private_key=key)
|
|
else:
|
|
raise TransipConfigException(
|
|
'Missing `key` or `key_file` parameter in config'
|
|
)
|
|
|
|
def populate(self, zone, target=False, lenient=False):
|
|
'''
|
|
Populate the zone with records in-place.
|
|
'''
|
|
self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name,
|
|
target, lenient)
|
|
|
|
before = len(zone.records)
|
|
|
|
try:
|
|
domain = self._client.domains.get(zone.name.strip('.'))
|
|
records = domain.dns.list()
|
|
except TransIPHTTPError as e:
|
|
if e.response_code == 404 and target is False:
|
|
# Zone not found in account, and not a target so just
|
|
# leave an empty zone.
|
|
return False
|
|
elif e.response_code == 404 and target is True:
|
|
self.log.warning('populate: Transip can\'t create new zones')
|
|
raise TransipNewZoneException(
|
|
('populate: ({}) Transip used ' +
|
|
'as target for non-existing zone: {}').format(
|
|
e.response_code, zone.name))
|
|
else:
|
|
self.log.error(
|
|
'populate: (%s) %s ', e.response_code, e.message
|
|
)
|
|
raise TransipException(
|
|
'Unhandled error: ({}) {}'.format(
|
|
e.response_code, e.message
|
|
)
|
|
)
|
|
|
|
self.log.debug(
|
|
'populate: found %s records for zone %s', len(records), zone.name
|
|
)
|
|
if records:
|
|
values = defaultdict(lambda: defaultdict(list))
|
|
for record in records:
|
|
name = zone.hostname_from_fqdn(record.name)
|
|
if name == self.ROOT_RECORD:
|
|
name = ''
|
|
|
|
if record.type in self.SUPPORTS:
|
|
values[name][record.type].append(record)
|
|
|
|
for name, types in values.items():
|
|
for _type, records in types.items():
|
|
record = Record.new(
|
|
zone,
|
|
name,
|
|
_data_for(_type, records, zone),
|
|
source=self,
|
|
lenient=lenient,
|
|
)
|
|
zone.add_record(record, lenient=lenient)
|
|
self.log.info('populate: found %s records',
|
|
len(zone.records) - before)
|
|
|
|
return True
|
|
|
|
def _apply(self, plan):
|
|
desired = plan.desired
|
|
changes = plan.changes
|
|
self.log.debug('apply: zone=%s, changes=%d', desired.name,
|
|
len(changes))
|
|
|
|
try:
|
|
domain = self._client.domains.get(plan.desired.name[:-1])
|
|
except TransIPHTTPError as e:
|
|
self.log.exception('_apply: getting the domain failed')
|
|
raise TransipException(
|
|
'Unhandled error: ({}) {}'.format(e.response_code, e.message)
|
|
)
|
|
|
|
records = []
|
|
for record in plan.desired.records:
|
|
if record._type in self.SUPPORTS:
|
|
# Root records have '@' as name
|
|
name = record.name
|
|
if name == '':
|
|
name = self.ROOT_RECORD
|
|
|
|
records.extend(_entries_for(name, record))
|
|
|
|
# Transform DNSEntry namedtuples into transip.v6.objects.DnsEntry
|
|
# objects, which is a bit ugly because it's quite a magical object.
|
|
api_records = [DnsEntry(domain.dns, r._asdict()) for r in records]
|
|
try:
|
|
domain.dns.replace(api_records)
|
|
except TransIPHTTPError as e:
|
|
self.log.warning(
|
|
'_apply: Set DNS returned one or more errors: {}'.format(e)
|
|
)
|
|
raise TransipException(
|
|
'Unhandled error: ({}) {}'.format(e.response_code, e.message)
|
|
)
|
|
|
|
|
|
def _data_for(type_, records, current_zone):
|
|
if type_ == 'CNAME':
|
|
return {
|
|
'type': type_,
|
|
'ttl': records[0].expire,
|
|
'value': _parse_to_fqdn(records[0].content, current_zone),
|
|
}
|
|
|
|
def format_mx(record):
|
|
preference, exchange = record.content.split(' ', 1)
|
|
return {
|
|
'preference': preference,
|
|
'exchange': _parse_to_fqdn(exchange, current_zone),
|
|
}
|
|
|
|
def format_srv(record):
|
|
priority, weight, port, target = record.content.split(' ', 3)
|
|
return {
|
|
'port': port,
|
|
'priority': priority,
|
|
'target': _parse_to_fqdn(target, current_zone),
|
|
'weight': weight,
|
|
}
|
|
|
|
def format_sshfp(record):
|
|
algorithm, fp_type, fingerprint = record.content.split(' ', 2)
|
|
return {
|
|
'algorithm': algorithm,
|
|
'fingerprint': fingerprint.lower(),
|
|
'fingerprint_type': fp_type,
|
|
}
|
|
|
|
def format_caa(record):
|
|
flags, tag, value = record.content.split(' ', 2)
|
|
return {'flags': flags, 'tag': tag, 'value': value}
|
|
|
|
def format_txt(record):
|
|
return record.content.replace(';', '\\;')
|
|
|
|
value_formatter = {
|
|
'MX': format_mx,
|
|
'SRV': format_srv,
|
|
'SSHFP': format_sshfp,
|
|
'CAA': format_caa,
|
|
'TXT': format_txt,
|
|
}.get(type_, lambda r: r.content)
|
|
|
|
return {
|
|
'type': type_,
|
|
'ttl': _get_lowest_ttl(records),
|
|
'values': [value_formatter(r) for r in records],
|
|
}
|
|
|
|
|
|
def _parse_to_fqdn(value, current_zone):
|
|
# TransIP allows '@' as value to alias the root record.
|
|
# this provider won't set an '@' value, but can be an existing record
|
|
if value == TransipProvider.ROOT_RECORD:
|
|
value = current_zone.name
|
|
|
|
if value[-1] != '.':
|
|
value = '{}.{}'.format(value, current_zone.name)
|
|
|
|
return value
|
|
|
|
|
|
def _get_lowest_ttl(records):
|
|
return min([r.expire for r in records] + [100000])
|
|
|
|
|
|
def _entries_for(name, record):
|
|
values = record.values if hasattr(record, 'values') else [record.value]
|
|
formatter = {
|
|
'MX': lambda v: f'{v.preference} {v.exchange}',
|
|
'SRV': lambda v: f'{v.priority} {v.weight} {v.port} {v.target}',
|
|
'SSHFP': lambda v: (
|
|
f'{v.algorithm} {v.fingerprint_type} {v.fingerprint}'
|
|
),
|
|
'CAA': lambda v: f'{v.flags} {v.tag} {v.value}',
|
|
'TXT': lambda v: v.replace('\\;', ';'),
|
|
}.get(record._type, lambda r: r)
|
|
return [
|
|
DNSEntry(name, record.ttl, record._type, formatter(value))
|
|
for value in values
|
|
]
|