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
 | 
						|
    ]
 |