From c6486ce3d1ac29ff706ebfb95a9a4c3fbe78b8be Mon Sep 17 00:00:00 2001 From: "Yaroshevich, Denis" Date: Mon, 26 Jul 2021 17:57:16 +0300 Subject: [PATCH] add support for NS, MX, TXT, SRV, CNAME, PTR --- README.md | 2 +- octodns/provider/gcore.py | 117 ++++++++++- tests/fixtures/gcore-no-changes.json | 285 ++++++++++++++++++++++----- tests/fixtures/gcore-records.json | 221 +++++++++++++++++++-- tests/test_octodns_provider_gcore.py | 145 ++++++++++++-- 5 files changed, 677 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 52d3a25..2323633 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,7 @@ The above command pulled the existing data out of Route53 and placed the results | [EtcHostsProvider](/octodns/provider/etc_hosts.py) | | A, AAAA, ALIAS, CNAME | No | | | [EnvVarSource](/octodns/source/envvar.py) | | TXT | No | read-only environment variable injection | | [GandiProvider](/octodns/provider/gandi.py) | | A, AAAA, ALIAS, CAA, CNAME, DNAME, MX, NS, PTR, SPF, SRV, SSHFP, TXT | No | | -| [GCoreProvider](/octodns/provider/gcore.py) | | A, AAAA | No | | +| [GCoreProvider](/octodns/provider/gcore.py) | | A, AAAA, NS, MX, TXT, SRV, CNAME, PTR | No | | | [GoogleCloudProvider](/octodns/provider/googlecloud.py) | google-cloud-dns | A, AAAA, CAA, CNAME, MX, NAPTR, NS, PTR, SPF, SRV, TXT | No | | | [MythicBeastsProvider](/octodns/provider/mythicbeasts.py) | Mythic Beasts | A, AAAA, ALIAS, CNAME, MX, NS, SRV, SSHFP, CAA, TXT | No | | | [Ns1Provider](/octodns/provider/ns1.py) | ns1-python | All | Yes | Missing `NA` geo target | diff --git a/octodns/provider/gcore.py b/octodns/provider/gcore.py index d5c7fa0..c551f03 100644 --- a/octodns/provider/gcore.py +++ b/octodns/provider/gcore.py @@ -161,7 +161,7 @@ class GCoreProvider(BaseProvider): SUPPORTS_GEO = False SUPPORTS_DYNAMIC = False - SUPPORTS = set(("A", "AAAA")) + SUPPORTS = set(("A", "AAAA", "NS", "MX", "TXT", "SRV", "CNAME", "PTR")) def __init__(self, id, *args, **kwargs): token = kwargs.pop("token", None) @@ -183,7 +183,22 @@ class GCoreProvider(BaseProvider): password=password, ) + def _add_dot_if_need(self, value): + return "{}.".format(value) if not value.endswith(".") else value + def _data_for_single(self, _type, record): + return { + "ttl": record["ttl"], + "type": _type, + "value": self._add_dot_if_need( + record["resource_records"][0]["content"][0] + ), + } + + _data_for_CNAME = _data_for_single + _data_for_PTR = _data_for_single + + def _data_for_multiple(self, _type, record): return { "ttl": record["ttl"], "type": _type, @@ -194,8 +209,62 @@ class GCoreProvider(BaseProvider): ], } - _data_for_A = _data_for_single - _data_for_AAAA = _data_for_single + _data_for_A = _data_for_multiple + _data_for_AAAA = _data_for_multiple + + def _data_for_TXT(self, _type, record): + return { + "ttl": record["ttl"], + "type": _type, + "values": [ + rr_value.replace(";", "\\;") + for resource_record in record["resource_records"] + for rr_value in resource_record["content"] + ], + } + + def _data_for_MX(self, _type, record): + return { + "ttl": record["ttl"], + "type": _type, + "values": [ + dict( + preference=preference, + exchange=self._add_dot_if_need(exchange), + ) + for preference, exchange in map( + lambda x: x["content"], record["resource_records"] + ) + ], + } + + def _data_for_NS(self, _type, record): + return { + "ttl": record["ttl"], + "type": _type, + "values": [ + self._add_dot_if_need(rr_value) + for resource_record in record["resource_records"] + for rr_value in resource_record["content"] + ], + } + + def _data_for_SRV(self, _type, record): + return { + "ttl": record["ttl"], + "type": _type, + "values": [ + dict( + priority=priority, + weight=weight, + port=port, + target=self._add_dot_if_need(target), + ) + for priority, weight, port, target in map( + lambda x: x["content"], record["resource_records"] + ) + ], + } def zone_records(self, zone): try: @@ -241,6 +310,15 @@ class GCoreProvider(BaseProvider): return exists def _params_for_single(self, record): + return { + "ttl": record.ttl, + "resource_records": [{"content": [record.value]}], + } + + _params_for_CNAME = _params_for_single + _params_for_PTR = _params_for_single + + def _params_for_multiple(self, record): return { "ttl": record.ttl, "resource_records": [ @@ -248,8 +326,37 @@ class GCoreProvider(BaseProvider): ], } - _params_for_A = _params_for_single - _params_for_AAAA = _params_for_single + _params_for_A = _params_for_multiple + _params_for_AAAA = _params_for_multiple + _params_for_NS = _params_for_multiple + + def _params_for_TXT(self, record): + # print(record.values) + return { + "ttl": record.ttl, + "resource_records": [ + {"content": [value.replace("\\;", ";")]} + for value in record.values + ], + } + + def _params_for_MX(self, record): + return { + "ttl": record.ttl, + "resource_records": [ + {"content": [rec.preference, rec.exchange]} + for rec in record.values + ], + } + + def _params_for_SRV(self, record): + return { + "ttl": record.ttl, + "resource_records": [ + {"content": [rec.priority, rec.weight, rec.port, rec.target]} + for rec in record.values + ], + } def _apply_create(self, change): self.log.info("creating: %s", change) diff --git a/tests/fixtures/gcore-no-changes.json b/tests/fixtures/gcore-no-changes.json index e5ff8c9..b1a3b25 100644 --- a/tests/fixtures/gcore-no-changes.json +++ b/tests/fixtures/gcore-no-changes.json @@ -1,56 +1,245 @@ { - "rrsets": [{ - "name": "unit.tests", - "type": "A", - "ttl": 300, - "resource_records": [{ - "content": [ - "1.2.3.4" + "rrsets": [ + { + "name": "unit.tests", + "type": "A", + "ttl": 300, + "resource_records": [ + { + "content": [ + "1.2.3.4" + ] + }, + { + "content": [ + "1.2.3.5" + ] + } ] - }, { - "content": [ - "1.2.3.5" + }, + { + "name": "unit.tests", + "type": "NS", + "ttl": 300, + "resource_records": [ + { + "content": [ + "ns2.gcdn.services" + ] + }, + { + "content": [ + "ns1.gcorelabs.net" + ] + } ] - }] - }, { - "name": "aaaa.unit.tests", - "type": "AAAA", - "ttl": 600, - "resource_records": [{ - "content": [ - "2601:644:500:e210:62f8:1dff:feb8:947a" + }, + { + "name": "_imap._tcp", + "type": "SRV", + "ttl": 600, + "resource_records": [ + { + "content": [ + 0, + 0, + 0, + "." + ] + } ] - }] - }, { - "name": "www.unit.tests.", - "type": "A", - "ttl": 300, - "resource_records": [{ - "content": [ - "2.2.3.6" + }, + { + "name": "_pop3._tcp", + "type": "SRV", + "ttl": 600, + "resource_records": [ + { + "content": [ + 0, + 0, + 0, + "." + ] + } ] - }] - }, { - "name": "www.sub.unit.tests.", - "type": "A", - "ttl": 300, - "resource_records": [{ - "content": [ - "2.2.3.6" + }, + { + "name": "_srv._tcp", + "type": "SRV", + "ttl": 600, + "resource_records": [ + { + "content": [ + 12, + 20, + 30, + "foo-2.unit.tests" + ] + }, + { + "content": [ + 10, + 20, + 30, + "foo-1.unit.tests" + ] + } ] - }] - }, { - "name": "unit.tests", - "type": "ns", - "ttl": 300, - "resource_records": [{ - "content": [ - "ns2.gcdn.services" + }, + { + "name": "aaaa.unit.tests", + "type": "AAAA", + "ttl": 600, + "resource_records": [ + { + "content": [ + "2601:644:500:e210:62f8:1dff:feb8:947a" + ] + } ] - }, { - "content": [ - "ns1.gcorelabs.net" + }, + { + "name": "cname.unit.tests", + "type": "CNAME", + "ttl": 300, + "resource_records": [ + { + "content": [ + "unit.tests." + ] + } ] - }] - }] -} + }, + { + "name": "excluded.unit.tests", + "type": "CNAME", + "ttl": 3600, + "resource_records": [ + { + "content": [ + "unit.tests." + ] + } + ] + }, + { + "name": "mx.unit.tests", + "type": "MX", + "ttl": 300, + "resource_records": [ + { + "content": [ + 40, + "smtp-1.unit.tests." + ] + }, + { + "content": [ + 20, + "smtp-2.unit.tests." + ] + }, + { + "content": [ + 30, + "smtp-3.unit.tests." + ] + }, + { + "content": [ + 10, + "smtp-4.unit.tests." + ] + } + ] + }, + { + "name": "ptr.unit.tests.", + "type": "PTR", + "ttl": 300, + "resource_records": [ + { + "content": [ + "foo.bar.com" + ] + } + ] + }, + { + "name": "sub.unit.tests", + "type": "NS", + "ttl": 3600, + "resource_records": [ + { + "content": [ + "6.2.3.4" + ] + }, + { + "content": [ + "7.2.3.4" + ] + } + ] + }, + { + "name": "txt.unit.tests", + "type": "TXT", + "ttl": 600, + "resource_records": [ + { + "content": [ + "Bah bah black sheep" + ] + }, + { + "content": [ + "have you any wool." + ] + }, + { + "content": [ + "v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs" + ] + } + ] + }, + { + "name": "www.unit.tests.", + "type": "A", + "ttl": 300, + "resource_records": [ + { + "content": [ + "2.2.3.6" + ] + } + ] + }, + { + "name": "www.sub.unit.tests.", + "type": "A", + "ttl": 300, + "resource_records": [ + { + "content": [ + "2.2.3.6" + ] + } + ] + }, + { + "name": "spf.sub.unit.tests.", + "type": "SPF", + "ttl": 600, + "resource_records": [ + { + "content": [ + "v=spf1 ip4:192.168.0.1/16-all" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/gcore-records.json b/tests/fixtures/gcore-records.json index 4086049..ba29246 100644 --- a/tests/fixtures/gcore-records.json +++ b/tests/fixtures/gcore-records.json @@ -1,25 +1,204 @@ { - "rrsets": [{ - "name": "unit.tests", - "type": "A", - "ttl": 300, - "resource_records": [{ - "content": [ - "1.2.3.4" + "rrsets": [ + { + "name": "unit.tests", + "type": "A", + "ttl": 300, + "resource_records": [ + { + "content": [ + "1.2.3.4" + ] + } ] - }] - }, { - "name": "unit.tests", - "type": "ns", - "ttl": 300, - "resource_records": [{ - "content": [ - "ns2.gcdn.services" + }, + { + "name": "unit.tests", + "type": "NS", + "ttl": 300, + "resource_records": [ + { + "content": [ + "ns2.gcdn.services" + ] + }, + { + "content": [ + "ns1.gcorelabs.net" + ] + } ] - }, { - "content": [ - "ns1.gcorelabs.net" + }, + { + "name": "_imap._tcp", + "type": "SRV", + "ttl": 1200, + "resource_records": [ + { + "content": [ + 0, + 0, + 0, + "." + ] + } ] - }] - }] -} + }, + { + "name": "_pop3._tcp", + "type": "SRV", + "ttl": 1200, + "resource_records": [ + { + "content": [ + 0, + 0, + 0, + "." + ] + } + ] + }, + { + "name": "_srv._tcp", + "type": "SRV", + "ttl": 1200, + "resource_records": [ + { + "content": [ + 12, + 20, + 30, + "foo-2.unit.tests." + ] + }, + { + "content": [ + 10, + 20, + 30, + "foo-1.unit.tests." + ] + } + ] + }, + { + "name": "aaaa.unit.tests", + "type": "AAAA", + "ttl": 600, + "resource_records": [ + { + "content": [ + "2601:644:500:e210:62f8:1dff:feb8:947a" + ] + } + ] + }, + { + "name": "cname.unit.tests", + "type": "CNAME", + "ttl": 300, + "resource_records": [ + { + "content": [ + "unit.tests." + ] + } + ] + }, + { + "name": "mx.unit.tests", + "type": "MX", + "ttl": 600, + "resource_records": [ + { + "content": [ + 40, + "smtp-1.unit.tests." + ] + }, + { + "content": [ + 20, + "smtp-2.unit.tests." + ] + } + ] + }, + { + "name": "ptr.unit.tests.", + "type": "PTR", + "ttl": 300, + "resource_records": [ + { + "content": [ + "foo.bar.com" + ] + } + ] + }, + { + "name": "sub.unit.tests", + "type": "NS", + "ttl": 300, + "resource_records": [ + { + "content": [ + "6.2.3.4" + ] + }, + { + "content": [ + "7.2.3.4" + ] + } + ] + }, + { + "name": "txt.unit.tests", + "type": "TXT", + "ttl": 300, + "resource_records": [ + { + "content": [ + "\"Bah bah black sheep\"" + ] + }, + { + "content": [ + "\"have you any wool.\"" + ] + }, + { + "content": [ + "\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\"" + ] + } + ] + }, + { + "name": "www.unit.tests.", + "type": "A", + "ttl": 300, + "resource_records": [ + { + "content": [ + "2.2.3.6" + ] + } + ] + }, + { + "name": "www.sub.unit.tests.", + "type": "A", + "ttl": 300, + "resource_records": [ + { + "content": [ + "2.2.3.6" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/test_octodns_provider_gcore.py b/tests/test_octodns_provider_gcore.py index 06e199e..2ddbb59 100644 --- a/tests/test_octodns_provider_gcore.py +++ b/tests/test_octodns_provider_gcore.py @@ -64,13 +64,13 @@ class TestGCoreProvider(TestCase): with self.assertRaises(GCoreClientException) as ctx: zone = Zone("unit.tests.", []) provider.populate(zone) - self.assertEquals("Things caught fire", text_type(ctx.exception)) + self.assertEqual("Things caught fire", text_type(ctx.exception)) # No credentials or token error with requests_mock() as mock: with self.assertRaises(ValueError) as ctx: GCoreProvider("test_id") - self.assertEquals( + self.assertEqual( "either token or login & password must be set", text_type(ctx.exception), ) @@ -116,14 +116,29 @@ class TestGCoreProvider(TestCase): zone = Zone("unit.tests.", []) provider.populate(zone) - self.assertEquals(4, len(zone.records)) - self.assertEquals( - {"aaaa", "www", "www.sub", ""}, {r.name for r in zone.records} + self.assertEqual(14, len(zone.records)) + self.assertEqual( + { + "", + "_imap._tcp", + "_pop3._tcp", + "_srv._tcp", + "aaaa", + "cname", + "excluded", + "mx", + "ptr", + "sub", + "txt", + "www", + "www.sub", + }, + {r.name for r in zone.records}, ) changes = self.expected.changes(zone, provider) - self.assertEquals(0, len(changes)) + self.assertEqual(0, len(changes)) - # 3 removed + 1 modified + # 1 removed + 7 modified with requests_mock() as mock: base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets" with open("tests/fixtures/gcore-records.json") as fh: @@ -131,14 +146,14 @@ class TestGCoreProvider(TestCase): zone = Zone("unit.tests.", []) provider.populate(zone) - self.assertEquals(1, len(zone.records)) + self.assertEqual(13, len(zone.records)) changes = self.expected.changes(zone, provider) - self.assertEquals(4, len(changes)) - self.assertEquals( - 3, len([c for c in changes if isinstance(c, Delete)]) + self.assertEqual(8, len(changes)) + self.assertEqual( + 1, len([c for c in changes if isinstance(c, Delete)]) ) - self.assertEquals( - 1, len([c for c in changes if isinstance(c, Update)]) + self.assertEqual( + 7, len([c for c in changes if isinstance(c, Update)]) ) def test_apply(self): @@ -192,8 +207,8 @@ class TestGCoreProvider(TestCase): plan = provider.plan(self.expected) # create all - self.assertEquals(4, len(plan.changes)) - self.assertEquals(4, provider.apply(plan)) + self.assertEqual(13, len(plan.changes)) + self.assertEqual(13, provider.apply(plan)) self.assertFalse(plan.exists) provider._client._request.assert_has_calls( @@ -221,6 +236,73 @@ class TestGCoreProvider(TestCase): "resource_records": [{"content": ["2.2.3.6"]}], }, ), + call( + "POST", + "http://api/zones/unit.tests/txt.unit.tests./TXT", + data={ + "ttl": 600, + "resource_records": [ + {"content": ["Bah bah black sheep"]}, + {"content": ["have you any wool."]}, + { + "content": [ + "v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+" + "of/long/string+with+numb3rs" + ] + }, + ], + }, + ), + call( + "POST", + "http://api/zones/unit.tests/sub.unit.tests./NS", + data={ + "ttl": 3600, + "resource_records": [ + {"content": ["6.2.3.4."]}, + {"content": ["7.2.3.4."]}, + ], + }, + ), + call( + "POST", + "http://api/zones/unit.tests/ptr.unit.tests./PTR", + data={ + "ttl": 300, + "resource_records": [ + {"content": ["foo.bar.com."]}, + ], + }, + ), + call( + "POST", + "http://api/zones/unit.tests/mx.unit.tests./MX", + data={ + "ttl": 300, + "resource_records": [ + {"content": [10, "smtp-4.unit.tests."]}, + {"content": [20, "smtp-2.unit.tests."]}, + {"content": [30, "smtp-3.unit.tests."]}, + {"content": [40, "smtp-1.unit.tests."]}, + ], + }, + ), + call( + "POST", + "http://api/zones/unit.tests/excluded.unit.tests./CNAME", + data={ + "ttl": 3600, + "resource_records": [{"content": ["unit.tests."]}], + }, + ), + call( + "POST", + "http://api/zones/unit.tests/cname.unit.tests./CNAME", + data={ + "ttl": 300, + "resource_records": [{"content": ["unit.tests."]}], + }, + ), call( "POST", "http://api/zones/unit.tests/aaaa.unit.tests./AAAA", @@ -235,6 +317,33 @@ class TestGCoreProvider(TestCase): ], }, ), + call( + "POST", + "http://api/zones/unit.tests/_srv._tcp.unit.tests./SRV", + data={ + "ttl": 600, + "resource_records": [ + {"content": [10, 20, 30, "foo-1.unit.tests."]}, + {"content": [12, 20, 30, "foo-2.unit.tests."]}, + ], + }, + ), + call( + "POST", + "http://api/zones/unit.tests/_pop3._tcp.unit.tests./SRV", + data={ + "ttl": 600, + "resource_records": [{"content": [0, 0, 0, "."]}], + }, + ), + call( + "POST", + "http://api/zones/unit.tests/_imap._tcp.unit.tests./SRV", + data={ + "ttl": 600, + "resource_records": [{"content": [0, 0, 0, "."]}], + }, + ), call( "POST", "http://api/zones/unit.tests/unit.tests./A", @@ -249,7 +358,7 @@ class TestGCoreProvider(TestCase): ] ) # expected number of total calls - self.assertEquals(7, provider._client._request.call_count) + self.assertEqual(16, provider._client._request.call_count) provider._client._request.reset_mock() @@ -283,8 +392,8 @@ class TestGCoreProvider(TestCase): plan = provider.plan(wanted) self.assertTrue(plan.exists) - self.assertEquals(2, len(plan.changes)) - self.assertEquals(2, provider.apply(plan)) + self.assertEqual(2, len(plan.changes)) + self.assertEqual(2, provider.apply(plan)) provider._client._request.assert_has_calls( [