# # # from __future__ import absolute_import, division, print_function, \ unicode_literals from suds import WebFault from collections import defaultdict from . import ProviderException from .base import BaseProvider from logging import getLogger from ..record import Record from transip.service.domain import DomainService from transip.service.objects import DnsEntry 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 is None and key_file is None: raise TransipConfigException( 'Missing `key` or `key_file` parameter in config' ) self.account = account self.key = key self.key_file = key_file self._client = None self._currentZone = {} @property def client(self): # This can't happen in __init__ b/c it makes network calls during the # construction of the object and that before the tests have had a # chance to install the mock client if self._client is None: if self.key_file is not None: self._client = DomainService(self.account, private_key_file=self.key_file) else: # we checked key in __init__ so can assume it's not None self._client = DomainService(self.account, private_key=self.key) return self._client def populate(self, zone, target=False, lenient=False): exists = False self._currentZone = zone self.log.debug('populate: name=%s, target=%s, lenient=%s', zone.name, target, lenient) before = len(zone.records) try: zoneInfo = self.client.get_info(zone.name[:-1]) except WebFault as e: if e.fault.faultcode == '102' and target is False: # Zone not found in account, and not a target so just # leave an empty zone. return exists elif e.fault.faultcode == '102' 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.fault.faultcode, zone.name)) else: self.log.error('populate: (%s) %s ', e.fault.faultcode, e.fault.faultstring) raise e self.log.debug('populate: found %s records for zone %s', len(zoneInfo.dnsEntries), zone.name) exists = True if zoneInfo.dnsEntries: values = defaultdict(lambda: defaultdict(list)) for record in zoneInfo.dnsEntries: 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(): data_for = getattr(self, '_data_for_{}'.format(_type)) record = Record.new(zone, name, data_for(_type, records), source=self, lenient=lenient) zone.add_record(record, lenient=lenient) self.log.info('populate: found %s records, exists = %s', len(zone.records) - before, exists) self._currentZone = {} return exists def _apply(self, plan): desired = plan.desired changes = plan.changes self.log.debug('apply: zone=%s, changes=%d', desired.name, len(changes)) self._currentZone = plan.desired try: self.client.get_info(plan.desired.name[:-1]) except WebFault as e: self.log.exception('_apply: get_info failed') raise e _dns_entries = [] for record in plan.desired.records: if record._type in self.SUPPORTS: entries_for = getattr(self, '_entries_for_{}'.format(record._type)) # Root records have '@' as name name = record.name if name == '': name = self.ROOT_RECORD _dns_entries.extend(entries_for(name, record)) try: self.client.set_dns_entries(plan.desired.name[:-1], _dns_entries) except WebFault as e: self.log.warning(('_apply: Set DNS returned ' + 'one or more errors: {}').format( e.fault.faultstring)) raise TransipException(200, e.fault.faultstring) self._currentZone = {} def _entries_for_multiple(self, name, record): _entries = [] for value in record.values: _entries.append(DnsEntry(name, record.ttl, record._type, value)) return _entries def _entries_for_single(self, name, record): return [DnsEntry(name, record.ttl, record._type, record.value)] _entries_for_A = _entries_for_multiple _entries_for_AAAA = _entries_for_multiple _entries_for_NS = _entries_for_multiple _entries_for_SPF = _entries_for_multiple _entries_for_CNAME = _entries_for_single def _entries_for_MX(self, name, record): _entries = [] for value in record.values: content = "{} {}".format(value.preference, value.exchange) _entries.append(DnsEntry(name, record.ttl, record._type, content)) return _entries def _entries_for_SRV(self, name, record): _entries = [] for value in record.values: content = "{} {} {} {}".format(value.priority, value.weight, value.port, value.target) _entries.append(DnsEntry(name, record.ttl, record._type, content)) return _entries def _entries_for_SSHFP(self, name, record): _entries = [] for value in record.values: content = "{} {} {}".format(value.algorithm, value.fingerprint_type, value.fingerprint) _entries.append(DnsEntry(name, record.ttl, record._type, content)) return _entries def _entries_for_CAA(self, name, record): _entries = [] for value in record.values: content = "{} {} {}".format(value.flags, value.tag, value.value) _entries.append(DnsEntry(name, record.ttl, record._type, content)) return _entries def _entries_for_TXT(self, name, record): _entries = [] for value in record.values: value = value.replace('\\;', ';') _entries.append(DnsEntry(name, record.ttl, record._type, value)) return _entries def _parse_to_fqdn(self, value): # Enforce switch from suds.sax.text.Text to string value = str(value) # TransIP allows '@' as value to alias the root record. # this provider won't set an '@' value, but can be an existing record if value == self.ROOT_RECORD: value = self._currentZone.name if value[-1] != '.': self.log.debug('parseToFQDN: changed %s to %s', value, '{}.{}'.format(value, self._currentZone.name)) value = '{}.{}'.format(value, self._currentZone.name) return value def _get_lowest_ttl(self, records): _ttl = 100000 for record in records: _ttl = min(_ttl, record['expire']) return _ttl def _data_for_multiple(self, _type, records): _values = [] for record in records: # Enforce switch from suds.sax.text.Text to string _values.append(str(record['content'])) return { 'ttl': self._get_lowest_ttl(records), 'type': _type, 'values': _values } _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): return { 'ttl': records[0]['expire'], 'type': _type, 'value': self._parse_to_fqdn(records[0]['content']) } def _data_for_MX(self, _type, records): _values = [] for record in records: preference, exchange = record['content'].split(" ", 1) _values.append({ 'preference': preference, 'exchange': self._parse_to_fqdn(exchange) }) return { 'ttl': self._get_lowest_ttl(records), 'type': _type, 'values': _values } def _data_for_SRV(self, _type, records): _values = [] for record in records: priority, weight, port, target = record['content'].split(' ', 3) _values.append({ 'port': port, 'priority': priority, 'target': self._parse_to_fqdn(target), 'weight': weight }) return { 'type': _type, 'ttl': self._get_lowest_ttl(records), 'values': _values } def _data_for_SSHFP(self, _type, records): _values = [] for record in records: algorithm, fp_type, fingerprint = record['content'].split(' ', 2) _values.append({ 'algorithm': algorithm, 'fingerprint': fingerprint.lower(), 'fingerprint_type': fp_type }) return { 'type': _type, 'ttl': self._get_lowest_ttl(records), 'values': _values } def _data_for_CAA(self, _type, records): _values = [] for record in records: flags, tag, value = record['content'].split(' ', 2) _values.append({ 'flags': flags, 'tag': tag, 'value': value }) return { 'type': _type, 'ttl': self._get_lowest_ttl(records), 'values': _values } def _data_for_TXT(self, _type, records): _values = [] for record in records: _values.append(record['content'].replace(';', '\\;')) return { 'type': _type, 'ttl': self._get_lowest_ttl(records), 'values': _values }