class _TransipClient(object):
    """Encapsulates all communication with the Transip API."""
    def __init__(self, username, key_file):
        self.logger = logger.getChild(self.__class__.__name__)
        self.domain_service = DomainService(login=username,
                                            private_key_file=key_file)

    def add_txt_record(self, domain_name, record_name, record_content):
        """
        Add a TXT record using the supplied information.

        :param str domain_name: The domain to use to associate the record with.
        :param str record_name: The record name (typically beginning with '_acme-challenge.').
        :param str record_content: The record content (typically the challenge validation).
        :raises certbot.errors.PluginError: if an error occurs communicating with the Transip
                                            API
        """
        try:
            domain = self._find_domain(domain_name)
        except suds.WebFault as e:
            self.logger.error('Error finding domain using the Transip API: %s',
                              e)
            raise errors.PluginError(
                'Error finding domain using the Transip API: {0}'.format(e))

        try:
            domain_records = self.domain_service.get_info(
                domain_name=domain).dnsEntries
        except suds.WebFault as e:
            self.logger.error(
                'Error getting DNS records using the Transip API: %s', e)
            return

        try:
            new_record = DnsEntry(
                name=self._compute_record_name(domain, record_name),
                record_type='TXT',
                content=record_content,
                expire=1,
            )
        except suds.WebFault as e:
            self.logger.error(
                'Error getting DNS records using the Transip API: %s', e)
            return

        domain_records.append(new_record)

        try:
            self.domain_service.set_dns_entries(domain_name=domain,
                                                dns_entries=domain_records)
            self.logger.info('Successfully added TXT record')
        except suds.WebFault as e:
            self.logger.error(
                'Error adding TXT record using the Transip API: %s', e)
            raise errors.PluginError(
                'Error adding TXT record using the Transip API: {0}'.format(e))

    def del_txt_record(self, domain_name, record_name, record_content):
        """
        Delete a TXT record using the supplied information.

        Note that both the record's name and content are used to ensure that similar records
        created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted.

        Failures are logged, but not raised.

        :param str domain_name: The domain to use to associate the record with.
        :param str record_name: The record name (typically beginning with '_acme-challenge.').
        :param str record_content: The record content (typically the challenge validation).
        """
        try:
            domain = self._find_domain(domain_name)
        except suds.WebFault as e:
            self.logger.error('Error finding domain using the Transip API: %s',
                              e)
            return

        try:
            domain_records = self.domain_service.get_info(
                domain_name=domain).dnsEntries

            matching_records = [
                record for record in domain_records if record.type == 'TXT'
                and record.name == self._compute_record_name(
                    domain, record_name) and record.content == record_content
            ]
        except suds.WebFault as e:
            self.logger.error(
                'Error getting DNS records using the Transip API: %s', e)
            return

        for record in matching_records:
            try:
                self.logger.info('Removing TXT record with name: %s',
                                 record.name)
                del domain_records[domain_records.index(record)]
            except suds.WebFault as e:
                pass
                self.logger.warn(
                    'Error deleting TXT record %s using the Transip API: %s',
                    record.name, e)
        try:
            self.domain_service.set_dns_entries(domain_name=domain,
                                                dns_entries=domain_records)
        except suds.WebFault as e:
            self.logger.error('Error while storing DNS records: %s', e)

    def _find_domain(self, domain_name):
        """
        Find the domain object for a given domain name.

        :param str domain_name: The domain name for which to find the corresponding Domain.
        :returns: The Domain, if found.
        :rtype: `str`
        :raises certbot.errors.PluginError: if no matching Domain is found.
        """
        domain_name_guesses = dns_common.base_domain_name_guesses(domain_name)

        domains = self.domain_service.get_domain_names()

        for guess in domain_name_guesses:
            if guess in domains:
                self.logger.debug('Found base domain for %s using name %s',
                                  domain_name, guess)
                return guess

        raise errors.PluginError(
            'Unable to determine base domain for {0} using names: {1}.'
            # .format(domain_name, domain_name_guesses)
        )

    @staticmethod
    def _compute_record_name(domain, full_record_name):
        # The domain, from Transip's point of view, is automatically appended.
        return full_record_name.rpartition("." + domain)[0]
예제 #2
0
class Provider(BaseProvider):

    """
    provider_options can be overwritten by a Provider to setup custom defaults.
    They will be overwritten by any options set via the CLI or Env.
    order is:

    """
    def provider_options(self):
        return {'ttl': 86400}

    def __init__(self, options, engine_overrides=None):
        super(Provider, self).__init__(options, engine_overrides)
        self.provider_name = 'transip'
        self.domain_id = None

        username = self.options.get('auth_username')
        key_file = self.options.get('auth_api_key')

        if not username or not key_file:
            raise Exception("No username and/or keyfile was specified")

        self.client = DomainService(
            login=username,
            private_key_file=key_file
        )

    # Authenticate against provider,
    # Make any requests required to get the domain's id for this provider, so it can be used in subsequent calls.
    # Should throw an error if authentication fails for any reason, of if the domain does not exist.
    def authenticate(self):
        ## This request will fail when the domain does not exist,
        ## allowing us to check for existence
        domain = self.options.get('domain')
        try:
            self.client.get_info(domain)
        except:
            raise
            raise Exception("Could not retrieve information about {0}, "
                                "is this domain yours?".format(domain))
        self.domain_id = domain

    # Create record. If record already exists with the same content, do nothing'
    def create_record(self, type, name, content):
        records = self.client.get_info(self.options.get('domain')).dnsEntries

        if self._filter_records(records, type, name, content):
            # Nothing to do, record already exists
            logger.debug('create_record: already exists')
            return True

        records.append(DnsEntry(**{
            "name": self._relative_name(name),
            "record_type": type,
            "content": self._bind_format_target(type, content),
            "expire": self.options.get('ttl')
        }))

        self.client.set_dns_entries(self.options.get('domain'), records)
        status = len(self.list_records(type, name, content, show_output=False)) >= 1
        logger.debug('create_record: %s', status)
        return status

    # List all records. Return an empty list if no records found
    # type, name and content are used to filter records.
    # If possible filter during the query, otherwise filter after response is received.
    def list_records(self, type=None, name=None, content=None, show_output=True):
        all_records = self._convert_records(self.client.get_info(self.options.get('domain')).dnsEntries)
        records = self._filter_records(
            records=all_records,
            type=type,
            name=name,
            content=content
        )

        if show_output:
            logger.debug('list_records: %s', records)
        return records

    # Update a record. Identifier must be specified.
    def update_record(self, identifier=None, type=None, name=None, content=None):
        if not (type or name or content):
            raise Exception("At least one of type, name or content must be specified.")

        all_records = self.list_records(show_output=False)
        filtered_records = self._filter_records(all_records, type, name)

        for record in filtered_records:
            all_records.remove(record)
        all_records.append({
            "name": name,
            "type": type,
            "content": self._bind_format_target(type, content),
            "ttl": self.options.get('ttl')
        })

        self.client.set_dns_entries(self.options.get('domain'), self._convert_records_back(all_records))
        status = len(self.list_records(type, name, content, show_output=False)) >= 1
        logger.debug('update_record: %s', status)
        return status

    # Delete an existing record.
    # If record does not exist, do nothing.
    # If an identifier is specified, use it, otherwise do a lookup using type, name and content.
    def delete_record(self, identifier=None, type=None, name=None, content=None):
        if not (type or name or content):
            raise Exception("At least one of type, name or content must be specified.")

        all_records = self.list_records(show_output=False)
        filtered_records = self._filter_records(all_records, type, name, content)

        for record in filtered_records:
            all_records.remove(record)

        self.client.set_dns_entries(self.options.get('domain'), self._convert_records_back(all_records))
        status = len(self.list_records(type, name, content, show_output=False)) == 0
        logger.debug('delete_record: %s', status)
        return status

    def _full_name(self, record_name):
        if record_name == "@":
            record_name = self.options['domain']
        return super(Provider, self)._full_name(record_name)

    def _relative_name(self, record_name):
        name = super(Provider, self)._relative_name(record_name)
        if not name:
            name = "@"
        return name

    def _bind_format_target(self, type, target):
        if type == "CNAME" and not target.endswith("."):
            target += "."
        return target

    # Convert the objects from transip to dicts, for easier processing
    def _convert_records(self, records):
        _records = []
        for record in records:
            _records.append({
                "id": "{0}-{1}".format(self._full_name(record.name), record.type),
                "name": self._full_name(record.name),
                "type": record.type,
                "content": record.content,
                "ttl": record.expire
            })
        return _records

    def _to_dns_entry(self, _entry):
        return DnsEntry(self._relative_name(_entry['name']), _entry['ttl'], _entry['type'], _entry['content'])

    def _convert_records_back(self, _records):
        return [self._to_dns_entry(record) for record in _records]

    # Filter a list of records based on criteria
    def _filter_records(self, records, type=None, name=None, content=None):
        _records = []
        for record in records:
            if (not type or record['type'] == type) and \
               (not name or self._full_name(record['name']) == self._full_name(name)) and \
               (not content or record['content'] == content):
                _records.append(record)
        return _records
예제 #3
0
파일: transip.py 프로젝트: mintopia/octodns
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 = DomainService(account, private_key_file=key_file)
        elif key is not None:
            self._client = DomainService(account, private_key=key)
        else:
            raise TransipConfigException(
                'Missing `key` of `key_file` parameter in config')

        self.account = account
        self.key = key

        self._currentZone = {}

    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
        }