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