From 3d806b27ffd523da39f3dfa25e105fa04a286c64 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Thu, 16 Sep 2021 13:46:16 -0700 Subject: [PATCH] Remove (hopefully temporarily) TransipProvider due to suds failure to install --- CHANGELOG.md | 3 + README.md | 1 - octodns/provider/transip.py | 354 ------------------------- requirements.txt | 1 - tests/test_octodns_provider_transip.py | 291 -------------------- 5 files changed, 3 insertions(+), 647 deletions(-) delete mode 100644 octodns/provider/transip.py delete mode 100644 tests/test_octodns_provider_transip.py diff --git a/CHANGELOG.md b/CHANGELOG.md index caf5e00..da55eb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ previous versions of octoDNS are discouraged and may result in undefined behavior and broken records. See https://github.com/octodns/octodns/pull/749 for related discussion. +* TransipProvider removed as it currently relies on `suds` which is broken in + new python versions and hasn't seen a release since 2010. May return with + https://github.com/octodns/octodns/pull/762 ## v0.9.13 - 2021-07-18 - Processors Alpha diff --git a/README.md b/README.md index ebd7111..9b983a2 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,6 @@ The above command pulled the existing data out of Route53 and placed the results | [Rackspace](/octodns/provider/rackspace.py) | | A, AAAA, ALIAS, CNAME, MX, NS, PTR, SPF, TXT | No | | | [Route53](/octodns/provider/route53.py) | boto3 | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | Both | CNAME health checks don't support a Host header | | [Selectel](/octodns/provider/selectel.py) | | A, AAAA, CNAME, MX, NS, SPF, SRV, TXT | No | | -| [Transip](/octodns/provider/transip.py) | transip | A, AAAA, CNAME, MX, NS, SRV, SPF, TXT, SSHFP, CAA | No | | | [UltraDns](/octodns/provider/ultra.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | | | [AxfrSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, LOC, MX, NS, PTR, SPF, SRV, TXT | No | read-only | | [ZoneFileSource](/octodns/source/axfr.py) | | A, AAAA, CAA, CNAME, MX, NS, PTR, SPF, SRV, TXT | No | read-only | diff --git a/octodns/provider/transip.py b/octodns/provider/transip.py deleted file mode 100644 index 176da88..0000000 --- a/octodns/provider/transip.py +++ /dev/null @@ -1,354 +0,0 @@ -# -# -# - -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_file is not None: - self._client = self._domain_service(account, - private_key_file=key_file) - elif key is not None: - self._client = self._domain_service(account, private_key=key) - else: - raise TransipConfigException( - 'Missing `key` or `key_file` parameter in config' - ) - - self._currentZone = {} - - def _domain_service(self, *args, **kwargs): - 'This exists only for mocking purposes' - return DomainService(*args, **kwargs) - - 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: - 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 - } diff --git a/requirements.txt b/requirements.txt index 6f5ed73..af9b483 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,4 +26,3 @@ requests==2.24.0 s3transfer==0.3.3 setuptools==44.1.1 six==1.15.0 -transip==2.1.2 diff --git a/tests/test_octodns_provider_transip.py b/tests/test_octodns_provider_transip.py deleted file mode 100644 index 8a2e11a..0000000 --- a/tests/test_octodns_provider_transip.py +++ /dev/null @@ -1,291 +0,0 @@ -# -# -# - -from __future__ import absolute_import, division, print_function, \ - unicode_literals - -from os.path import dirname, join -from six import text_type - -from suds import WebFault - -from mock import patch -from unittest import TestCase - -from octodns.provider.transip import TransipProvider -from octodns.provider.yaml import YamlProvider -from octodns.zone import Zone -from transip.service.objects import DnsEntry - - -class MockFault(object): - faultstring = "" - faultcode = "" - - def __init__(self, code, string, *args, **kwargs): - self.faultstring = string - self.faultcode = code - - -class MockResponse(object): - dnsEntries = [] - - -class MockDomainService(object): - - def __init__(self, *args, **kwargs): - self.mockupEntries = [] - self.throw_auth_fault = False - - def mockup(self, records): - - provider = TransipProvider('', '', '') - - _dns_entries = [] - for record in records: - if record._type in provider.SUPPORTS: - entries_for = getattr(provider, - '_entries_for_{}'.format(record._type)) - - # Root records have '@' as name - name = record.name - if name == '': - name = provider.ROOT_RECORD - - _dns_entries.extend(entries_for(name, record)) - - # Add a non-supported type - # so it triggers the "is supported" (transip.py:115) check and - # give 100% code coverage - _dns_entries.append( - DnsEntry('@', '3600', 'BOGUS', 'ns01.transip.nl.')) - - self.mockupEntries = _dns_entries - - # Skips authentication layer and returns the entries loaded by "Mockup" - def get_info(self, domain_name): - - if self.throw_auth_fault: - self.raiseInvalidAuth() - - # Special 'domain' to trigger error - if str(domain_name) == str('notfound.unit.tests'): - self.raiseZoneNotFound() - - result = MockResponse() - result.dnsEntries = self.mockupEntries - return result - - def set_dns_entries(self, domain_name, dns_entries): - - # Special 'domain' to trigger error - if str(domain_name) == str('failsetdns.unit.tests'): - self.raiseSaveError() - - return True - - def raiseZoneNotFound(self): - fault = MockFault(str('102'), '102 is zone not found') - document = {} - raise WebFault(fault, document) - - def raiseInvalidAuth(self): - fault = MockFault(str('200'), '200 is invalid auth') - document = {} - raise WebFault(fault, document) - - def raiseSaveError(self): - fault = MockFault(str('200'), '202 random error') - document = {} - raise WebFault(fault, document) - - -class TestTransipProvider(TestCase): - - bogus_key = str("""-----BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEA0U5HGCkLrz423IyUf3u4cKN2WrNz1x5KNr6PvH2M/zxas+zB -elbxkdT3AQ+wmfcIvOuTmFRTHv35q2um1aBrPxVw+2s+lWo28VwIRttwIB1vIeWu -lSBnkEZQRLyPI2tH0i5QoMX4CVPf9rvij3Uslimi84jdzDfPFIh6jZ6C8nLipOTG -0IMhge1ofVfB0oSy5H+7PYS2858QLAf5ruYbzbAxZRivS402wGmQ0d0Lc1KxraAj -kiMM5yj/CkH/Vm2w9I6+tLFeASE4ub5HCP5G/ig4dbYtqZMQMpqyAbGxd5SOVtyn -UHagAJUxf8DT3I8PyjEHjxdOPUsxNyRtepO/7QIDAQABAoIBAQC7fiZ7gxE/ezjD -2n6PsHFpHVTBLS2gzzZl0dCKZeFvJk6ODJDImaeuHhrh7X8ifMNsEI9XjnojMhl8 -MGPzy88mZHugDNK0H8B19x5G8v1/Fz7dG5WHas660/HFkS+b59cfdXOugYiOOn9O -08HBBpLZNRUOmVUuQfQTjapSwGLG8PocgpyRD4zx0LnldnJcqYCxwCdev+AAsPnq -ibNtOd/MYD37w9MEGcaxLE8wGgkv8yd97aTjkgE+tp4zsM4QE4Rag133tsLLNznT -4Qr/of15M3NW/DXq/fgctyRcJjZpU66eCXLCz2iRTnLyyxxDC2nwlxKbubV+lcS0 -S4hbfd/BAoGBAO8jXxEaiybR0aIhhSR5esEc3ymo8R8vBN3ZMJ+vr5jEPXr/ZuFj -/R4cZ2XV3VoQJG0pvIOYVPZ5DpJM7W+zSXtJ/7bLXy4Bnmh/rc+YYgC+AXQoLSil -iD2OuB2xAzRAK71DVSO0kv8gEEXCersPT2i6+vC2GIlJvLcYbOdRKWGxAoGBAOAQ -aJbRLtKujH+kMdoMI7tRlL8XwI+SZf0FcieEu//nFyerTePUhVgEtcE+7eQ7hyhG -fIXUFx/wALySoqFzdJDLc8U8pTLhbUaoLOTjkwnCTKQVprhnISqQqqh/0U5u47IE -RWzWKN6OHb0CezNTq80Dr6HoxmPCnJHBHn5LinT9AoGAQSpvZpbIIqz8pmTiBl2A -QQ2gFpcuFeRXPClKYcmbXVLkuhbNL1BzEniFCLAt4LQTaRf9ghLJ3FyCxwVlkpHV -zV4N6/8hkcTpKOraL38D/dXJSaEFJVVuee/hZl3tVJjEEpA9rDwx7ooLRSdJEJ6M -ciq55UyKBSdt4KssSiDI2RECgYBL3mJ7xuLy5bWfNsrGiVvD/rC+L928/5ZXIXPw -26oI0Yfun7ulDH4GOroMcDF/GYT/Zzac3h7iapLlR0WYI47xxGI0A//wBZLJ3QIu -krxkDo2C9e3Y/NqnHgsbOQR3aWbiDT4wxydZjIeXS3LKA2fl6Hyc90PN3cTEOb8I -hq2gRQKBgEt0SxhhtyB93SjgTzmUZZ7PiEf0YJatfM6cevmjWHexrZH+x31PB72s -fH2BQyTKKzoCLB1k/6HRaMnZdrWyWSZ7JKz3AHJ8+58d0Hr8LTrzDM1L6BbjeDct -N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd ------END RSA PRIVATE KEY-----""") - - def make_expected(self): - expected = Zone('unit.tests.', []) - source = YamlProvider('test', join(dirname(__file__), 'config')) - source.populate(expected) - return expected - - @patch('octodns.provider.transip.TransipProvider._domain_service', - return_value=MockDomainService()) - def test_init(self, _): - - # No key nor key_file - with self.assertRaises(Exception) as ctx: - TransipProvider('test', 'unittest') - - self.assertEquals( - str('Missing `key` or `key_file` parameter in config'), - str(ctx.exception)) - - # With key - TransipProvider('test', 'unittest', key=self.bogus_key) - - # With key_file - TransipProvider('test', 'unittest', key_file='/fake/path') - - @patch('suds.client.Client.__init__', new=lambda *args, **kwargs: None) - def test_domain_service(self): - # Special case smoke test for DomainService to get coverage - TransipProvider('test', 'unittest', key=self.bogus_key) - - @patch('octodns.provider.transip.TransipProvider._domain_service', - return_value=MockDomainService()) - def test_populate(self, _): - _expected = self.make_expected() - - # Unhappy Plan - Not authenticated - # Live test against API, will fail in an unauthorized error - with self.assertRaises(WebFault) as ctx: - provider = TransipProvider('test', 'unittest', self.bogus_key) - provider._client.throw_auth_fault = True - zone = Zone('unit.tests.', []) - provider.populate(zone, True) - - self.assertEquals(str('WebFault'), - str(ctx.exception.__class__.__name__)) - - self.assertEquals(str('200'), ctx.exception.fault.faultcode) - - # No more auth problems - provider._client.throw_auth_fault = False - - # Unhappy Plan - Zone does not exists - # Will trigger an exception if provider is used as a target for a - # non-existing zone - with self.assertRaises(Exception) as ctx: - provider = TransipProvider('test', 'unittest', self.bogus_key) - zone = Zone('notfound.unit.tests.', []) - provider.populate(zone, True) - - self.assertEquals(str('TransipNewZoneException'), - str(ctx.exception.__class__.__name__)) - - self.assertEquals( - 'populate: (102) Transip used as target' + - ' for non-existing zone: notfound.unit.tests.', - text_type(ctx.exception)) - - # Happy Plan - Zone does not exists - # Won't trigger an exception if provider is NOT used as a target for a - # non-existing zone. - provider = TransipProvider('test', 'unittest', self.bogus_key) - zone = Zone('notfound.unit.tests.', []) - provider.populate(zone, False) - - # Happy Plan - Populate with mockup records - provider = TransipProvider('test', 'unittest', self.bogus_key) - provider._client.mockup(_expected.records) - zone = Zone('unit.tests.', []) - provider.populate(zone, False) - - # Transip allows relative values for types like cname, mx. - # Test is these are correctly appended with the domain - provider._currentZone = zone - self.assertEquals("www.unit.tests.", provider._parse_to_fqdn("www")) - self.assertEquals("www.unit.tests.", - provider._parse_to_fqdn("www.unit.tests.")) - self.assertEquals("www.sub.sub.sub.unit.tests.", - provider._parse_to_fqdn("www.sub.sub.sub")) - self.assertEquals("unit.tests.", - provider._parse_to_fqdn("@")) - - # Happy Plan - Even if the zone has no records the zone should exist - provider = TransipProvider('test', 'unittest', self.bogus_key) - zone = Zone('unit.tests.', []) - exists = provider.populate(zone, True) - self.assertTrue(exists, 'populate should return true') - - return - - @patch('octodns.provider.transip.TransipProvider._domain_service', - return_value=MockDomainService()) - def test_plan(self, _): - _expected = self.make_expected() - - # Test Happy plan, only create - provider = TransipProvider('test', 'unittest', self.bogus_key) - plan = provider.plan(_expected) - - self.assertEqual(15, plan.change_counts['Create']) - self.assertEqual(0, plan.change_counts['Update']) - self.assertEqual(0, plan.change_counts['Delete']) - - return - - @patch('octodns.provider.transip.TransipProvider._domain_service', - return_value=MockDomainService()) - def test_apply(self, _): - _expected = self.make_expected() - - # Test happy flow. Create all supoorted records - provider = TransipProvider('test', 'unittest', self.bogus_key) - plan = provider.plan(_expected) - self.assertEqual(15, len(plan.changes)) - changes = provider.apply(plan) - self.assertEqual(changes, len(plan.changes)) - - # Test unhappy flow. Trigger 'not found error' in apply stage - # This should normally not happen as populate will capture it first - # but just in case. - changes = [] # reset changes - with self.assertRaises(Exception) as ctx: - provider = TransipProvider('test', 'unittest', self.bogus_key) - plan = provider.plan(_expected) - plan.desired.name = 'notfound.unit.tests.' - changes = provider.apply(plan) - - # Changes should not be set due to an Exception - self.assertEqual([], changes) - - self.assertEquals(str('WebFault'), - str(ctx.exception.__class__.__name__)) - - self.assertEquals(str('102'), ctx.exception.fault.faultcode) - - # Test unhappy flow. Trigger a unrecoverable error while saving - _expected = self.make_expected() # reset expected - changes = [] # reset changes - - with self.assertRaises(Exception) as ctx: - provider = TransipProvider('test', 'unittest', self.bogus_key) - plan = provider.plan(_expected) - plan.desired.name = 'failsetdns.unit.tests.' - changes = provider.apply(plan) - - # Changes should not be set due to an Exception - self.assertEqual([], changes) - - self.assertEquals(str('TransipException'), - str(ctx.exception.__class__.__name__))