Merge remote-tracking branch 'origin/master' into constellix-improvements

This commit is contained in:
apatserkovskyi
2021-09-01 22:37:40 +03:00
143 changed files with 12948 additions and 784 deletions
+21
View File
@@ -0,0 +1,21 @@
manager:
max_workers: 2
providers:
in:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
zones:
unit.tests.:
sources:
- in
targets:
- dump
alias.tests.:
alias: unit.tests.
alias-loop.tests.:
alias: alias.tests.
+23
View File
@@ -109,6 +109,29 @@ cname:
- pool: iad
type: CNAME
value: target.unit.tests.
pool-only-in-fallback:
dynamic:
pools:
one:
fallback: two
values:
- value: 1.1.1.1
three:
values:
- value: 3.3.3.3
two:
values:
- value: 2.2.2.2
rules:
- geos:
- NA-US
pool: one
- geos:
- AS-SG
pool: three
ttl: 300
type: A
values: [4.4.4.4]
real-ish-a:
dynamic:
pools:
+6
View File
@@ -0,0 +1,6 @@
manager:
plan_outputs:
"doesntexist":
class: octodns.provider.plan.DoesntExist
providers: {}
zones: {}
@@ -0,0 +1,23 @@
providers:
config:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
geo:
class: helpers.GeoProvider
nosshfp:
class: helpers.NoSshFpProvider
processors:
no-class: {}
zones:
unit.tests.:
processors:
- noop
sources:
- in
targets:
- dump
+25
View File
@@ -0,0 +1,25 @@
providers:
config:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
geo:
class: helpers.GeoProvider
nosshfp:
class: helpers.NoSshFpProvider
processors:
# valid class, but it wants a param and we're not passing it
wants-config:
class: helpers.WantsConfigProcessor
zones:
unit.tests.:
processors:
- noop
sources:
- in
targets:
- dump
+33
View File
@@ -0,0 +1,33 @@
providers:
config:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
geo:
class: helpers.GeoProvider
nosshfp:
class: helpers.NoSshFpProvider
processors:
# Just testing config so any processor will do
noop:
class: octodns.processor.base.BaseProcessor
zones:
unit.tests.:
processors:
- noop
sources:
- config
targets:
- dump
bad.unit.tests.:
processors:
- doesnt-exist
sources:
- in
targets:
- dump
+19
View File
@@ -0,0 +1,19 @@
manager:
max_workers: 2
providers:
in:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
zones:
unit.tests.:
sources:
- in
targets:
- dump
alias.tests.:
alias: unit.tests.
+3
View File
@@ -4,14 +4,17 @@ providers:
in:
class: octodns.provider.yaml.SplitYamlProvider
directory: tests/config/split
extension: .tst
dump:
class: octodns.provider.yaml.SplitYamlProvider
directory: env/YAML_TMP_DIR
extension: .tst
# This is sort of ugly, but it shouldn't hurt anything. It'll just write out
# the target file twice where it and dump are both used
dump2:
class: octodns.provider.yaml.SplitYamlProvider
directory: env/YAML_TMP_DIR
extension: .tst
simple:
class: helpers.SimpleProvider
geo:
@@ -0,0 +1,5 @@
---
dname:
ttl: 300
type: DNAME
value: unit.tests.
@@ -2,4 +2,4 @@
ptr:
ttl: 300
type: PTR
value: foo.bar.com.
values: [foo.bar.com.]
@@ -0,0 +1,15 @@
---
urlfwd:
ttl: 300
type: URLFWD
values:
- code: 302
masking: 2
path: '/'
query: 0
target: 'http://www.unit.tests'
- code: 301
masking: 2
path: '/target'
query: 0
target: 'http://target.unit.tests'
+63 -1
View File
@@ -36,6 +36,22 @@
- flags: 0
tag: issue
value: ca.unit.tests
_imap._tcp:
ttl: 600
type: SRV
values:
- port: 0
priority: 0
target: .
weight: 0
_pop3._tcp:
ttl: 600
type: SRV
values:
- port: 0
priority: 0
target: .
weight: 0
_srv._tcp:
ttl: 600
type: SRV
@@ -56,6 +72,10 @@ cname:
ttl: 300
type: CNAME
value: unit.tests.
dname:
ttl: 300
type: DNAME
value: unit.tests.
excluded:
octodns:
excluded:
@@ -73,6 +93,34 @@ included:
- test
type: CNAME
value: unit.tests.
loc:
ttl: 300
type: LOC
values:
- altitude: 20
lat_degrees: 31
lat_direction: S
lat_minutes: 58
lat_seconds: 52.1
long_degrees: 115
long_direction: E
long_minutes: 49
long_seconds: 11.7
precision_horz: 10
precision_vert: 2
size: 10
- altitude: 20
lat_degrees: 53
lat_direction: N
lat_minutes: 13
lat_seconds: 10
long_degrees: 2
long_direction: W
long_minutes: 18
long_seconds: 26
precision_horz: 1000
precision_vert: 2
size: 10
mx:
ttl: 300
type: MX
@@ -104,7 +152,7 @@ naptr:
ptr:
ttl: 300
type: PTR
value: foo.bar.com.
values: [foo.bar.com.]
spf:
ttl: 600
type: SPF
@@ -121,6 +169,20 @@ txt:
- Bah bah black sheep
- have you any wool.
- 'v=DKIM1\;k=rsa\;s=email\;h=sha256\;p=A/kinda+of/long/string+with+numb3rs'
urlfwd:
ttl: 300
type: URLFWD
values:
- code: 302
masking: 2
path: '/'
query: 0
target: 'http://www.unit.tests'
- code: 301
masking: 2
path: '/target'
query: 0
target: 'http://target.unit.tests'
www:
ttl: 300
type: A
+17
View File
@@ -0,0 +1,17 @@
manager:
max_workers: 2
providers:
in:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
zones:
unit.tests.:
sources:
- in
processors:
- missing
targets:
- dump
+18
View File
@@ -0,0 +1,18 @@
manager:
max_workers: 2
providers:
in:
class: octodns.provider.yaml.YamlProvider
directory: tests/config
dump:
class: octodns.provider.yaml.YamlProvider
directory: env/YAML_TMP_DIR
zones:
unit.tests.:
sources:
- in
targets:
- dump
alias.tests.:
alias: does-not-exists.tests.
+16 -16
View File
@@ -177,15 +177,15 @@
{
"id": "fc12ab34cd5611334422ab3322997656",
"type": "SRV",
"name": "_srv._tcp.unit.tests",
"name": "_imap._tcp.unit.tests",
"data": {
"service": "_srv",
"service": "_imap",
"proto": "_tcp",
"name": "unit.tests",
"priority": 12,
"weight": 20,
"port": 30,
"target": "foo-2.unit.tests"
"priority": 0,
"weight": 0,
"port": 0,
"target": "."
},
"proxiable": true,
"proxied": false,
@@ -202,15 +202,15 @@
{
"id": "fc12ab34cd5611334422ab3322997656",
"type": "SRV",
"name": "_srv._tcp.unit.tests",
"name": "_pop3._tcp.unit.tests",
"data": {
"service": "_srv",
"proto": "_tcp",
"service": "_imap",
"proto": "_pop3",
"name": "unit.tests",
"priority": 10,
"weight": 20,
"port": 30,
"target": "foo-1.unit.tests"
"priority": 0,
"weight": 0,
"port": 0,
"target": "."
},
"proxiable": true,
"proxied": false,
@@ -227,10 +227,10 @@
],
"result_info": {
"page": 2,
"per_page": 11,
"total_pages": 2,
"per_page": 10,
"total_pages": 3,
"count": 10,
"total_count": 20
"total_count": 24
},
"success": true,
"errors": [],
+128
View File
@@ -0,0 +1,128 @@
{
"result": [
{
"id": "fc12ab34cd5611334422ab3322997656",
"type": "SRV",
"name": "_srv._tcp.unit.tests",
"data": {
"service": "_srv",
"proto": "_tcp",
"name": "unit.tests",
"priority": 12,
"weight": 20,
"port": 30,
"target": "foo-2.unit.tests"
},
"proxiable": true,
"proxied": false,
"ttl": 600,
"locked": false,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"modified_on": "2017-03-11T18:01:43.940682Z",
"created_on": "2017-03-11T18:01:43.940682Z",
"meta": {
"auto_added": false
}
},
{
"id": "fc12ab34cd5611334422ab3322997656",
"type": "SRV",
"name": "_srv._tcp.unit.tests",
"data": {
"service": "_srv",
"proto": "_tcp",
"name": "unit.tests",
"priority": 10,
"weight": 20,
"port": 30,
"target": "foo-1.unit.tests"
},
"proxiable": true,
"proxied": false,
"ttl": 600,
"locked": false,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"modified_on": "2017-03-11T18:01:43.940682Z",
"created_on": "2017-03-11T18:01:43.940682Z",
"meta": {
"auto_added": false
}
},
{
"id": "372e67954025e0ba6aaa6d586b9e0b59",
"type": "LOC",
"name": "loc.unit.tests",
"content": "IN LOC 31 58 52.1 S 115 49 11.7 E 20m 10m 10m 2m",
"proxiable": true,
"proxied": false,
"ttl": 300,
"locked": false,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"created_on": "2020-01-28T05:20:00.12345Z",
"modified_on": "2020-01-28T05:20:00.12345Z",
"data": {
"lat_degrees": 31,
"lat_minutes": 58,
"lat_seconds": 52.1,
"lat_direction": "S",
"long_degrees": 115,
"long_minutes": 49,
"long_seconds": 11.7,
"long_direction": "E",
"altitude": 20,
"size": 10,
"precision_horz": 10,
"precision_vert": 2
},
"meta": {
"auto_added": true,
"source": "primary"
}
},
{
"id": "372e67954025e0ba6aaa6d586b9e0b59",
"type": "LOC",
"name": "loc.unit.tests",
"content": "IN LOC 53 14 10 N 2 18 26 W 20m 10m 1000m 2m",
"proxiable": true,
"proxied": false,
"ttl": 300,
"locked": false,
"zone_id": "ff12ab34cd5611334422ab3322997650",
"zone_name": "unit.tests",
"created_on": "2020-01-28T05:20:00.12345Z",
"modified_on": "2020-01-28T05:20:00.12345Z",
"data": {
"lat_degrees": 53,
"lat_minutes": 13,
"lat_seconds": 10,
"lat_direction": "N",
"long_degrees": 2,
"long_minutes": 18,
"long_seconds": 26,
"long_direction": "W",
"altitude": 20,
"size": 10,
"precision_horz": 1000,
"precision_vert": 2
},
"meta": {
"auto_added": true,
"source": "primary"
}
}
],
"result_info": {
"page": 3,
"per_page": 10,
"total_pages": 3,
"count": 4,
"total_count": 24
},
"success": true,
"errors": [],
"messages": []
}
+103
View File
@@ -0,0 +1,103 @@
{
"result": [
{
"id": "2b1ec1793185213139f22059a165376e",
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "urlfwd0.unit.tests/"
}
}
],
"actions": [
{
"id": "always_use_https"
}
],
"priority": 4,
"status": "active",
"created_on": "2021-06-29T17:14:28.000000Z",
"modified_on": "2021-06-29T17:15:33.000000Z"
},
{
"id": "2b1ec1793185213139f22059a165376f",
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "urlfwd0.unit.tests/*"
}
}
],
"actions": [
{
"id": "forwarding_url",
"value": {
"url": "https://www.unit.tests/",
"status_code": 301
}
}
],
"priority": 3,
"status": "active",
"created_on": "2021-06-29T17:07:12.000000Z",
"modified_on": "2021-06-29T17:15:12.000000Z"
},
{
"id": "2b1ec1793185213139f22059a165377e",
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "urlfwd1.unit.tests/*"
}
}
],
"actions": [
{
"id": "forwarding_url",
"value": {
"url": "https://www.unit.tests/",
"status_code": 302
}
}
],
"priority": 2,
"status": "active",
"created_on": "2021-06-28T22:42:27.000000Z",
"modified_on": "2021-06-28T22:43:13.000000Z"
},
{
"id": "2a9140b17ffb0e6aed826049eec970b8",
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "urlfwd2.unit.tests/*"
}
}
],
"actions": [
{
"id": "forwarding_url",
"value": {
"url": "https://www.unit.tests/",
"status_code": 301
}
}
],
"priority": 1,
"status": "active",
"created_on": "2021-06-25T20:10:50.000000Z",
"modified_on": "2021-06-28T22:38:10.000000Z"
}
],
"success": true,
"errors": [],
"messages": []
}
+56 -37
View File
@@ -64,6 +64,62 @@
"roundRobinFailover": [],
"pools": [],
"poolsDetail": []
}, {
"id": 1898527,
"type": "SRV",
"recordType": "srv",
"name": "_imap._tcp",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149714387,
"value": [{
"value": ".",
"priority": 0,
"weight": 0,
"port": 0,
"disableFlag": false
}],
"roundRobin": [{
"value": ".",
"priority": 0,
"weight": 0,
"port": 0,
"disableFlag": false
}]
}, {
"id": 1898528,
"type": "SRV",
"recordType": "srv",
"name": "_pop3._tcp",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 600,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565149714387,
"value": [{
"value": ".",
"priority": 0,
"weight": 0,
"port": 0,
"disableFlag": false
}],
"roundRobin": [{
"value": ".",
"priority": 0,
"weight": 0,
"port": 0,
"disableFlag": false
}]
}, {
"id": 1808527,
"type": "SRV",
@@ -523,43 +579,6 @@
"roundRobinFailover": [],
"pools": [],
"poolsDetail": []
}, {
"id": 1808603,
"type": "ANAME",
"recordType": "aname",
"name": "sub",
"recordOption": "roundRobin",
"noAnswer": false,
"note": "",
"ttl": 1800,
"gtdRegion": 1,
"parentId": 123123,
"parent": "domain",
"source": "Domain",
"modifiedTs": 1565153387855,
"value": [{
"value": "aname.unit.tests.",
"disableFlag": false
}],
"roundRobin": [{
"value": "aname.unit.tests.",
"disableFlag": false
}],
"geolocation": null,
"recordFailover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"failover": {
"disabled": false,
"failoverType": 1,
"failoverTypeStr": "Normal (always lowest level)",
"values": []
},
"pools": [],
"poolsDetail": []
}, {
"id": 1808520,
"type": "A",
+22
View File
@@ -76,6 +76,28 @@
"weight": null,
"flags": null,
"tag": null
}, {
"id": 11189896,
"type": "SRV",
"name": "_imap._tcp",
"data": ".",
"priority": 0,
"port": 0,
"ttl": 600,
"weight": 0,
"flags": null,
"tag": null
}, {
"id": 11189897,
"type": "SRV",
"name": "_pop3._tcp",
"data": ".",
"priority": 0,
"port": 0,
"ttl": 600,
"weight": 0,
"flags": null,
"tag": null
}],
"links": {
"pages": {
-14
View File
@@ -320,20 +320,6 @@
"name": "",
"value": "aname.unit.tests.",
"id": 11189895,
"type": "ANAME"
}, {
"failover": false,
"monitor": false,
"sourceId": 123123,
"dynamicDns": false,
"failed": false,
"gtdLocation": "DEFAULT",
"hardLink": false,
"ttl": 1800,
"source": 1,
"name": "sub",
"value": "aname",
"id": 11189896,
"type": "ANAME"
}, {
"failover": false,
+24 -2
View File
@@ -264,10 +264,32 @@
"rdata": "v=DKIM1;k=rsa;s=email;h=sha256;p=A\/kinda+of\/long\/string+with+numb3rs",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340025",
"domain": "unit.tests",
"host": "_imap._tcp",
"ttl": "600",
"prio": "0",
"type": "SRV",
"rdata": "0 0 0 .",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
},
{
"id": "12340026",
"domain": "unit.tests",
"host": "_pop3._tcp",
"ttl": "600",
"prio": "0",
"type": "SRV",
"rdata": "0 0 0 .",
"geozone_id": "0",
"last_mod": "2020-01-01 01:01:01"
}
],
"count": 24,
"total": 24,
"count": 26,
"total": 26,
"start": 0,
"max": 1000,
"status": 200
+18 -2
View File
@@ -9,6 +9,22 @@
"name": "_srv._tcp.unit.tests",
"ttl": 600
},
{
"rdata": [
"0 0 0 ."
],
"type": "SRV",
"name": "_imap._tcp.unit.tests",
"ttl": 600
},
{
"rdata": [
"0 0 0 ."
],
"type": "SRV",
"name": "_pop3._tcp.unit.tests",
"ttl": 600
},
{
"rdata": [
"2601:644:500:e210:62f8:1dff:feb8:947a"
@@ -151,7 +167,7 @@
}
],
"metadata": {
"totalElements": 16,
"totalElements": 18,
"showAll": true
}
}
}
+154
View File
@@ -0,0 +1,154 @@
[
{
"rrset_type": "A",
"rrset_ttl": 300,
"rrset_name": "@",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/%40/A",
"rrset_values": [
"1.2.3.4",
"1.2.3.5"
]
},
{
"rrset_type": "CAA",
"rrset_ttl": 3600,
"rrset_name": "@",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/%40/CAA",
"rrset_values": [
"0 issue \"ca.unit.tests\""
]
},
{
"rrset_type": "SSHFP",
"rrset_ttl": 3600,
"rrset_name": "@",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/%40/SSHFP",
"rrset_values": [
"1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49",
"1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73"
]
},
{
"rrset_type": "AAAA",
"rrset_ttl": 600,
"rrset_name": "aaaa",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/aaaa/AAAA",
"rrset_values": [
"2601:644:500:e210:62f8:1dff:feb8:947a"
]
},
{
"rrset_type": "CNAME",
"rrset_ttl": 300,
"rrset_name": "cname",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/cname/CNAME",
"rrset_values": [
"unit.tests."
]
},
{
"rrset_type": "DNAME",
"rrset_ttl": 300,
"rrset_name": "dname",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/dname/DNAME",
"rrset_values": [
"unit.tests."
]
},
{
"rrset_type": "CNAME",
"rrset_ttl": 3600,
"rrset_name": "excluded",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/excluded/CNAME",
"rrset_values": [
"unit.tests."
]
},
{
"rrset_type": "MX",
"rrset_ttl": 300,
"rrset_name": "mx",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/mx/MX",
"rrset_values": [
"10 smtp-4.unit.tests.",
"20 smtp-2.unit.tests.",
"30 smtp-3.unit.tests.",
"40 smtp-1.unit.tests."
]
},
{
"rrset_type": "PTR",
"rrset_ttl": 300,
"rrset_name": "ptr",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/ptr/PTR",
"rrset_values": [
"foo.bar.com."
]
},
{
"rrset_type": "SPF",
"rrset_ttl": 600,
"rrset_name": "spf",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/spf/SPF",
"rrset_values": [
"\"v=spf1 ip4:192.168.0.1/16-all\""
]
},
{
"rrset_type": "TXT",
"rrset_ttl": 600,
"rrset_name": "txt",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/txt/TXT",
"rrset_values": [
"\"Bah bah black sheep\"",
"\"have you any wool.\"",
"\"v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs\""
]
},
{
"rrset_type": "A",
"rrset_ttl": 300,
"rrset_name": "www",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/www/A",
"rrset_values": [
"2.2.3.6"
]
},
{
"rrset_type": "A",
"rrset_ttl": 300,
"rrset_name": "www.sub",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/www.sub/A",
"rrset_values": [
"2.2.3.6"
]
},
{
"rrset_type": "SRV",
"rrset_ttl": 600,
"rrset_name": "_imap._tcp",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_imap._tcp/SRV",
"rrset_values": [
"0 0 0 ."
]
},
{
"rrset_type": "SRV",
"rrset_ttl": 600,
"rrset_name": "_pop3._tcp",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_pop3._tcp/SRV",
"rrset_values": [
"0 0 0 ."
]
},
{
"rrset_type": "SRV",
"rrset_ttl": 600,
"rrset_name": "_srv._tcp",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_srv._tcp/SRV",
"rrset_values": [
"10 20 30 foo-1.unit.tests.",
"12 20 30 foo-2.unit.tests."
]
}
]
+111
View File
@@ -0,0 +1,111 @@
[
{
"rrset_type": "A",
"rrset_ttl": 10800,
"rrset_name": "@",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/%40/A",
"rrset_values": [
"217.70.184.38"
]
},
{
"rrset_type": "MX",
"rrset_ttl": 10800,
"rrset_name": "@",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/%40/MX",
"rrset_values": [
"10 spool.mail.gandi.net.",
"50 fb.mail.gandi.net."
]
},
{
"rrset_type": "TXT",
"rrset_ttl": 10800,
"rrset_name": "@",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/%40/TXT",
"rrset_values": [
"\"v=spf1 include:_mailcust.gandi.net ?all\""
]
},
{
"rrset_type": "CNAME",
"rrset_ttl": 10800,
"rrset_name": "webmail",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/webmail/CNAME",
"rrset_values": [
"webmail.gandi.net."
]
},
{
"rrset_type": "CNAME",
"rrset_ttl": 10800,
"rrset_name": "www",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/www/CNAME",
"rrset_values": [
"webredir.vip.gandi.net."
]
},
{
"rrset_type": "SRV",
"rrset_ttl": 10800,
"rrset_name": "_imap._tcp",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_imap._tcp/SRV",
"rrset_values": [
"0 0 0 ."
]
},
{
"rrset_type": "SRV",
"rrset_ttl": 10800,
"rrset_name": "_imaps._tcp",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_imaps._tcp/SRV",
"rrset_values": [
"0 1 993 mail.gandi.net."
]
},
{
"rrset_type": "SRV",
"rrset_ttl": 10800,
"rrset_name": "_pop3._tcp",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_pop3._tcp/SRV",
"rrset_values": [
"0 0 0 ."
]
},
{
"rrset_type": "SRV",
"rrset_ttl": 10800,
"rrset_name": "_pop3s._tcp",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_pop3s._tcp/SRV",
"rrset_values": [
"10 1 995 mail.gandi.net."
]
},
{
"rrset_type": "SRV",
"rrset_ttl": 10800,
"rrset_name": "_submission._tcp",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/_submission._tcp/SRV",
"rrset_values": [
"0 1 465 mail.gandi.net."
]
},
{
"rrset_type": "CDS",
"rrset_ttl": 10800,
"rrset_name": "sub",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/sub/CDS",
"rrset_values": [
"32128 13 1 6823D9BB1B03DF714DD0EB163E20B341C96D18C0"
]
},
{
"rrset_type": "CNAME",
"rrset_ttl": 10800,
"rrset_name": "relative",
"rrset_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records/relative/CNAME",
"rrset_values": [
"target"
]
}
]
+7
View File
@@ -0,0 +1,7 @@
{
"domain_keys_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/keys",
"fqdn": "unit.tests",
"automatic_snapshots": true,
"domain_records_href": "https://api.gandi.net/v5/livedns/domains/unit.tests/records",
"domain_href": "https://api.gandi.net/v5/livedns/domains/unit.tests"
}
+245
View File
@@ -0,0 +1,245 @@
{
"rrsets": [
{
"name": "unit.tests",
"type": "A",
"ttl": 300,
"resource_records": [
{
"content": [
"1.2.3.4"
]
},
{
"content": [
"1.2.3.5"
]
}
]
},
{
"name": "unit.tests",
"type": "NS",
"ttl": 300,
"resource_records": [
{
"content": [
"ns2.gcdn.services"
]
},
{
"content": [
"ns1.gcorelabs.net"
]
}
]
},
{
"name": "_imap._tcp",
"type": "SRV",
"ttl": 600,
"resource_records": [
{
"content": [
0,
0,
0,
"."
]
}
]
},
{
"name": "_pop3._tcp",
"type": "SRV",
"ttl": 600,
"resource_records": [
{
"content": [
0,
0,
0,
"."
]
}
]
},
{
"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": "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": "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"
]
}
]
}
]
}
+428
View File
@@ -0,0 +1,428 @@
{
"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"
]
},
{
"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"
]
}
]
},
{
"name": "geo-A-single.unit.tests.",
"type": "A",
"ttl": 300,
"filters": [
{
"type": "geodns"
},
{
"limit": 1,
"strict": false,
"type": "default"
},
{
"limit": 1,
"type": "first_n"
}
],
"resource_records": [
{
"content": [
"7.7.7.7"
],
"meta": {
"countries": [
"RU"
]
}
},
{
"content": [
"8.8.8.8"
],
"meta": {
"countries": [
"RU"
]
}
},
{
"content": [
"9.9.9.9"
],
"meta": {
"continents": [
"EU"
]
}
},
{
"content": [
"10.10.10.10"
],
"meta": {
"default": true
}
}
]
},
{
"name": "geo-no-def.unit.tests.",
"type": "A",
"ttl": 300,
"filters": [
{
"type": "geodns"
},
{
"limit": 1,
"strict": false,
"type": "default"
},
{
"limit": 1,
"type": "first_n"
}
],
"resource_records": [
{
"content": [
"7.7.7.7"
],
"meta": {
"countries": [
"RU"
]
}
}
]
},
{
"name": "geo-CNAME.unit.tests.",
"type": "CNAME",
"ttl": 300,
"filters": [
{
"type": "geodns"
},
{
"limit": 1,
"strict": false,
"type": "default"
},
{
"limit": 1,
"type": "first_n"
}
],
"resource_records": [
{
"content": [
"ru-1.unit.tests"
],
"meta": {
"countries": [
"RU"
]
}
},
{
"content": [
"ru-2.unit.tests"
],
"meta": {
"countries": [
"RU"
]
}
},
{
"content": [
"eu.unit.tests"
],
"meta": {
"continents": [
"EU"
]
}
},
{
"content": [
"any.unit.tests."
],
"meta": {
"default": true
}
}
]
},
{
"name": "geo-ignore-len-filters.unit.tests.",
"type": "A",
"ttl": 300,
"filters": [
{
"limit": 1,
"type": "first_n"
},
{
"limit": 1,
"strict": false,
"type": "default"
}
],
"resource_records": [
{
"content": [
"7.7.7.7"
]
}
]
},
{
"name": "geo-ignore-types.unit.tests.",
"type": "A",
"ttl": 300,
"filters": [
{
"type": "geodistance"
},
{
"limit": 1,
"type": "first_n"
},
{
"limit": 1,
"strict": false,
"type": "default"
}
],
"resource_records": [
{
"content": [
"7.7.7.7"
]
}
]
},
{
"name": "geo-ignore-limits.unit.tests.",
"type": "A",
"ttl": 300,
"filters": [
{
"type": "geodns"
},
{
"limit": 2,
"strict": false,
"type": "default"
},
{
"limit": 1,
"type": "first_n"
}
],
"resource_records": [
{
"content": [
"7.7.7.7"
]
}
]
}
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"id": 27757,
"name": "unit.test",
"nx_ttl": 300,
"retry": 5400,
"refresh": 0,
"expiry": 1209600,
"contact": "[email protected]",
"serial": 1614752868,
"primary_server": "ns1.gcorelabs.net",
"records": [
{
"id": 12419,
"name": "unit.test",
"type": "ns",
"ttl": 300,
"short_answers": [
"[ns2.gcdn.services]",
"[ns1.gcorelabs.net]"
]
}
],
"dns_servers": [
"ns1.gcorelabs.net",
"ns2.gcdn.services"
]
}
+223
View File
@@ -0,0 +1,223 @@
{
"records": [
{
"id": "SOA",
"type": "SOA",
"name": "@",
"value": "hydrogen.ns.hetzner.com. dns.hetzner.com. 1 86400 10800 3600000 3600",
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "NS:sub:0",
"type": "NS",
"name": "sub",
"value": "6.2.3.4",
"ttl": 3600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "NS:sub:1",
"type": "NS",
"name": "sub",
"value": "7.2.3.4",
"ttl": 3600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "SRV:_srv._tcp:0",
"type": "SRV",
"name": "_srv._tcp",
"value": "10 20 30 foo-1.unit.tests",
"ttl": 600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "SRV:_srv._tcp:1",
"type": "SRV",
"name": "_srv._tcp",
"value": "12 20 30 foo-2.unit.tests",
"ttl": 600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "TXT:txt:0",
"type": "TXT",
"name": "txt",
"value": "\"Bah bah black sheep\"",
"ttl": 600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "TXT:txt:1",
"type": "TXT",
"name": "txt",
"value": "\"have you any wool.\"",
"ttl": 600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "A:@:0",
"type": "A",
"name": "@",
"value": "1.2.3.4",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "A:@:1",
"type": "A",
"name": "@",
"value": "1.2.3.5",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "A:www:0",
"type": "A",
"name": "www",
"value": "2.2.3.6",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "MX:mx:0",
"type": "MX",
"name": "mx",
"value": "10 smtp-4.unit.tests",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "MX:mx:1",
"type": "MX",
"name": "mx",
"value": "20 smtp-2.unit.tests",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "MX:mx:2",
"type": "MX",
"name": "mx",
"value": "30 smtp-3.unit.tests",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "MX:mx:3",
"type": "MX",
"name": "mx",
"value": "40 smtp-1.unit.tests",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "AAAA:aaaa:0",
"type": "AAAA",
"name": "aaaa",
"value": "2601:644:500:e210:62f8:1dff:feb8:947a",
"ttl": 600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "CNAME:cname:0",
"type": "CNAME",
"name": "cname",
"value": "unit.tests",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "A:www.sub:0",
"type": "A",
"name": "www.sub",
"value": "2.2.3.6",
"ttl": 300,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "TXT:txt:2",
"type": "TXT",
"name": "txt",
"value": "v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string+with+numb3rs",
"ttl": 600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "CAA:@:0",
"type": "CAA",
"name": "@",
"value": "0 issue \"ca.unit.tests\"",
"ttl": 3600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "CNAME:included:0",
"type": "CNAME",
"name": "included",
"value": "unit.tests",
"ttl": 3600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "SRV:_imap._tcp:0",
"type": "SRV",
"name": "_imap._tcp",
"value": "0 0 0 .",
"ttl": 600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
},
{
"id": "SRV:_pop3._tcp:0",
"type": "SRV",
"name": "_pop3._tcp",
"value": "0 0 0 .",
"ttl": 600,
"zone_id": "unit.tests",
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"modified": "0000-00-00 00:00:00.000 +0000 UTC"
}
]
}
+43
View File
@@ -0,0 +1,43 @@
{
"zones": [
{
"id": "unit.tests",
"name": "unit.tests",
"ttl": 3600,
"registrar": "",
"legacy_dns_host": "",
"legacy_ns": [],
"ns": [],
"created": "0000-00-00 00:00:00.000 +0000 UTC",
"verified": "",
"modified": "0000-00-00 00:00:00.000 +0000 UTC",
"project": "",
"owner": "",
"permission": "",
"zone_type": {
"id": "",
"name": "",
"description": "",
"prices": null
},
"status": "verified",
"paused": false,
"is_secondary_dns": false,
"txt_verification": {
"name": "",
"token": ""
},
"records_count": null
}
],
"meta": {
"pagination": {
"page": 1,
"per_page": 100,
"previous_page": 1,
"next_page": 1,
"last_page": 1,
"total_entries": 1
}
}
}
+2
View File
@@ -5,6 +5,8 @@
@ 3600 SSHFP 1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73
@ 3600 SSHFP 1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49
@ 3600 CAA 0 issue ca.unit.tests
_imap._tcp 600 SRV 0 0 0 .
_pop3._tcp 600 SRV 0 0 0 .
_srv._tcp 600 SRV 10 20 30 foo-1.unit.tests.
_srv._tcp 600 SRV 12 20 30 foo-2.unit.tests.
aaaa 600 AAAA 2601:644:500:e210:62f8:1dff:feb8:947a
+40
View File
@@ -32,6 +32,22 @@
"ttl": 300,
"type": "MX"
},
{
"comments": [],
"name": "loc.unit.tests.",
"records": [
{
"content": "31 58 52.100 S 115 49 11.700 E 20.00m 10.00m 10.00m 2.00m",
"disabled": false
},
{
"content": "53 13 10.000 N 2 18 26.000 W 20.00m 10.00m 1000.00m 2.00m",
"disabled": false
}
],
"ttl": 300,
"type": "LOC"
},
{
"comments": [],
"name": "sub.unit.tests.",
@@ -59,6 +75,30 @@
"ttl": 300,
"type": "A"
},
{
"comments": [],
"name": "_imap._tcp.unit.tests.",
"records": [
{
"content": "0 0 0 .",
"disabled": false
}
],
"ttl": 600,
"type": "SRV"
},
{
"comments": [],
"name": "_pop3._tcp.unit.tests.",
"records": [
{
"content": "0 0 0 .",
"disabled": false
}
],
"ttl": 600,
"type": "SRV"
},
{
"comments": [],
"name": "_srv._tcp.unit.tests.",
+1 -1
View File
@@ -87,7 +87,7 @@
}
],
"resultInfo": {
"totalCount": 12,
"totalCount": 13,
"offset": 0,
"returnedCount": 10
}
+19 -2
View File
@@ -24,11 +24,28 @@
"order": "FIXED",
"description": "octodns1.test."
}
},
{
"ownerName": "octodns1.test.",
"rrtype": "APEXALIAS (65282)",
"ttl": 3600,
"rdata": [
"www.octodns1.test."
]
},
{
"ownerName": "host1.octodns1.test.",
"rrtype": "RRSET (70)",
"ttl": 3600,
"rdata": [
"E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855"
]
}
],
"resultInfo": {
"totalCount": 12,
"totalCount": 13,
"offset": 10,
"returnedCount": 2
"returnedCount": 3
}
}
+1 -1
View File
@@ -19,7 +19,7 @@
"dnssecStatus": "UNSIGNED",
"status": "ACTIVE",
"owner": "phelpstest",
"resourceRecordCount": 5,
"resourceRecordCount": 6,
"lastModifiedDateTime": "2020-06-19T01:05Z"
}
},
+30
View File
@@ -7,6 +7,10 @@ from __future__ import absolute_import, division, print_function, \
from shutil import rmtree
from tempfile import mkdtemp
from logging import getLogger
from octodns.processor.base import BaseProcessor
from octodns.provider.base import BaseProvider
class SimpleSource(object):
@@ -90,3 +94,29 @@ class TemporaryDirectory(object):
rmtree(self.dirname)
else:
raise Exception(self.dirname)
class WantsConfigProcessor(BaseProcessor):
def __init__(self, name, some_config):
super(WantsConfigProcessor, self).__init__(name)
class PlannableProvider(BaseProvider):
log = getLogger('PlannableProvider')
SUPPORTS_GEO = False
SUPPORTS_DYNAMIC = False
SUPPORTS = set(('A',))
def __init__(self, *args, **kwargs):
super(PlannableProvider, self).__init__(*args, **kwargs)
def populate(self, zone, source=False, target=False, lenient=False):
pass
def supports(self, record):
return True
def __repr__(self):
return self.__class__.__name__
+219 -17
View File
@@ -8,16 +8,19 @@ from __future__ import absolute_import, division, print_function, \
from os import environ
from os.path import dirname, join
from six import text_type
from unittest import TestCase
from octodns.record import Record
from octodns.manager import _AggregateTarget, MainThreadExecutor, Manager, \
ManagerException
from octodns.processor.base import BaseProcessor
from octodns.record import Create, Delete, Record
from octodns.yaml import safe_load
from octodns.zone import Zone
from mock import MagicMock, patch
from unittest import TestCase
from helpers import DynamicProvider, GeoProvider, NoSshFpProvider, \
SimpleProvider, TemporaryDirectory
PlannableProvider, SimpleProvider, TemporaryDirectory
config_dir = join(dirname(__file__), 'config')
@@ -118,12 +121,12 @@ class TestManager(TestCase):
environ['YAML_TMP_DIR'] = tmpdir.dirname
tc = Manager(get_config_filename('simple.yaml')) \
.sync(dry_run=False)
self.assertEquals(21, tc)
self.assertEquals(26, tc)
# try with just one of the zones
tc = Manager(get_config_filename('simple.yaml')) \
.sync(dry_run=False, eligible_zones=['unit.tests.'])
self.assertEquals(15, tc)
self.assertEquals(20, tc)
# the subzone, with 2 targets
tc = Manager(get_config_filename('simple.yaml')) \
@@ -138,18 +141,18 @@ class TestManager(TestCase):
# Again with force
tc = Manager(get_config_filename('simple.yaml')) \
.sync(dry_run=False, force=True)
self.assertEquals(21, tc)
self.assertEquals(26, tc)
# Again with max_workers = 1
tc = Manager(get_config_filename('simple.yaml'), max_workers=1) \
.sync(dry_run=False, force=True)
self.assertEquals(21, tc)
self.assertEquals(26, tc)
# Include meta
tc = Manager(get_config_filename('simple.yaml'), max_workers=1,
include_meta=True) \
.sync(dry_run=False, force=True)
self.assertEquals(25, tc)
self.assertEquals(30, tc)
def test_eligible_sources(self):
with TemporaryDirectory() as tmpdir:
@@ -167,6 +170,38 @@ class TestManager(TestCase):
.sync(eligible_targets=['foo'])
self.assertEquals(0, tc)
def test_aliases(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
# Alias zones with a valid target.
tc = Manager(get_config_filename('simple-alias-zone.yaml')) \
.sync()
self.assertEquals(0, tc)
# Alias zone with an invalid target.
with self.assertRaises(ManagerException) as ctx:
tc = Manager(get_config_filename('unknown-source-zone.yaml')) \
.sync()
self.assertEquals('Invalid alias zone alias.tests.: source zone '
'does-not-exists.tests. does not exist',
text_type(ctx.exception))
# Alias zone that points to another alias zone.
with self.assertRaises(ManagerException) as ctx:
tc = Manager(get_config_filename('alias-zone-loop.yaml')) \
.sync()
self.assertEquals('Invalid alias zone alias-loop.tests.: source '
'zone alias.tests. is an alias zone',
text_type(ctx.exception))
# Sync an alias without the zone it refers to
with self.assertRaises(ManagerException) as ctx:
tc = Manager(get_config_filename('simple-alias-zone.yaml')) \
.sync(eligible_zones=["alias.tests."])
self.assertEquals('Zone alias.tests. cannot be sync without zone '
'unit.tests. sinced it is aliased',
text_type(ctx.exception))
def test_compare(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
@@ -183,13 +218,13 @@ class TestManager(TestCase):
fh.write('---\n{}')
changes = manager.compare(['in'], ['dump'], 'unit.tests.')
self.assertEquals(15, len(changes))
self.assertEquals(20, len(changes))
# Compound sources with varying support
changes = manager.compare(['in', 'nosshfp'],
['dump'],
'unit.tests.')
self.assertEquals(14, len(changes))
self.assertEquals(19, len(changes))
with self.assertRaises(ManagerException) as ctx:
manager.compare(['nope'], ['dump'], 'unit.tests.')
@@ -286,6 +321,41 @@ class TestManager(TestCase):
.validate_configs()
self.assertTrue('unknown source' in text_type(ctx.exception))
# Alias zone using an invalid source zone.
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('unknown-source-zone.yaml')) \
.validate_configs()
self.assertTrue('does not exist' in
text_type(ctx.exception))
# Alias zone that points to another alias zone.
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('alias-zone-loop.yaml')) \
.validate_configs()
self.assertTrue('is an alias zone' in
text_type(ctx.exception))
# Valid config file using an alias zone.
Manager(get_config_filename('simple-alias-zone.yaml')) \
.validate_configs()
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('unknown-processor.yaml')) \
.validate_configs()
self.assertTrue('unknown processor' in text_type(ctx.exception))
def test_get_zone(self):
Manager(get_config_filename('simple.yaml')).get_zone('unit.tests.')
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('simple.yaml')).get_zone('unit.tests')
self.assertTrue('missing ending dot' in text_type(ctx.exception))
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('simple.yaml')) \
.get_zone('unknown-zone.tests.')
self.assertTrue('Unknown zone name' in text_type(ctx.exception))
def test_populate_lenient_fallback(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
@@ -294,20 +364,152 @@ class TestManager(TestCase):
class NoLenient(SimpleProvider):
def populate(self, zone, source=False):
def populate(self, zone):
pass
# This should be ok, we'll fall back to not passing it
manager._populate_and_plan('unit.tests.', [NoLenient()], [])
manager._populate_and_plan('unit.tests.', [], [NoLenient()], [])
class NoZone(SimpleProvider):
class OtherType(SimpleProvider):
def populate(self, lenient=False):
pass
def populate(self, zone, lenient=False):
raise TypeError('something else')
# This will blow up, we don't fallback for source
with self.assertRaises(TypeError):
manager._populate_and_plan('unit.tests.', [NoZone()], [])
with self.assertRaises(TypeError) as ctx:
manager._populate_and_plan('unit.tests.', [], [OtherType()],
[])
self.assertEquals('something else', text_type(ctx.exception))
def test_plan_processors_fallback(self):
with TemporaryDirectory() as tmpdir:
environ['YAML_TMP_DIR'] = tmpdir.dirname
# Only allow a target that doesn't exist
manager = Manager(get_config_filename('simple.yaml'))
class NoProcessors(SimpleProvider):
def plan(self, zone):
pass
# This should be ok, we'll fall back to not passing it
manager._populate_and_plan('unit.tests.', [], [],
[NoProcessors()])
class OtherType(SimpleProvider):
def plan(self, zone, processors):
raise TypeError('something else')
# This will blow up, we don't fallback for source
with self.assertRaises(TypeError) as ctx:
manager._populate_and_plan('unit.tests.', [], [],
[OtherType()])
self.assertEquals('something else', text_type(ctx.exception))
@patch('octodns.manager.Manager._get_named_class')
def test_sync_passes_file_handle(self, mock):
plan_output_mock = MagicMock()
plan_output_class_mock = MagicMock()
plan_output_class_mock.return_value = plan_output_mock
mock.return_value = plan_output_class_mock
fh_mock = MagicMock()
Manager(get_config_filename('plan-output-filehandle.yaml')
).sync(plan_output_fh=fh_mock)
# Since we only care about the fh kwarg, and different _PlanOutputs are
# are free to require arbitrary kwargs anyway, we concern ourselves
# with checking the value of fh only.
plan_output_mock.run.assert_called()
_, kwargs = plan_output_mock.run.call_args
self.assertEqual(fh_mock, kwargs.get('fh'))
def test_processor_config(self):
# Smoke test loading a valid config
manager = Manager(get_config_filename('processors.yaml'))
self.assertEquals(['noop'], list(manager.processors.keys()))
# This zone specifies a valid processor
manager.sync(['unit.tests.'])
with self.assertRaises(ManagerException) as ctx:
# This zone specifies a non-existant processor
manager.sync(['bad.unit.tests.'])
self.assertTrue('Zone bad.unit.tests., unknown processor: '
'doesnt-exist' in text_type(ctx.exception))
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('processors-missing-class.yaml'))
self.assertTrue('Processor no-class is missing class' in
text_type(ctx.exception))
with self.assertRaises(ManagerException) as ctx:
Manager(get_config_filename('processors-wants-config.yaml'))
self.assertTrue('Incorrect processor config for wants-config' in
text_type(ctx.exception))
def test_processors(self):
manager = Manager(get_config_filename('simple.yaml'))
targets = [PlannableProvider('prov')]
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
# muck with sources
class MockProcessor(BaseProcessor):
def process_source_zone(self, zone, sources):
zone = zone.copy()
zone.add_record(record)
return zone
mock = MockProcessor('mock')
plans, zone = manager._populate_and_plan('unit.tests.', [mock], [],
targets)
# Our mock was called and added the record
self.assertEquals(record, list(zone.records)[0])
# We got a create for the thing added to the expected state (source)
self.assertIsInstance(plans[0][1].changes[0], Create)
# muck with targets
class MockProcessor(BaseProcessor):
def process_target_zone(self, zone, target):
zone = zone.copy()
zone.add_record(record)
return zone
mock = MockProcessor('mock')
plans, zone = manager._populate_and_plan('unit.tests.', [mock], [],
targets)
# No record added since it's target this time
self.assertFalse(zone.records)
# We got a delete for the thing added to the existing state (target)
self.assertIsInstance(plans[0][1].changes[0], Delete)
# muck with plans
class MockProcessor(BaseProcessor):
def process_target_zone(self, zone, target):
zone = zone.copy()
zone.add_record(record)
return zone
def process_plan(self, plans, sources, target):
# get rid of the change
plans.changes.pop(0)
mock = MockProcessor('mock')
plans, zone = manager._populate_and_plan('unit.tests.', [mock], [],
targets)
# We planned a delete again, but this time removed it from the plan, so
# no plans
self.assertFalse(plans)
class TestMainThreadExecutor(TestCase):
+102
View File
@@ -0,0 +1,102 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from unittest import TestCase
from octodns.processor.acme import AcmeMangingProcessor
from octodns.record import Record
from octodns.zone import Zone
zone = Zone('unit.tests.', [])
records = {
'root-unowned': Record.new(zone, '_acme-challenge', {
'ttl': 30,
'type': 'TXT',
'value': 'magic bit',
}),
'sub-unowned': Record.new(zone, '_acme-challenge.sub-unowned', {
'ttl': 30,
'type': 'TXT',
'value': 'magic bit',
}),
'not-txt': Record.new(zone, '_acme-challenge.not-txt', {
'ttl': 30,
'type': 'AAAA',
'value': '::1',
}),
'not-acme': Record.new(zone, 'not-acme', {
'ttl': 30,
'type': 'TXT',
'value': 'Hello World!',
}),
'managed': Record.new(zone, '_acme-challenge.managed', {
'ttl': 30,
'type': 'TXT',
'value': 'magic bit',
}),
'owned': Record.new(zone, '_acme-challenge.owned', {
'ttl': 30,
'type': 'TXT',
'values': ['*octoDNS*', 'magic bit'],
}),
'going-away': Record.new(zone, '_acme-challenge.going-away', {
'ttl': 30,
'type': 'TXT',
'values': ['*octoDNS*', 'magic bit'],
}),
}
class TestAcmeMangingProcessor(TestCase):
def test_process_zones(self):
acme = AcmeMangingProcessor('acme')
source = Zone(zone.name, [])
# Unrelated stuff that should be untouched
source.add_record(records['not-txt'])
source.add_record(records['not-acme'])
# A managed acme that will have ownership value added
source.add_record(records['managed'])
got = acme.process_source_zone(source)
self.assertEquals([
'_acme-challenge.managed',
'_acme-challenge.not-txt',
'not-acme',
], sorted([r.name for r in got.records]))
managed = None
for record in got.records:
if record.name.endswith('managed'):
managed = record
break
self.assertTrue(managed)
# Ownership was marked with an extra value
self.assertEquals(['*octoDNS*', 'magic bit'], record.values)
existing = Zone(zone.name, [])
# Unrelated stuff that should be untouched
existing.add_record(records['not-txt'])
existing.add_record(records['not-acme'])
# Stuff that will be ignored
existing.add_record(records['root-unowned'])
existing.add_record(records['sub-unowned'])
# A managed acme that needs ownership value added
existing.add_record(records['managed'])
# A managed acme that has ownershp managed
existing.add_record(records['owned'])
# A managed acme that needs to go away
existing.add_record(records['going-away'])
got = acme.process_target_zone(existing)
self.assertEquals([
'_acme-challenge.going-away',
'_acme-challenge.managed',
'_acme-challenge.not-txt',
'_acme-challenge.owned',
'not-acme'
], sorted([r.name for r in got.records]))
+90
View File
@@ -0,0 +1,90 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from unittest import TestCase
from octodns.processor.filter import TypeAllowlistFilter, TypeRejectlistFilter
from octodns.record import Record
from octodns.zone import Zone
zone = Zone('unit.tests.', [])
for record in [
Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
}),
Record.new(zone, 'aaaa', {
'ttl': 30,
'type': 'AAAA',
'value': '::1',
}),
Record.new(zone, 'txt', {
'ttl': 30,
'type': 'TXT',
'value': 'Hello World!',
}),
Record.new(zone, 'a2', {
'ttl': 30,
'type': 'A',
'value': '2.3.4.5',
}),
Record.new(zone, 'txt2', {
'ttl': 30,
'type': 'TXT',
'value': 'That will do',
}),
]:
zone.add_record(record)
class TestTypeAllowListFilter(TestCase):
def test_basics(self):
filter_a = TypeAllowlistFilter('only-a', set(('A')))
got = filter_a.process_source_zone(zone.copy())
self.assertEquals(['a', 'a2'], sorted([r.name for r in got.records]))
filter_aaaa = TypeAllowlistFilter('only-aaaa', ('AAAA',))
got = filter_aaaa.process_source_zone(zone.copy())
self.assertEquals(['aaaa'], sorted([r.name for r in got.records]))
filter_txt = TypeAllowlistFilter('only-txt', ['TXT'])
got = filter_txt.process_target_zone(zone.copy())
self.assertEquals(['txt', 'txt2'],
sorted([r.name for r in got.records]))
filter_a_aaaa = TypeAllowlistFilter('only-aaaa', set(('A', 'AAAA')))
got = filter_a_aaaa.process_target_zone(zone.copy())
self.assertEquals(['a', 'a2', 'aaaa'],
sorted([r.name for r in got.records]))
class TestTypeRejectListFilter(TestCase):
def test_basics(self):
filter_a = TypeRejectlistFilter('not-a', set(('A')))
got = filter_a.process_source_zone(zone.copy())
self.assertEquals(['aaaa', 'txt', 'txt2'],
sorted([r.name for r in got.records]))
filter_aaaa = TypeRejectlistFilter('not-aaaa', ('AAAA',))
got = filter_aaaa.process_source_zone(zone.copy())
self.assertEquals(['a', 'a2', 'txt', 'txt2'],
sorted([r.name for r in got.records]))
filter_txt = TypeRejectlistFilter('not-txt', ['TXT'])
got = filter_txt.process_target_zone(zone.copy())
self.assertEquals(['a', 'a2', 'aaaa'],
sorted([r.name for r in got.records]))
filter_a_aaaa = TypeRejectlistFilter('not-a-aaaa', set(('A', 'AAAA')))
got = filter_a_aaaa.process_target_zone(zone.copy())
self.assertEquals(['txt', 'txt2'],
sorted([r.name for r in got.records]))
+146
View File
@@ -0,0 +1,146 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from unittest import TestCase
from octodns.processor.ownership import OwnershipProcessor
from octodns.record import Delete, Record
from octodns.zone import Zone
from helpers import PlannableProvider
zone = Zone('unit.tests.', [])
records = {}
for record in [
Record.new(zone, '', {
'ttl': 30,
'type': 'A',
'values': [
'1.2.3.4',
'5.6.7.8',
],
}),
Record.new(zone, 'the-a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
}),
Record.new(zone, 'the-aaaa', {
'ttl': 30,
'type': 'AAAA',
'value': '::1',
}),
Record.new(zone, 'the-txt', {
'ttl': 30,
'type': 'TXT',
'value': 'Hello World!',
}),
Record.new(zone, '*', {
'ttl': 30,
'type': 'A',
'value': '4.3.2.1',
}),
]:
records[record.name] = record
zone.add_record(record)
class TestOwnershipProcessor(TestCase):
def test_process_source_zone(self):
ownership = OwnershipProcessor('ownership')
got = ownership.process_source_zone(zone.copy())
self.assertEquals([
'',
'*',
'_owner.a',
'_owner.a._wildcard',
'_owner.a.the-a',
'_owner.aaaa.the-aaaa',
'_owner.txt.the-txt',
'the-a',
'the-aaaa',
'the-txt',
], sorted([r.name for r in got.records]))
found = False
for record in got.records:
if record.name.startswith(ownership.txt_name):
self.assertEquals([ownership.txt_value], record.values)
# test _is_ownership while we're in here
self.assertTrue(ownership._is_ownership(record))
found = True
else:
self.assertFalse(ownership._is_ownership(record))
self.assertTrue(found)
def test_process_plan(self):
ownership = OwnershipProcessor('ownership')
provider = PlannableProvider('helper')
# No plan, is a quick noop
self.assertFalse(ownership.process_plan(None))
# Nothing exists create both records and ownership
ownership_added = ownership.process_source_zone(zone.copy())
plan = provider.plan(ownership_added)
self.assertTrue(plan)
# Double the number of records
self.assertEquals(len(records) * 2, len(plan.changes))
# Now process the plan, shouldn't make any changes, we're creating
# everything
got = ownership.process_plan(plan)
self.assertTrue(got)
self.assertEquals(len(records) * 2, len(got.changes))
# Something extra exists and doesn't have ownership TXT, leave it
# alone, we don't own it.
extra_a = Record.new(zone, 'extra-a', {
'ttl': 30,
'type': 'A',
'value': '4.4.4.4',
})
plan.existing.add_record(extra_a)
# If we'd done a "real" plan we'd have a delete for the extra thing.
plan.changes.append(Delete(extra_a))
# Process the plan, shouldn't make any changes since the extra bit is
# something we don't own
got = ownership.process_plan(plan)
self.assertTrue(got)
self.assertEquals(len(records) * 2, len(got.changes))
# Something extra exists and does have an ownership record so we will
# delete it...
copy = Zone('unit.tests.', [])
for record in records.values():
if record.name != 'the-a':
copy.add_record(record)
# New ownership, without the `the-a`
ownership_added = ownership.process_source_zone(copy)
self.assertEquals(len(records) * 2 - 2, len(ownership_added.records))
plan = provider.plan(ownership_added)
# Fake the extra existing by adding the record, its ownership, and the
# two delete changes.
the_a = records['the-a']
plan.existing.add_record(the_a)
name = '{}.a.the-a'.format(ownership.txt_name)
the_a_ownership = Record.new(zone, name, {
'ttl': 30,
'type': 'TXT',
'value': ownership.txt_value,
})
plan.existing.add_record(the_a_ownership)
plan.changes.append(Delete(the_a))
plan.changes.append(Delete(the_a_ownership))
# Finally process the plan, should be a noop and we should get the same
# plan out, meaning the planned deletes were allowed to happen.
got = ownership.process_plan(plan)
self.assertTrue(got)
self.assertEquals(plan, got)
self.assertEquals(len(plan.changes), len(got.changes))
File diff suppressed because it is too large Load Diff
+106 -2
View File
@@ -6,12 +6,15 @@ from __future__ import absolute_import, division, print_function, \
unicode_literals
from logging import getLogger
from mock import MagicMock, call
from six import text_type
from unittest import TestCase
from octodns.record import Create, Delete, Record, Update
from octodns.processor.base import BaseProcessor
from octodns.provider import SupportsException
from octodns.provider.base import BaseProvider
from octodns.provider.plan import Plan, UnsafePlan
from octodns.record import Create, Delete, Record, Update
from octodns.zone import Zone
@@ -20,8 +23,9 @@ class HelperProvider(BaseProvider):
SUPPORTS = set(('A',))
id = 'test'
strict_supports = False
def __init__(self, extra_changes, apply_disabled=False,
def __init__(self, extra_changes=[], apply_disabled=False,
include_change_callback=None):
self.__extra_changes = extra_changes
self.apply_disabled = apply_disabled
@@ -43,6 +47,29 @@ class HelperProvider(BaseProvider):
pass
class TrickyProcessor(BaseProcessor):
def __init__(self, name, add_during_process_target_zone):
super(TrickyProcessor, self).__init__(name)
self.add_during_process_target_zone = add_during_process_target_zone
self.reset()
def reset(self):
self.existing = None
self.target = None
def process_target_zone(self, existing, target):
self.existing = existing
self.target = target
new = existing.copy()
for record in existing.records:
new.add_record(record, replace=True)
for record in self.add_during_process_target_zone:
new.add_record(record, replace=True)
return new
class TestBaseProvider(TestCase):
def test_base_provider(self):
@@ -138,6 +165,45 @@ class TestBaseProvider(TestCase):
self.assertTrue(plan)
self.assertEquals(1, len(plan.changes))
def test_plan_with_processors(self):
zone = Zone('unit.tests.', [])
record = Record.new(zone, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
})
provider = HelperProvider()
# Processor that adds a record to the zone, which planning will then
# delete since it won't know anything about it
tricky = TrickyProcessor('tricky', [record])
plan = provider.plan(zone, processors=[tricky])
self.assertTrue(plan)
self.assertEquals(1, len(plan.changes))
self.assertIsInstance(plan.changes[0], Delete)
# Called processor stored its params
self.assertTrue(tricky.existing)
self.assertEquals(zone.name, tricky.existing.name)
# Chain of processors happen one after the other
other = Record.new(zone, 'b', {
'ttl': 30,
'type': 'A',
'value': '5.6.7.8',
})
# Another processor will add its record, thus 2 deletes
another = TrickyProcessor('tricky', [other])
plan = provider.plan(zone, processors=[tricky, another])
self.assertTrue(plan)
self.assertEquals(2, len(plan.changes))
self.assertIsInstance(plan.changes[0], Delete)
self.assertIsInstance(plan.changes[1], Delete)
# 2nd processor stored its params, and we'll see the record the
# first one added
self.assertTrue(another.existing)
self.assertEquals(zone.name, another.existing.name)
self.assertEquals(1, len(another.existing.records))
def test_apply(self):
ignored = Zone('unit.tests.', [])
@@ -167,6 +233,20 @@ class TestBaseProvider(TestCase):
# We filtered out the only change
self.assertFalse(plan)
def test_process_desired_zone(self):
zone1 = Zone('unit.tests.', [])
record1 = Record.new(zone1, 'ptr', {
'type': 'PTR',
'ttl': 3600,
'values': ['foo.com.', 'bar.com.'],
})
zone1.add_record(record1)
zone2 = HelperProvider('hasptr')._process_desired_zone(zone1)
record2 = list(zone2.records)[0]
self.assertEqual(len(record2.values), 1)
def test_safe_none(self):
# No changes is safe
Plan(None, None, [], True).raise_if_unsafe()
@@ -366,3 +446,27 @@ class TestBaseProvider(TestCase):
delete_pcent_threshold=safe_pcent).raise_if_unsafe()
self.assertTrue('Too many deletes' in text_type(ctx.exception))
def test_supports_warn_or_except(self):
class MinimalProvider(BaseProvider):
SUPPORTS = set()
SUPPORTS_GEO = False
def __init__(self, **kwargs):
self.log = MagicMock()
super(MinimalProvider, self).__init__('minimal', **kwargs)
normal = MinimalProvider(strict_supports=False)
# Should log and not expect
normal.supports_warn_or_except('Hello World!', 'Goodbye')
normal.log.warning.assert_called_once()
normal.log.warning.assert_has_calls([
call('Hello World!; Goodbye')
])
strict = MinimalProvider(strict_supports=True)
# Should log and not expect
with self.assertRaises(SupportsException) as ctx:
strict.supports_warn_or_except('Hello World!', 'Will not see')
self.assertEquals('minimal: Hello World!', text_type(ctx.exception))
strict.log.warning.assert_not_called()
+314 -18
View File
@@ -166,9 +166,15 @@ class TestCloudflareProvider(TestCase):
json={'result': [], 'result_info': {'count': 0,
'per_page': 0}})
base = '{}/234234243423aaabb334342aaa343435'.format(base)
# pagerules/URLFWD
with open('tests/fixtures/cloudflare-pagerules.json') as fh:
mock.get('{}/pagerules?status=active'.format(base),
status_code=200, text=fh.read())
# records
base = '{}/234234243423aaabb334342aaa343435/dns_records' \
.format(base)
base = '{}/dns_records'.format(base)
with open('tests/fixtures/cloudflare-dns_records-'
'page-1.json') as fh:
mock.get('{}?page=1'.format(base), status_code=200,
@@ -177,19 +183,23 @@ class TestCloudflareProvider(TestCase):
'page-2.json') as fh:
mock.get('{}?page=2'.format(base), status_code=200,
text=fh.read())
with open('tests/fixtures/cloudflare-dns_records-'
'page-3.json') as fh:
mock.get('{}?page=3'.format(base), status_code=200,
text=fh.read())
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(13, len(zone.records))
self.assertEquals(19, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
self.assertEquals(4, len(changes))
# re-populating the same zone/records comes out of cache, no calls
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(13, len(again.records))
self.assertEquals(19, len(again.records))
def test_apply(self):
provider = CloudflareProvider('test', 'email', 'token', retry_period=0)
@@ -203,12 +213,12 @@ class TestCloudflareProvider(TestCase):
'id': 42,
}
}, # zone create
] + [None] * 22 # individual record creates
] + [None] * 27 # individual record creates
# non-existent zone, create everything
plan = provider.plan(self.expected)
self.assertEquals(13, len(plan.changes))
self.assertEquals(13, provider.apply(plan))
self.assertEquals(17, len(plan.changes))
self.assertEquals(17, provider.apply(plan))
self.assertFalse(plan.exists)
provider._request.assert_has_calls([
@@ -232,9 +242,31 @@ class TestCloudflareProvider(TestCase):
'name': 'txt.unit.tests',
'ttl': 600
}),
# create at least one pagerules
call('POST', '/zones/42/pagerules', data={
'targets': [
{
'target': 'url',
'constraint': {
'operator': 'matches',
'value': 'urlfwd.unit.tests/'
}
}
],
'actions': [
{
'id': 'forwarding_url',
'value': {
'url': 'http://www.unit.tests',
'status_code': 302
}
}
],
'status': 'active'
}),
], True)
# expected number of total calls
self.assertEquals(23, provider._request.call_count)
self.assertEquals(29, provider._request.call_count)
provider._request.reset_mock()
@@ -307,6 +339,56 @@ class TestCloudflareProvider(TestCase):
"auto_added": False
}
},
{
"id": "2a9140b17ffb0e6aed826049eec970b7",
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "urlfwd.unit.tests/"
}
}
],
"actions": [
{
"id": "forwarding_url",
"value": {
"url": "https://www.unit.tests",
"status_code": 302
}
}
],
"priority": 1,
"status": "active",
"created_on": "2021-06-25T20:10:50.000000Z",
"modified_on": "2021-06-28T22:38:10.000000Z"
},
{
"id": "2a9141b18ffb0e6aed826050eec970b8",
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "urlfwdother.unit.tests/target"
}
}
],
"actions": [
{
"id": "forwarding_url",
"value": {
"url": "https://target.unit.tests",
"status_code": 301
}
}
],
"priority": 2,
"status": "active",
"created_on": "2021-06-25T20:10:50.000000Z",
"modified_on": "2021-06-28T22:38:10.000000Z"
},
])
# we don't care about the POST/create return values
@@ -315,7 +397,7 @@ class TestCloudflareProvider(TestCase):
# Test out the create rate-limit handling, then 9 successes
provider._request.side_effect = [
CloudflareRateLimitError('{}'),
] + ([None] * 3)
] + ([None] * 5)
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, 'nc', {
@@ -328,14 +410,31 @@ class TestCloudflareProvider(TestCase):
'type': 'A',
'value': '3.2.3.4'
}))
wanted.add_record(Record.new(wanted, 'urlfwd', {
'ttl': 300,
'type': 'URLFWD',
'value': {
'path': '/*', # path change
'target': 'https://www.unit.tests/', # target change
'code': 301, # status_code change
'masking': '2',
'query': 0,
}
}))
plan = provider.plan(wanted)
# only see the delete & ttl update, below min-ttl is filtered out
self.assertEquals(2, len(plan.changes))
self.assertEquals(2, provider.apply(plan))
self.assertEquals(4, len(plan.changes))
self.assertEquals(4, provider.apply(plan))
self.assertTrue(plan.exists)
# creates a the new value and then deletes all the old
provider._request.assert_has_calls([
call('DELETE', '/zones/42/'
'pagerules/2a9141b18ffb0e6aed826050eec970b8'),
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
'dns_records/fc12ab34cd5611334422ab3322997653'),
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
'dns_records/fc12ab34cd5611334422ab3322997654'),
call('PUT', '/zones/42/dns_records/'
'fc12ab34cd5611334422ab3322997655', data={
'content': '3.2.3.4',
@@ -344,10 +443,28 @@ class TestCloudflareProvider(TestCase):
'proxied': False,
'ttl': 300
}),
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
'dns_records/fc12ab34cd5611334422ab3322997653'),
call('DELETE', '/zones/ff12ab34cd5611334422ab3322997650/'
'dns_records/fc12ab34cd5611334422ab3322997654')
call('PUT', '/zones/42/pagerules/'
'2a9140b17ffb0e6aed826049eec970b7', data={
'targets': [
{
'target': 'url',
'constraint': {
'operator': 'matches',
'value': 'urlfwd.unit.tests/*'
}
}
],
'actions': [
{
'id': 'forwarding_url',
'value': {
'url': 'https://www.unit.tests/',
'status_code': 301
}
}
],
'status': 'active',
}),
])
def test_update_add_swap(self):
@@ -496,6 +613,56 @@ class TestCloudflareProvider(TestCase):
"auto_added": False
}
},
{
"id": "2a9140b17ffb0e6aed826049eec974b7",
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "urlfwd1.unit.tests/"
}
}
],
"actions": [
{
"id": "forwarding_url",
"value": {
"url": "https://www.unit.tests",
"status_code": 302
}
}
],
"priority": 1,
"status": "active",
"created_on": "2021-06-25T20:10:50.000000Z",
"modified_on": "2021-06-28T22:38:10.000000Z"
},
{
"id": "2a9141b18ffb0e6aed826054eec970b8",
"targets": [
{
"target": "url",
"constraint": {
"operator": "matches",
"value": "urlfwd1.unit.tests/target"
}
}
],
"actions": [
{
"id": "forwarding_url",
"value": {
"url": "https://target.unit.tests",
"status_code": 301
}
}
],
"priority": 2,
"status": "active",
"created_on": "2021-06-25T20:10:50.000000Z",
"modified_on": "2021-06-28T22:38:10.000000Z"
},
])
provider._request = Mock()
@@ -509,6 +676,8 @@ class TestCloudflareProvider(TestCase):
}, # zone create
None,
None,
None,
None,
]
# Add something and delete something
@@ -519,14 +688,46 @@ class TestCloudflareProvider(TestCase):
# This matches the zone data above, one to delete, one to leave
'values': ['ns1.foo.bar.', 'ns2.foo.bar.'],
})
exstingurlfwd = Record.new(zone, 'urlfwd1', {
'ttl': 300,
'type': 'URLFWD',
'values': [
{
'path': '/',
'target': 'https://www.unit.tests',
'code': 302,
'masking': '2',
'query': 0,
},
{
'path': '/target',
'target': 'https://target.unit.tests',
'code': 301,
'masking': '2',
'query': 0,
}
]
})
new = Record.new(zone, '', {
'ttl': 300,
'type': 'NS',
# This leaves one and deletes one
'value': 'ns2.foo.bar.',
})
newurlfwd = Record.new(zone, 'urlfwd1', {
'ttl': 300,
'type': 'URLFWD',
'value': {
'path': '/',
'target': 'https://www.unit.tests',
'code': 302,
'masking': '2',
'query': 0,
}
})
change = Update(existing, new)
plan = Plan(zone, zone, [change], True)
changeurlfwd = Update(exstingurlfwd, newurlfwd)
plan = Plan(zone, zone, [change, changeurlfwd], True)
provider._apply(plan)
# Get zones, create zone, create a record, delete a record
@@ -544,7 +745,31 @@ class TestCloudflareProvider(TestCase):
'ttl': 300
}),
call('DELETE', '/zones/42/dns_records/'
'fc12ab34cd5611334422ab3322997653')
'fc12ab34cd5611334422ab3322997653'),
call('PUT', '/zones/42/pagerules/'
'2a9140b17ffb0e6aed826049eec974b7', data={
'targets': [
{
'target': 'url',
'constraint': {
'operator': 'matches',
'value': 'urlfwd1.unit.tests/'
}
}
],
'actions': [
{
'id': 'forwarding_url',
'value': {
'url': 'https://www.unit.tests',
'status_code': 302
}
}
],
'status': 'active'
}),
call('DELETE', '/zones/42/pagerules/'
'2a9141b18ffb0e6aed826054eec970b8'),
])
def test_ptr(self):
@@ -566,6 +791,52 @@ class TestCloudflareProvider(TestCase):
'content': 'foo.bar.com.'
}, list(ptr_record_contents)[0])
def test_loc(self):
self.maxDiff = None
provider = CloudflareProvider('test', 'email', 'token')
zone = Zone('unit.tests.', [])
# LOC record
loc_record = Record.new(zone, 'example', {
'ttl': 300,
'type': 'LOC',
'value': {
'lat_degrees': 31,
'lat_minutes': 58,
'lat_seconds': 52.1,
'lat_direction': 'S',
'long_degrees': 115,
'long_minutes': 49,
'long_seconds': 11.7,
'long_direction': 'E',
'altitude': 20,
'size': 10,
'precision_horz': 10,
'precision_vert': 2,
}
})
loc_record_contents = provider._gen_data(loc_record)
self.assertEquals({
'name': 'example.unit.tests',
'ttl': 300,
'type': 'LOC',
'data': {
'lat_degrees': 31,
'lat_minutes': 58,
'lat_seconds': 52.1,
'lat_direction': 'S',
'long_degrees': 115,
'long_minutes': 49,
'long_seconds': 11.7,
'long_direction': 'E',
'altitude': 20,
'size': 10,
'precision_horz': 10,
'precision_vert': 2,
}
}, list(loc_record_contents)[0])
def test_srv(self):
provider = CloudflareProvider('test', 'email', 'token')
@@ -697,6 +968,23 @@ class TestCloudflareProvider(TestCase):
},
'type': 'SRV',
}),
('31 58 52.1 S 115 49 11.7 E 20 10 10 2', {
'data': {
'lat_degrees': 31,
'lat_minutes': 58,
'lat_seconds': 52.1,
'lat_direction': 'S',
'long_degrees': 115,
'long_minutes': 49,
'long_seconds': 11.7,
'long_direction': 'E',
'altitude': 20,
'size': 10,
'precision_horz': 10,
'precision_vert': 2,
},
'type': 'LOC',
}),
):
self.assertEqual(expected, provider._gen_key(data))
@@ -1343,3 +1631,11 @@ class TestCloudflareProvider(TestCase):
with self.assertRaises(CloudflareRateLimitError) as ctx:
provider.zone_records(zone)
self.assertEquals('last', text_type(ctx.exception))
def test_ttl_mapping(self):
provider = CloudflareProvider('test', 'email', 'token')
self.assertEquals(120, provider._ttl_data(120))
self.assertEquals(120, provider._ttl_data(120))
self.assertEquals(3600, provider._ttl_data(3600))
self.assertEquals(300, provider._ttl_data(1))
+1 -1
View File
@@ -172,7 +172,7 @@ class TestConstellixProvider(TestCase):
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no unsupported
n = len(self.expected.records) - 5
n = len(self.expected.records) - 8
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
+22 -4
View File
@@ -83,14 +83,14 @@ class TestDigitalOceanProvider(TestCase):
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(12, len(zone.records))
self.assertEquals(14, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(12, len(again.records))
self.assertEquals(14, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
@@ -163,7 +163,7 @@ class TestDigitalOceanProvider(TestCase):
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no unsupported
n = len(self.expected.records) - 7
n = len(self.expected.records) - 10
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
self.assertFalse(plan.exists)
@@ -190,6 +190,24 @@ class TestDigitalOceanProvider(TestCase):
'flags': 0, 'name': '@',
'tag': 'issue',
'ttl': 3600, 'type': 'CAA'}),
call('POST', '/domains/unit.tests/records', data={
'name': '_imap._tcp',
'weight': 0,
'data': '.',
'priority': 0,
'ttl': 600,
'type': 'SRV',
'port': 0
}),
call('POST', '/domains/unit.tests/records', data={
'name': '_pop3._tcp',
'weight': 0,
'data': '.',
'priority': 0,
'ttl': 600,
'type': 'SRV',
'port': 0
}),
call('POST', '/domains/unit.tests/records', data={
'name': '_srv._tcp',
'weight': 20,
@@ -200,7 +218,7 @@ class TestDigitalOceanProvider(TestCase):
'port': 30
}),
])
self.assertEquals(24, provider._client._request.call_count)
self.assertEquals(26, provider._client._request.call_count)
provider._client._request.reset_mock()
+1 -1
View File
@@ -137,7 +137,7 @@ class TestDnsimpleProvider(TestCase):
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded
n = len(self.expected.records) - 3
n = len(self.expected.records) - 8
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
self.assertFalse(plan.exists)
+4 -10
View File
@@ -44,12 +44,6 @@ class TestDnsMadeEasyProvider(TestCase):
'value': 'aname.unit.tests.'
}))
expected.add_record(Record.new(expected, 'sub', {
'ttl': 1800,
'type': 'ALIAS',
'value': 'aname.unit.tests.'
}))
for record in list(expected.records):
if record.name == 'sub' and record._type == 'NS':
expected._remove_record(record)
@@ -108,14 +102,14 @@ class TestDnsMadeEasyProvider(TestCase):
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(15, len(zone.records))
self.assertEquals(14, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(15, len(again.records))
self.assertEquals(14, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
@@ -140,7 +134,7 @@ class TestDnsMadeEasyProvider(TestCase):
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no unsupported
n = len(self.expected.records) - 5
n = len(self.expected.records) - 10
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
@@ -180,7 +174,7 @@ class TestDnsMadeEasyProvider(TestCase):
'port': 30
}),
])
self.assertEquals(27, provider._client._request.call_count)
self.assertEquals(26, provider._client._request.call_count)
provider._client._request.reset_mock()
+4 -4
View File
@@ -80,14 +80,14 @@ class TestEasyDNSProvider(TestCase):
text=fh.read())
provider.populate(zone)
self.assertEquals(13, len(zone.records))
self.assertEquals(15, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(13, len(again.records))
self.assertEquals(15, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
@@ -374,12 +374,12 @@ class TestEasyDNSProvider(TestCase):
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no unsupported
n = len(self.expected.records) - 6
n = len(self.expected.records) - 9
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
self.assertFalse(plan.exists)
self.assertEquals(23, provider._client._request.call_count)
self.assertEquals(25, provider._client._request.call_count)
provider._client._request.reset_mock()
+5 -5
View File
@@ -77,14 +77,14 @@ class TestEdgeDnsProvider(TestCase):
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(16, len(zone.records))
self.assertEquals(18, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(16, len(again.records))
self.assertEquals(18, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
@@ -105,7 +105,7 @@ class TestEdgeDnsProvider(TestCase):
mock.delete(ANY, status_code=204)
changes = provider.apply(plan)
self.assertEquals(29, changes)
self.assertEquals(31, changes)
# Test against a zone that doesn't exist yet
with requests_mock() as mock:
@@ -118,7 +118,7 @@ class TestEdgeDnsProvider(TestCase):
mock.delete(ANY, status_code=204)
changes = provider.apply(plan)
self.assertEquals(14, changes)
self.assertEquals(16, changes)
# Test against a zone that doesn't exist yet, but gid not provided
with requests_mock() as mock:
@@ -132,7 +132,7 @@ class TestEdgeDnsProvider(TestCase):
mock.delete(ANY, status_code=204)
changes = provider.apply(plan)
self.assertEquals(14, changes)
self.assertEquals(16, changes)
# Test against a zone that doesn't exist, but cid not provided
+377
View File
@@ -0,0 +1,377 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from mock import Mock, call
from os.path import dirname, join
from requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record
from octodns.provider.gandi import GandiProvider, GandiClientBadRequest, \
GandiClientUnauthorized, GandiClientForbidden, GandiClientNotFound, \
GandiClientUnknownDomainName
from octodns.provider.yaml import YamlProvider
from octodns.zone import Zone
class TestGandiProvider(TestCase):
expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
# We remove this record from the test zone as Gandi API reject it
# (rightfully).
expected._remove_record(Record.new(expected, 'sub', {
'ttl': 1800,
'type': 'NS',
'values': [
'6.2.3.4.',
'7.2.3.4.'
]
}))
def test_populate(self):
provider = GandiProvider('test_id', 'token')
# 400 - Bad Request.
with requests_mock() as mock:
mock.get(ANY, status_code=400,
text='{"status": "error", "errors": [{"location": '
'"body", "name": "items", "description": '
'"\'6.2.3.4.\': invalid hostname (param: '
'{\'rrset_type\': u\'NS\', \'rrset_ttl\': 3600, '
'\'rrset_name\': u\'sub\', \'rrset_values\': '
'[u\'6.2.3.4.\', u\'7.2.3.4.\']})"}, {"location": '
'"body", "name": "items", "description": '
'"\'7.2.3.4.\': invalid hostname (param: '
'{\'rrset_type\': u\'NS\', \'rrset_ttl\': 3600, '
'\'rrset_name\': u\'sub\', \'rrset_values\': '
'[u\'6.2.3.4.\', u\'7.2.3.4.\']})"}]}')
with self.assertRaises(GandiClientBadRequest) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertIn('"status": "error"', text_type(ctx.exception))
# 401 - Unauthorized.
with requests_mock() as mock:
mock.get(ANY, status_code=401,
text='{"code":401,"message":"The server could not verify '
'that you authorized to access the document you '
'requested. Either you supplied the wrong '
'credentials (e.g., bad api key), or your access '
'token has expired","object":"HTTPUnauthorized",'
'"cause":"Unauthorized"}')
with self.assertRaises(GandiClientUnauthorized) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertIn('"cause":"Unauthorized"', text_type(ctx.exception))
# 403 - Forbidden.
with requests_mock() as mock:
mock.get(ANY, status_code=403,
text='{"code":403,"message":"Access was denied to this '
'resource.","object":"HTTPForbidden","cause":'
'"Forbidden"}')
with self.assertRaises(GandiClientForbidden) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertIn('"cause":"Forbidden"', text_type(ctx.exception))
# 404 - Not Found.
with requests_mock() as mock:
mock.get(ANY, status_code=404,
text='{"code": 404, "message": "The resource could not '
'be found.", "object": "HTTPNotFound", "cause": '
'"Not Found"}')
with self.assertRaises(GandiClientNotFound) as ctx:
zone = Zone('unit.tests.', [])
provider._client.zone(zone)
self.assertIn('"cause": "Not Found"', text_type(ctx.exception))
# General error
with requests_mock() as mock:
mock.get(ANY, status_code=502, text='Things caught fire')
with self.assertRaises(HTTPError) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(502, ctx.exception.response.status_code)
# No diffs == no changes
with requests_mock() as mock:
base = 'https://api.gandi.net/v5/livedns/domains/unit.tests' \
'/records'
with open('tests/fixtures/gandi-no-changes.json') as fh:
mock.get(base, text=fh.read())
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(16, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
del provider._zone_records[zone.name]
# Default Gandi zone file.
with requests_mock() as mock:
base = 'https://api.gandi.net/v5/livedns/domains/unit.tests' \
'/records'
with open('tests/fixtures/gandi-records.json') as fh:
mock.get(base, text=fh.read())
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(11, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(24, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(11, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
def test_apply(self):
provider = GandiProvider('test_id', 'token')
# Zone does not exists but can be created.
with requests_mock() as mock:
mock.get(ANY, status_code=404,
text='{"code": 404, "message": "The resource could not '
'be found.", "object": "HTTPNotFound", "cause": '
'"Not Found"}')
mock.post(ANY, status_code=201,
text='{"message": "Domain Created"}')
plan = provider.plan(self.expected)
provider.apply(plan)
# Zone does not exists and can't be created.
with requests_mock() as mock:
mock.get(ANY, status_code=404,
text='{"code": 404, "message": "The resource could not '
'be found.", "object": "HTTPNotFound", "cause": '
'"Not Found"}')
mock.post(ANY, status_code=404,
text='{"code": 404, "message": "The resource could not '
'be found.", "object": "HTTPNotFound", "cause": '
'"Not Found"}')
with self.assertRaises((GandiClientNotFound,
GandiClientUnknownDomainName)) as ctx:
plan = provider.plan(self.expected)
provider.apply(plan)
self.assertIn('This domain is not registered at Gandi.',
text_type(ctx.exception))
resp = Mock()
resp.json = Mock()
provider._client._request = Mock(return_value=resp)
with open('tests/fixtures/gandi-zone.json') as fh:
zone = fh.read()
# non-existent domain
resp.json.side_effect = [
GandiClientNotFound(resp), # no zone in populate
GandiClientNotFound(resp), # no domain during apply
zone
]
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no LOC
n = len(self.expected.records) - 6
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
self.assertFalse(plan.exists)
provider._client._request.assert_has_calls([
call('GET', '/livedns/domains/unit.tests/records'),
call('GET', '/livedns/domains/unit.tests'),
call('POST', '/livedns/domains', data={
'fqdn': 'unit.tests',
'zone': {}
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'www.sub',
'rrset_ttl': 300,
'rrset_type': 'A',
'rrset_values': ['2.2.3.6']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'www',
'rrset_ttl': 300,
'rrset_type': 'A',
'rrset_values': ['2.2.3.6']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'txt',
'rrset_ttl': 600,
'rrset_type': 'TXT',
'rrset_values': [
'Bah bah black sheep',
'have you any wool.',
'v=DKIM1;k=rsa;s=email;h=sha256;p=A/kinda+of/long/string'
'+with+numb3rs'
]
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'spf',
'rrset_ttl': 600,
'rrset_type': 'SPF',
'rrset_values': ['v=spf1 ip4:192.168.0.1/16-all']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'ptr',
'rrset_ttl': 300,
'rrset_type': 'PTR',
'rrset_values': ['foo.bar.com.']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'mx',
'rrset_ttl': 300,
'rrset_type': 'MX',
'rrset_values': [
'10 smtp-4.unit.tests.',
'20 smtp-2.unit.tests.',
'30 smtp-3.unit.tests.',
'40 smtp-1.unit.tests.'
]
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'excluded',
'rrset_ttl': 3600,
'rrset_type': 'CNAME',
'rrset_values': ['unit.tests.']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'dname',
'rrset_ttl': 300,
'rrset_type': 'DNAME',
'rrset_values': ['unit.tests.']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'cname',
'rrset_ttl': 300,
'rrset_type': 'CNAME',
'rrset_values': ['unit.tests.']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'aaaa',
'rrset_ttl': 600,
'rrset_type': 'AAAA',
'rrset_values': ['2601:644:500:e210:62f8:1dff:feb8:947a']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': '_srv._tcp',
'rrset_ttl': 600,
'rrset_type': 'SRV',
'rrset_values': [
'10 20 30 foo-1.unit.tests.',
'12 20 30 foo-2.unit.tests.'
]
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': '_pop3._tcp',
'rrset_ttl': 600,
'rrset_type': 'SRV',
'rrset_values': [
'0 0 0 .',
]
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': '_imap._tcp',
'rrset_ttl': 600,
'rrset_type': 'SRV',
'rrset_values': [
'0 0 0 .',
]
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': '@',
'rrset_ttl': 3600,
'rrset_type': 'SSHFP',
'rrset_values': [
'1 1 7491973e5f8b39d5327cd4e08bc81b05f7710b49',
'1 1 bf6b6825d2977c511a475bbefb88aad54a92ac73'
]
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': '@',
'rrset_ttl': 3600,
'rrset_type': 'CAA',
'rrset_values': ['0 issue "ca.unit.tests"']
}),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': '@',
'rrset_ttl': 300,
'rrset_type': 'A',
'rrset_values': ['1.2.3.4', '1.2.3.5']
})
])
# expected number of total calls
self.assertEquals(19, provider._client._request.call_count)
provider._client._request.reset_mock()
# delete 1 and update 1
provider._client.zone_records = Mock(return_value=[
{
'rrset_name': 'www',
'rrset_ttl': 300,
'rrset_type': 'A',
'rrset_values': ['1.2.3.4']
},
{
'rrset_name': 'www',
'rrset_ttl': 300,
'rrset_type': 'A',
'rrset_values': ['2.2.3.4']
},
{
'rrset_name': 'ttl',
'rrset_ttl': 600,
'rrset_type': 'A',
'rrset_values': ['3.2.3.4']
}
])
# Domain exists, we don't care about return
resp.json.side_effect = ['{}']
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, 'ttl', {
'ttl': 300,
'type': 'A',
'value': '3.2.3.4'
}))
plan = provider.plan(wanted)
self.assertTrue(plan.exists)
self.assertEquals(2, len(plan.changes))
self.assertEquals(2, provider.apply(plan))
# recreate for update, and deletes for the 2 parts of the other
provider._client._request.assert_has_calls([
call('DELETE', '/livedns/domains/unit.tests/records/www/A'),
call('DELETE', '/livedns/domains/unit.tests/records/ttl/A'),
call('POST', '/livedns/domains/unit.tests/records', data={
'rrset_name': 'ttl',
'rrset_ttl': 300,
'rrset_type': 'A',
'rrset_values': ['3.2.3.4']
})
], any_order=True)
+672
View File
@@ -0,0 +1,672 @@
#
#
#
from __future__ import (
absolute_import,
division,
print_function,
unicode_literals,
)
from mock import Mock, call
from os.path import dirname, join
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record, Update, Delete, Create
from octodns.provider.gcore import (
GCoreProvider,
GCoreClientBadRequest,
GCoreClientNotFound,
GCoreClientException,
)
from octodns.provider.yaml import YamlProvider
from octodns.zone import Zone
class TestGCoreProvider(TestCase):
expected = Zone("unit.tests.", [])
source = YamlProvider("test", join(dirname(__file__), "config"))
source.populate(expected)
default_filters = [
{"type": "geodns"},
{
"type": "default",
"limit": 1,
"strict": False,
},
{"type": "first_n", "limit": 1},
]
def test_populate(self):
provider = GCoreProvider("test_id", token="token")
# TC: 400 - Bad Request.
with requests_mock() as mock:
mock.get(ANY, status_code=400, text='{"error":"bad body"}')
with self.assertRaises(GCoreClientBadRequest) as ctx:
zone = Zone("unit.tests.", [])
provider.populate(zone)
self.assertIn('"error":"bad body"', text_type(ctx.exception))
# TC: 404 - Not Found.
with requests_mock() as mock:
mock.get(
ANY, status_code=404, text='{"error":"zone is not found"}'
)
with self.assertRaises(GCoreClientNotFound) as ctx:
zone = Zone("unit.tests.", [])
provider._client.zone(zone.name)
self.assertIn(
'"error":"zone is not found"', text_type(ctx.exception)
)
# TC: General error
with requests_mock() as mock:
mock.get(ANY, status_code=500, text="Things caught fire")
with self.assertRaises(GCoreClientException) as ctx:
zone = Zone("unit.tests.", [])
provider.populate(zone)
self.assertEqual("Things caught fire", text_type(ctx.exception))
# TC: No credentials or token error
with requests_mock() as mock:
with self.assertRaises(ValueError) as ctx:
GCoreProvider("test_id")
self.assertEqual(
"either token or login & password must be set",
text_type(ctx.exception),
)
# TC: Auth with login password
with requests_mock() as mock:
def match_body(request):
return {"username": "foo", "password": "bar"} == request.json()
auth_url = "http://api/auth/jwt/login"
mock.post(
auth_url,
additional_matcher=match_body,
status_code=200,
json={"access": "access"},
)
providerPassword = GCoreProvider(
"test_id",
url="http://dns",
auth_url="http://api",
login="foo",
password="bar",
)
assert mock.called
# make sure token passed in header
zone_rrset_url = "http://dns/zones/unit.tests/rrsets?all=true"
mock.get(
zone_rrset_url,
request_headers={"Authorization": "Bearer access"},
status_code=404,
)
zone = Zone("unit.tests.", [])
assert not providerPassword.populate(zone)
# TC: No diffs == no changes
with requests_mock() as mock:
base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets"
with open("tests/fixtures/gcore-no-changes.json") as fh:
mock.get(base, text=fh.read())
zone = Zone("unit.tests.", [])
provider.populate(zone)
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.assertEqual(0, len(changes))
# TC: 4 create (dynamic) + 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:
mock.get(base, text=fh.read())
zone = Zone("unit.tests.", [])
provider.populate(zone)
self.assertEqual(16, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEqual(11, len(changes))
self.assertEqual(
3, len([c for c in changes if isinstance(c, Create)])
)
self.assertEqual(
1, len([c for c in changes if isinstance(c, Delete)])
)
self.assertEqual(
7, len([c for c in changes if isinstance(c, Update)])
)
# TC: no pools can be built
with requests_mock() as mock:
base = "https://dnsapi.gcorelabs.com/v2/zones/unit.tests/rrsets"
mock.get(
base,
json={
"rrsets": [
{
"name": "unit.tests.",
"type": "A",
"ttl": 300,
"filters": self.default_filters,
"resource_records": [{"content": ["7.7.7.7"]}],
}
]
},
)
zone = Zone("unit.tests.", [])
with self.assertRaises(RuntimeError) as ctx:
provider.populate(zone)
self.assertTrue(
str(ctx.exception).startswith(
"filter is enabled, but no pools where built for"
),
"{} - is not start from desired text".format(ctx.exception),
)
def test_apply(self):
provider = GCoreProvider("test_id", url="http://api", token="token")
# TC: Zone does not exists but can be created.
with requests_mock() as mock:
mock.get(
ANY, status_code=404, text='{"error":"zone is not found"}'
)
mock.post(ANY, status_code=200, text='{"id":1234}')
plan = provider.plan(self.expected)
provider.apply(plan)
# TC: Zone does not exists and can't be created.
with requests_mock() as mock:
mock.get(
ANY, status_code=404, text='{"error":"zone is not found"}'
)
mock.post(
ANY,
status_code=400,
text='{"error":"parent zone is already'
' occupied by another client"}',
)
with self.assertRaises(
(GCoreClientNotFound, GCoreClientBadRequest)
) as ctx:
plan = provider.plan(self.expected)
provider.apply(plan)
self.assertIn(
"parent zone is already occupied by another client",
text_type(ctx.exception),
)
resp = Mock()
resp.json = Mock()
provider._client._request = Mock(return_value=resp)
with open("tests/fixtures/gcore-zone.json") as fh:
zone = fh.read()
# non-existent domain
resp.json.side_effect = [
GCoreClientNotFound(resp), # no zone in populate
GCoreClientNotFound(resp), # no domain during apply
zone,
]
plan = provider.plan(self.expected)
# TC: create all
self.assertEqual(13, len(plan.changes))
self.assertEqual(13, provider.apply(plan))
self.assertFalse(plan.exists)
provider._client._request.assert_has_calls(
[
call(
"GET",
"http://api/zones/unit.tests/rrsets",
params={"all": "true"},
),
call("GET", "http://api/zones/unit.tests"),
call("POST", "http://api/zones", data={"name": "unit.tests"}),
call(
"POST",
"http://api/zones/unit.tests/www.sub.unit.tests./A",
data={
"ttl": 300,
"resource_records": [{"content": ["2.2.3.6"]}],
},
),
call(
"POST",
"http://api/zones/unit.tests/www.unit.tests./A",
data={
"ttl": 300,
"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",
data={
"ttl": 600,
"resource_records": [
{
"content": [
"2601:644:500:e210:62f8:1dff:feb8:947a"
]
}
],
},
),
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",
data={
"ttl": 300,
"resource_records": [
{"content": ["1.2.3.4"]},
{"content": ["1.2.3.5"]},
],
},
),
]
)
# expected number of total calls
self.assertEqual(16, provider._client._request.call_count)
# TC: delete 1 and update 1
provider._client._request.reset_mock()
provider._client.zone_records = Mock(
return_value=[
{
"name": "www",
"ttl": 300,
"type": "A",
"resource_records": [{"content": ["1.2.3.4"]}],
},
{
"name": "ttl",
"ttl": 600,
"type": "A",
"resource_records": [{"content": ["3.2.3.4"]}],
},
]
)
# Domain exists, we don't care about return
resp.json.side_effect = ["{}"]
wanted = Zone("unit.tests.", [])
wanted.add_record(
Record.new(
wanted, "ttl", {"ttl": 300, "type": "A", "value": "3.2.3.4"}
)
)
plan = provider.plan(wanted)
self.assertTrue(plan.exists)
self.assertEqual(2, len(plan.changes))
self.assertEqual(2, provider.apply(plan))
provider._client._request.assert_has_calls(
[
call(
"DELETE", "http://api/zones/unit.tests/www.unit.tests./A"
),
call(
"PUT",
"http://api/zones/unit.tests/ttl.unit.tests./A",
data={
"ttl": 300,
"resource_records": [{"content": ["3.2.3.4"]}],
},
),
]
)
# TC: create dynamics
provider._client._request.reset_mock()
provider._client.zone_records = Mock(return_value=[])
# Domain exists, we don't care about return
resp.json.side_effect = ["{}"]
wanted = Zone("unit.tests.", [])
wanted.add_record(
Record.new(
wanted,
"geo-simple",
{
"ttl": 300,
"type": "A",
"value": "3.3.3.3",
"dynamic": {
"pools": {
"pool-1": {
"fallback": "other",
"values": [
{"value": "1.1.1.1"},
{"value": "1.1.1.2"},
],
},
"pool-2": {
"fallback": "other",
"values": [
{"value": "2.2.2.1"},
],
},
"other": {"values": [{"value": "3.3.3.3"}]},
},
"rules": [
{"pool": "pool-1", "geos": ["EU-RU"]},
{"pool": "pool-2", "geos": ["EU"]},
{"pool": "other"},
],
},
},
),
)
wanted.add_record(
Record.new(
wanted,
"geo-defaults",
{
"ttl": 300,
"type": "A",
"value": "3.2.3.4",
"dynamic": {
"pools": {
"pool-1": {
"values": [
{"value": "2.2.2.1"},
],
},
},
"rules": [
{"pool": "pool-1", "geos": ["EU"]},
],
},
},
),
)
wanted.add_record(
Record.new(
wanted,
"cname-smpl",
{
"ttl": 300,
"type": "CNAME",
"value": "en.unit.tests.",
"dynamic": {
"pools": {
"pool-1": {
"fallback": "other",
"values": [
{"value": "ru-1.unit.tests."},
{"value": "ru-2.unit.tests."},
],
},
"pool-2": {
"fallback": "other",
"values": [
{"value": "eu.unit.tests."},
],
},
"other": {"values": [{"value": "en.unit.tests."}]},
},
"rules": [
{"pool": "pool-1", "geos": ["EU-RU"]},
{"pool": "pool-2", "geos": ["EU"]},
{"pool": "other"},
],
},
},
),
)
wanted.add_record(
Record.new(
wanted,
"cname-dflt",
{
"ttl": 300,
"type": "CNAME",
"value": "en.unit.tests.",
"dynamic": {
"pools": {
"pool-1": {
"values": [
{"value": "eu.unit.tests."},
],
},
},
"rules": [
{"pool": "pool-1", "geos": ["EU"]},
],
},
},
),
)
plan = provider.plan(wanted)
self.assertTrue(plan.exists)
self.assertEqual(4, len(plan.changes))
self.assertEqual(4, provider.apply(plan))
provider._client._request.assert_has_calls(
[
call(
"POST",
"http://api/zones/unit.tests/geo-simple.unit.tests./A",
data={
"ttl": 300,
"filters": self.default_filters,
"resource_records": [
{
"content": ["1.1.1.1"],
"meta": {"countries": ["RU"]},
},
{
"content": ["1.1.1.2"],
"meta": {"countries": ["RU"]},
},
{
"content": ["2.2.2.1"],
"meta": {"continents": ["EU"]},
},
{
"content": ["3.3.3.3"],
"meta": {"default": True},
},
],
},
),
call(
"POST",
"http://api/zones/unit.tests/geo-defaults.unit.tests./A",
data={
"ttl": 300,
"filters": self.default_filters,
"resource_records": [
{
"content": ["2.2.2.1"],
"meta": {"continents": ["EU"]},
},
{
"content": ["3.2.3.4"],
},
],
},
),
call(
"POST",
"http://api/zones/unit.tests/cname-smpl.unit.tests./CNAME",
data={
"ttl": 300,
"filters": self.default_filters,
"resource_records": [
{
"content": ["ru-1.unit.tests."],
"meta": {"countries": ["RU"]},
},
{
"content": ["ru-2.unit.tests."],
"meta": {"countries": ["RU"]},
},
{
"content": ["eu.unit.tests."],
"meta": {"continents": ["EU"]},
},
{
"content": ["en.unit.tests."],
"meta": {"default": True},
},
],
},
),
call(
"POST",
"http://api/zones/unit.tests/cname-dflt.unit.tests./CNAME",
data={
"ttl": 300,
"filters": self.default_filters,
"resource_records": [
{
"content": ["eu.unit.tests."],
"meta": {"continents": ["EU"]},
},
{
"content": ["en.unit.tests."],
},
],
},
),
]
)
+341
View File
@@ -0,0 +1,341 @@
#
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from mock import Mock, call
from os.path import dirname, join
from requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from unittest import TestCase
from octodns.record import Record
from octodns.provider.hetzner import HetznerClientNotFound, \
HetznerProvider
from octodns.provider.yaml import YamlProvider
from octodns.zone import Zone
class TestHetznerProvider(TestCase):
expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
def test_populate(self):
provider = HetznerProvider('test', 'token')
# Bad auth
with requests_mock() as mock:
mock.get(ANY, status_code=401,
text='{"message":"Invalid authentication credentials"}')
with self.assertRaises(Exception) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals('Unauthorized', text_type(ctx.exception))
# General error
with requests_mock() as mock:
mock.get(ANY, status_code=502, text='Things caught fire')
with self.assertRaises(HTTPError) as ctx:
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(502, ctx.exception.response.status_code)
# Non-existent zone doesn't populate anything
with requests_mock() as mock:
mock.get(ANY, status_code=404,
text='{"zone":{"id":"","name":"","ttl":0,"registrar":"",'
'"legacy_dns_host":"","legacy_ns":null,"ns":null,'
'"created":"","verified":"","modified":"","project":"",'
'"owner":"","permission":"","zone_type":{"id":"",'
'"name":"","description":"","prices":null},"status":"",'
'"paused":false,"is_secondary_dns":false,'
'"txt_verification":{"name":"","token":""},'
'"records_count":0},"error":{'
'"message":"zone not found","code":404}}')
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(set(), zone.records)
# No diffs == no changes
with requests_mock() as mock:
base = provider._client.BASE_URL
with open('tests/fixtures/hetzner-zones.json') as fh:
mock.get('{}/zones'.format(base), text=fh.read())
with open('tests/fixtures/hetzner-records.json') as fh:
mock.get('{}/records'.format(base), text=fh.read())
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(13, len(zone.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
# 2nd populate makes no network calls/all from cache
again = Zone('unit.tests.', [])
provider.populate(again)
self.assertEquals(13, len(again.records))
# bust the cache
del provider._zone_records[zone.name]
def test_apply(self):
provider = HetznerProvider('test', 'token')
resp = Mock()
resp.json = Mock()
provider._client._do = Mock(return_value=resp)
domain_after_creation = {'zone': {
'id': 'unit.tests',
'name': 'unit.tests',
'ttl': 3600,
}}
# non-existent domain, create everything
resp.json.side_effect = [
HetznerClientNotFound, # no zone in populate
HetznerClientNotFound, # no zone during apply
domain_after_creation,
]
plan = provider.plan(self.expected)
# No root NS, no ignored, no excluded, no unsupported
n = len(self.expected.records) - 10
self.assertEquals(n, len(plan.changes))
self.assertEquals(n, provider.apply(plan))
self.assertFalse(plan.exists)
provider._client._do.assert_has_calls([
# created the zone
call('POST', '/zones', None, {
'name': 'unit.tests',
'ttl': None,
}),
# created all the records with their expected data
call('POST', '/records', data={
'name': '@',
'ttl': 300,
'type': 'A',
'value': '1.2.3.4',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': '@',
'ttl': 300,
'type': 'A',
'value': '1.2.3.5',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': '@',
'ttl': 3600,
'type': 'CAA',
'value': '0 issue "ca.unit.tests"',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': '_imap._tcp',
'ttl': 600,
'type': 'SRV',
'value': '0 0 0 .',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': '_pop3._tcp',
'ttl': 600,
'type': 'SRV',
'value': '0 0 0 .',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': '_srv._tcp',
'ttl': 600,
'type': 'SRV',
'value': '10 20 30 foo-1.unit.tests.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': '_srv._tcp',
'ttl': 600,
'type': 'SRV',
'value': '12 20 30 foo-2.unit.tests.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'aaaa',
'ttl': 600,
'type': 'AAAA',
'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'cname',
'ttl': 300,
'type': 'CNAME',
'value': 'unit.tests.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'included',
'ttl': 3600,
'type': 'CNAME',
'value': 'unit.tests.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'mx',
'ttl': 300,
'type': 'MX',
'value': '10 smtp-4.unit.tests.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'mx',
'ttl': 300,
'type': 'MX',
'value': '20 smtp-2.unit.tests.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'mx',
'ttl': 300,
'type': 'MX',
'value': '30 smtp-3.unit.tests.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'mx',
'ttl': 300,
'type': 'MX',
'value': '40 smtp-1.unit.tests.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'sub',
'ttl': 3600,
'type': 'NS',
'value': '6.2.3.4.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'sub',
'ttl': 3600,
'type': 'NS',
'value': '7.2.3.4.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'txt',
'ttl': 600,
'type': 'TXT',
'value': 'Bah bah black sheep',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'txt',
'ttl': 600,
'type': 'TXT',
'value': 'have you any wool.',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'txt',
'ttl': 600,
'type': 'TXT',
'value': 'v=DKIM1;k=rsa;s=email;h=sha256;'
'p=A/kinda+of/long/string+with+numb3rs',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'www',
'ttl': 300,
'type': 'A',
'value': '2.2.3.6',
'zone_id': 'unit.tests',
}),
call('POST', '/records', data={
'name': 'www.sub',
'ttl': 300,
'type': 'A',
'value': '2.2.3.6',
'zone_id': 'unit.tests',
}),
])
self.assertEquals(24, provider._client._do.call_count)
provider._client._do.reset_mock()
# delete 1 and update 1
provider._client.zone_get = Mock(return_value={
'id': 'unit.tests',
'name': 'unit.tests',
'ttl': 3600,
})
provider._client.zone_records_get = Mock(return_value=[
{
'type': 'A',
'id': 'one',
'created': '0000-00-00T00:00:00Z',
'modified': '0000-00-00T00:00:00Z',
'zone_id': 'unit.tests',
'name': 'www',
'value': '1.2.3.4',
'ttl': 300,
},
{
'type': 'A',
'id': 'two',
'created': '0000-00-00T00:00:00Z',
'modified': '0000-00-00T00:00:00Z',
'zone_id': 'unit.tests',
'name': 'www',
'value': '2.2.3.4',
'ttl': 300,
},
{
'type': 'A',
'id': 'three',
'created': '0000-00-00T00:00:00Z',
'modified': '0000-00-00T00:00:00Z',
'zone_id': 'unit.tests',
'name': 'ttl',
'value': '3.2.3.4',
'ttl': 600,
},
])
# Domain exists, we don't care about return
resp.json.side_effect = ['{}']
wanted = Zone('unit.tests.', [])
wanted.add_record(Record.new(wanted, 'ttl', {
'ttl': 300,
'type': 'A',
'value': '3.2.3.4',
}))
plan = provider.plan(wanted)
self.assertTrue(plan.exists)
self.assertEquals(2, len(plan.changes))
self.assertEquals(2, provider.apply(plan))
# recreate for update, and delete for the 2 parts of the other
provider._client._do.assert_has_calls([
call('POST', '/records', data={
'name': 'ttl',
'ttl': 300,
'type': 'A',
'value': '3.2.3.4',
'zone_id': 'unit.tests',
}),
call('DELETE', '/records/one'),
call('DELETE', '/records/two'),
call('DELETE', '/records/three'),
], any_order=True)
+6 -6
View File
@@ -171,7 +171,7 @@ class TestMythicBeastsProvider(TestCase):
def test_command_generation(self):
zone = Zone('unit.tests.', [])
zone.add_record(Record.new(zone, 'prawf-alias', {
zone.add_record(Record.new(zone, '', {
'ttl': 60,
'type': 'ALIAS',
'value': 'alias.unit.tests.',
@@ -228,7 +228,7 @@ class TestMythicBeastsProvider(TestCase):
)
expected_commands = [
'ADD prawf-alias.unit.tests 60 ANAME alias.unit.tests.',
'ADD unit.tests 60 ANAME alias.unit.tests.',
'ADD prawf-ns.unit.tests 300 NS alias.unit.tests.',
'ADD prawf-ns.unit.tests 300 NS alias2.unit.tests.',
'ADD prawf-a.unit.tests 60 A 1.2.3.4',
@@ -378,8 +378,8 @@ class TestMythicBeastsProvider(TestCase):
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(15, len(zone.records))
self.assertEquals(15, len(self.expected.records))
self.assertEquals(17, len(zone.records))
self.assertEquals(17, len(self.expected.records))
changes = self.expected.changes(zone, provider)
self.assertEquals(0, len(changes))
@@ -445,7 +445,7 @@ class TestMythicBeastsProvider(TestCase):
if isinstance(c, Update)]))
self.assertEquals(1, len([c for c in plan.changes
if isinstance(c, Delete)]))
self.assertEquals(14, len([c for c in plan.changes
self.assertEquals(16, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertEquals(16, provider.apply(plan))
self.assertEquals(18, provider.apply(plan))
self.assertTrue(plan.exists)
File diff suppressed because it is too large Load Diff
+18 -4
View File
@@ -82,6 +82,20 @@ class TestPowerDnsProvider(TestCase):
provider._powerdns_version = None
self.assertNotEquals(provider.powerdns_version, [4, 1, 10])
# Test version detection with pre-releases
with requests_mock() as mock:
# Reset version, so detection will try again
provider._powerdns_version = None
mock.get('http://non.existent:8081/api/v1/servers/localhost',
status_code=200, json={'version': "4.4.0-alpha1"})
self.assertEquals(provider.powerdns_version, [4, 4, 0])
provider._powerdns_version = None
mock.get('http://non.existent:8081/api/v1/servers/localhost',
status_code=200,
json={'version': "4.5.0-alpha0.435.master.gcb114252b"})
self.assertEquals(provider.powerdns_version, [4, 5, 0])
def test_provider_version_config(self):
provider = PowerDnsProvider('test', 'non.existent', 'api-key',
nameserver_values=['8.8.8.8.',
@@ -171,8 +185,8 @@ class TestPowerDnsProvider(TestCase):
expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
expected_n = len(expected.records) - 2
self.assertEquals(16, expected_n)
expected_n = len(expected.records) - 4
self.assertEquals(19, expected_n)
# No diffs == no changes
with requests_mock() as mock:
@@ -180,7 +194,7 @@ class TestPowerDnsProvider(TestCase):
zone = Zone('unit.tests.', [])
provider.populate(zone)
self.assertEquals(16, len(zone.records))
self.assertEquals(19, len(zone.records))
changes = expected.changes(zone, provider)
self.assertEquals(0, len(changes))
@@ -277,7 +291,7 @@ class TestPowerDnsProvider(TestCase):
expected = Zone('unit.tests.', [])
source = YamlProvider('test', join(dirname(__file__), 'config'))
source.populate(expected)
self.assertEquals(18, len(expected.records))
self.assertEquals(23, len(expected.records))
# A small change to a single record
with requests_mock() as mock:
+320
View File
@@ -394,6 +394,139 @@ class TestRoute53Provider(TestCase):
return (provider, stubber)
def test_process_desired_zone(self):
provider, stubber = self._get_stubbed_fallback_auth_provider()
# No records, essentially a no-op
desired = Zone('unit.tests.', [])
got = provider._process_desired_zone(desired)
self.assertEquals(desired.records, got.records)
# Record without any geos
desired = Zone('unit.tests.', [])
record = Record.new(desired, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
'dynamic': {
'pools': {
'one': {
'values': [{
'value': '2.2.3.4',
}],
},
},
'rules': [{
'pool': 'one',
}],
},
})
desired.add_record(record)
got = provider._process_desired_zone(desired)
self.assertEquals(desired.records, got.records)
self.assertEquals(1, len(list(got.records)[0].dynamic.rules))
self.assertFalse('geos' in list(got.records)[0].dynamic.rules[0].data)
# Record where all geos are supported
desired = Zone('unit.tests.', [])
record = Record.new(desired, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
'dynamic': {
'pools': {
'one': {
'values': [{
'value': '1.2.3.4',
}],
},
'two': {
'values': [{
'value': '2.2.3.4',
}],
},
},
'rules': [{
'geos': ['EU', 'NA-US-OR'],
'pool': 'two',
}, {
'pool': 'one',
}],
},
})
desired.add_record(record)
got = provider._process_desired_zone(desired)
self.assertEquals(2, len(list(got.records)[0].dynamic.rules))
self.assertEquals(['EU', 'NA-US-OR'],
list(got.records)[0].dynamic.rules[0].data['geos'])
self.assertFalse('geos' in list(got.records)[0].dynamic.rules[1].data)
# Record with NA-CA-* only rule which is removed
desired = Zone('unit.tests.', [])
record = Record.new(desired, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
'dynamic': {
'pools': {
'one': {
'values': [{
'value': '1.2.3.4',
}],
},
'two': {
'values': [{
'value': '2.2.3.4',
}],
},
},
'rules': [{
'geos': ['NA-CA-BC'],
'pool': 'two',
}, {
'pool': 'one',
}],
},
})
desired.add_record(record)
got = provider._process_desired_zone(desired)
self.assertEquals(1, len(list(got.records)[0].dynamic.rules))
self.assertFalse('geos' in list(got.records)[0].dynamic.rules[0].data)
# Record with NA-CA-* rule combined with other geos, filtered
desired = Zone('unit.tests.', [])
record = Record.new(desired, 'a', {
'ttl': 30,
'type': 'A',
'value': '1.2.3.4',
'dynamic': {
'pools': {
'one': {
'values': [{
'value': '1.2.3.4',
}],
},
'two': {
'values': [{
'value': '2.2.3.4',
}],
},
},
'rules': [{
'geos': ['EU', 'NA-CA-NB', 'NA-US-OR'],
'pool': 'two',
}, {
'pool': 'one',
}],
},
})
desired.add_record(record)
got = provider._process_desired_zone(desired)
self.assertEquals(2, len(list(got.records)[0].dynamic.rules))
self.assertEquals(['EU', 'NA-US-OR'],
list(got.records)[0].dynamic.rules[0].data['geos'])
self.assertFalse('geos' in list(got.records)[0].dynamic.rules[1].data)
def test_populate_with_fallback(self):
provider, stubber = self._get_stubbed_fallback_auth_provider()
@@ -1166,6 +1299,31 @@ class TestRoute53Provider(TestCase):
})
stubber.add_response('change_tags_for_resource', {})
health_check_config = {
'EnableSNI': False,
'FailureThreshold': 6,
'FullyQualifiedDomainName': '4.2.3.4',
'IPAddress': '4.2.3.4',
'MeasureLatency': True,
'Port': 8080,
'RequestInterval': 10,
'ResourcePath': '/_status',
'Type': 'HTTP'
}
stubber.add_response('create_health_check', {
'HealthCheck': {
'Id': '43',
'CallerReference': self.caller_ref,
'HealthCheckConfig': health_check_config,
'HealthCheckVersion': 1,
},
'Location': 'http://url',
}, {
'CallerReference': ANY,
'HealthCheckConfig': health_check_config,
})
stubber.add_response('change_tags_for_resource', {})
record = Record.new(self.expected, '', {
'ttl': 61,
'type': 'A',
@@ -1191,6 +1349,11 @@ class TestRoute53Provider(TestCase):
# when allowed to create we do
id = provider.get_health_check_id(record, value, True)
self.assertEquals('42', id)
# when allowed to create and when host is None
record._octodns['healthcheck']['host'] = None
id = provider.get_health_check_id(record, value, True)
self.assertEquals('43', id)
stubber.assert_no_pending_responses()
# A CNAME style healthcheck, without a value
@@ -1962,6 +2125,163 @@ class TestRoute53Provider(TestCase):
self.assertEquals(1, len(extra))
stubber.assert_no_pending_responses()
def test_extra_change_dynamic_has_health_check_cname(self):
provider, stubber = self._get_stubbed_provider()
list_hosted_zones_resp = {
'HostedZones': [{
'Name': 'unit.tests.',
'Id': 'z42',
'CallerReference': 'abc',
}],
'Marker': 'm',
'IsTruncated': False,
'MaxItems': '100',
}
stubber.add_response('list_hosted_zones', list_hosted_zones_resp, {})
# record with geo and no health check returns change
desired = Zone('unit.tests.', [])
record = Record.new(desired, 'cname', {
'ttl': 30,
'type': 'CNAME',
'value': 'cname.unit.tests.',
'dynamic': {
'pools': {
'one': {
'values': [{
'value': 'one.cname.unit.tests.',
}],
},
},
'rules': [{
'pool': 'one',
}],
},
})
desired.add_record(record)
list_resource_record_sets_resp = {
'ResourceRecordSets': [{
# Not dynamic value and other name
'Name': 'unit.tests.',
'Type': 'CNAME',
'GeoLocation': {
'CountryCode': '*',
},
'ResourceRecords': [{
'Value': 'cname.unit.tests.',
}],
'TTL': 61,
# All the non-matches have a different Id so we'll fail if they
# match
'HealthCheckId': '33',
}, {
# Not dynamic value, matching name, other type
'Name': 'cname.unit.tests.',
'Type': 'AAAA',
'ResourceRecords': [{
'Value': '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b'
}],
'TTL': 61,
'HealthCheckId': '33',
}, {
# default value pool
'Name': '_octodns-default-value.cname.unit.tests.',
'Type': 'CNAME',
'GeoLocation': {
'CountryCode': '*',
},
'ResourceRecords': [{
'Value': 'cname.unit.tests.',
}],
'TTL': 61,
'HealthCheckId': '33',
}, {
# different record
'Name': '_octodns-two-value.other.unit.tests.',
'Type': 'CNAME',
'GeoLocation': {
'CountryCode': '*',
},
'ResourceRecords': [{
'Value': 'cname.unit.tests.',
}],
'TTL': 61,
'HealthCheckId': '33',
}, {
# same everything, but different type
'Name': '_octodns-one-value.cname.unit.tests.',
'Type': 'AAAA',
'ResourceRecords': [{
'Value': '2001:0db8:3c4d:0015:0000:0000:1a2f:1a4b'
}],
'TTL': 61,
'HealthCheckId': '33',
}, {
# same everything, sub
'Name': '_octodns-one-value.sub.cname.unit.tests.',
'Type': 'CNAME',
'ResourceRecords': [{
'Value': 'cname.unit.tests.',
}],
'TTL': 61,
'HealthCheckId': '33',
}, {
# match
'Name': '_octodns-one-value.cname.unit.tests.',
'Type': 'CNAME',
'ResourceRecords': [{
'Value': 'one.cname.unit.tests.',
}],
'TTL': 61,
'HealthCheckId': '42',
}],
'IsTruncated': False,
'MaxItems': '100',
}
stubber.add_response('list_resource_record_sets',
list_resource_record_sets_resp,
{'HostedZoneId': 'z42'})
stubber.add_response('list_health_checks', {
'HealthChecks': [{
'Id': '42',
'CallerReference': self.caller_ref,
'HealthCheckConfig': {
'Type': 'HTTPS',
'FullyQualifiedDomainName': 'one.cname.unit.tests.',
'ResourcePath': '/_dns',
'Type': 'HTTPS',
'Port': 443,
'MeasureLatency': True,
'RequestInterval': 10,
},
'HealthCheckVersion': 2,
}],
'IsTruncated': False,
'MaxItems': '100',
'Marker': '',
})
extra = provider._extra_changes(desired=desired, changes=[])
self.assertEquals(0, len(extra))
stubber.assert_no_pending_responses()
# change b/c of healthcheck path
record._octodns['healthcheck'] = {
'path': '/_ready'
}
extra = provider._extra_changes(desired=desired, changes=[])
self.assertEquals(1, len(extra))
stubber.assert_no_pending_responses()
# no change b/c healthcheck host ignored for dynamic cname
record._octodns['healthcheck'] = {
'host': 'foo.bar.io'
}
extra = provider._extra_changes(desired=desired, changes=[])
self.assertEquals(0, len(extra))
stubber.assert_no_pending_responses()
def _get_test_plan(self, max_changes):
provider = Route53Provider('test', 'abc', '123', max_changes)
+6 -5
View File
@@ -56,10 +56,11 @@ class MockDomainService(DomainService):
_dns_entries.extend(entries_for(name, record))
# NS is not supported as a DNS Entry,
# so it should cover the if statement
# 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', 'NS', 'ns01.transip.nl.'))
DnsEntry('@', '3600', 'BOGUS', 'ns01.transip.nl.'))
self.mockupEntries = _dns_entries
@@ -222,7 +223,7 @@ N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
provider._client = MockDomainService('unittest', self.bogus_key)
plan = provider.plan(_expected)
self.assertEqual(12, plan.change_counts['Create'])
self.assertEqual(15, plan.change_counts['Create'])
self.assertEqual(0, plan.change_counts['Update'])
self.assertEqual(0, plan.change_counts['Delete'])
@@ -235,7 +236,7 @@ N4OiVz1I3rbZGYa396lpxO6ku8yCglisL1yrSP6DdEUp66ntpKVd
provider = TransipProvider('test', 'unittest', self.bogus_key)
provider._client = MockDomainService('unittest', self.bogus_key)
plan = provider.plan(_expected)
self.assertEqual(12, len(plan.changes))
self.assertEqual(15, len(plan.changes))
changes = provider.apply(plan)
self.assertEqual(changes, len(plan.changes))
+21 -8
View File
@@ -1,8 +1,11 @@
from __future__ import unicode_literals
from mock import Mock, call
from os.path import dirname, join
from requests import HTTPError
from requests_mock import ANY, mock as requests_mock
from six import text_type
from six.moves.urllib import parse
from unittest import TestCase
from json import load as json_load
@@ -55,7 +58,8 @@ class TestUltraProvider(TestCase):
self.assertEquals(1, mock.call_count)
expected_payload = "grant_type=password&username=user&"\
"password=rightpass"
self.assertEquals(mock.last_request.text, expected_payload)
self.assertEquals(parse.parse_qs(mock.last_request.text),
parse.parse_qs(expected_payload))
def test_get_zones(self):
provider = _get_provider()
@@ -274,7 +278,7 @@ class TestUltraProvider(TestCase):
self.assertTrue(provider.populate(zone))
self.assertEquals('octodns1.test.', zone.name)
self.assertEquals(11, len(zone.records))
self.assertEquals(12, len(zone.records))
self.assertEquals(4, mock.call_count)
def test_apply(self):
@@ -285,12 +289,12 @@ class TestUltraProvider(TestCase):
provider._request.side_effect = [
UltraNoZonesExistException('No Zones'),
None, # zone create
] + [None] * 13 # individual record creates
] + [None] * 15 # individual record creates
# non-existent zone, create everything
plan = provider.plan(self.expected)
self.assertEquals(13, len(plan.changes))
self.assertEquals(13, provider.apply(plan))
self.assertEquals(15, len(plan.changes))
self.assertEquals(15, provider.apply(plan))
self.assertFalse(plan.exists)
provider._request.assert_has_calls([
@@ -320,7 +324,7 @@ class TestUltraProvider(TestCase):
'p=A/kinda+of/long/string+with+numb3rs']}),
], True)
# expected number of total calls
self.assertEquals(15, provider._request.call_count)
self.assertEquals(17, provider._request.call_count)
# Create sample rrset payload to attempt to alter
page1 = json_load(open('tests/fixtures/ultra-records-page-1.json'))
@@ -352,8 +356,8 @@ class TestUltraProvider(TestCase):
}))
plan = provider.plan(wanted)
self.assertEquals(10, len(plan.changes))
self.assertEquals(10, provider.apply(plan))
self.assertEquals(11, len(plan.changes))
self.assertEquals(11, provider.apply(plan))
self.assertTrue(plan.exists)
provider._request.assert_has_calls([
@@ -492,6 +496,15 @@ class TestUltraProvider(TestCase):
Record.new(zone, 'txt',
{'ttl': 60, 'type': 'TXT',
'values': ['abc', 'def']})),
# ALIAS
('', 'ALIAS',
'/v2/zones/unit.tests./rrsets/APEXALIAS/unit.tests.',
{'ttl': 60, 'rdata': ['target.unit.tests.']},
Record.new(zone, '',
{'ttl': 60, 'type': 'ALIAS',
'value': 'target.unit.tests.'})),
):
# Validate path and payload based on record meet expectations
path, payload = provider._gen_data(expected_record)
+46 -29
View File
@@ -35,10 +35,10 @@ class TestYamlProvider(TestCase):
# without it we see everything
source.populate(zone)
self.assertEquals(18, len(zone.records))
self.assertEquals(23, len(zone.records))
source.populate(dynamic_zone)
self.assertEquals(5, len(dynamic_zone.records))
self.assertEquals(6, len(dynamic_zone.records))
# Assumption here is that a clean round-trip means that everything
# worked as expected, data that went in came back out and could be
@@ -58,21 +58,21 @@ class TestYamlProvider(TestCase):
# We add everything
plan = target.plan(zone)
self.assertEquals(15, len([c for c in plan.changes
self.assertEquals(20, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertFalse(isfile(yaml_file))
# Now actually do it
self.assertEquals(15, target.apply(plan))
self.assertEquals(20, target.apply(plan))
self.assertTrue(isfile(yaml_file))
# Dynamic plan
plan = target.plan(dynamic_zone)
self.assertEquals(5, len([c for c in plan.changes
self.assertEquals(6, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertFalse(isfile(dynamic_yaml_file))
# Apply it
self.assertEquals(5, target.apply(plan))
self.assertEquals(6, target.apply(plan))
self.assertTrue(isfile(dynamic_yaml_file))
# There should be no changes after the round trip
@@ -87,7 +87,7 @@ class TestYamlProvider(TestCase):
# A 2nd sync should still create everything
plan = target.plan(zone)
self.assertEquals(15, len([c for c in plan.changes
self.assertEquals(20, len([c for c in plan.changes
if isinstance(c, Create)]))
with open(yaml_file) as fh:
@@ -106,9 +106,14 @@ class TestYamlProvider(TestCase):
self.assertTrue('values' in data.pop('naptr'))
self.assertTrue('values' in data.pop('sub'))
self.assertTrue('values' in data.pop('txt'))
self.assertTrue('values' in data.pop('loc'))
self.assertTrue('values' in data.pop('urlfwd'))
# these are stored as singular 'value'
self.assertTrue('value' in data.pop('_imap._tcp'))
self.assertTrue('value' in data.pop('_pop3._tcp'))
self.assertTrue('value' in data.pop('aaaa'))
self.assertTrue('value' in data.pop('cname'))
self.assertTrue('value' in data.pop('dname'))
self.assertTrue('value' in data.pop('included'))
self.assertTrue('value' in data.pop('ptr'))
self.assertTrue('value' in data.pop('spf'))
@@ -144,6 +149,10 @@ class TestYamlProvider(TestCase):
self.assertTrue('value' in dyna)
# self.assertTrue('dynamic' in dyna)
dyna = data.pop('pool-only-in-fallback')
self.assertTrue('value' in dyna)
# self.assertTrue('dynamic' in dyna)
# make sure nothing is left
self.assertEquals([], list(data.keys()))
@@ -206,18 +215,20 @@ class TestSplitYamlProvider(TestCase):
def test_zone_directory(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'))
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
zone = Zone('unit.tests.', [])
self.assertEqual(
join(dirname(__file__), 'config/split/unit.tests.'),
join(dirname(__file__), 'config/split', 'unit.tests.tst'),
source._zone_directory(zone))
def test_apply_handles_existing_zone_directory(self):
with TemporaryDirectory() as td:
provider = SplitYamlProvider('test', join(td.dirname, 'config'))
makedirs(join(td.dirname, 'config', 'does.exist.'))
provider = SplitYamlProvider('test', join(td.dirname, 'config'),
extension='.tst')
makedirs(join(td.dirname, 'config', 'does.exist.tst'))
zone = Zone('does.exist.', [])
self.assertTrue(isdir(provider._zone_directory(zone)))
@@ -226,7 +237,8 @@ class TestSplitYamlProvider(TestCase):
def test_provider(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'))
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
zone = Zone('unit.tests.', [])
dynamic_zone = Zone('dynamic.tests.', [])
@@ -237,7 +249,7 @@ class TestSplitYamlProvider(TestCase):
# without it we see everything
source.populate(zone)
self.assertEquals(18, len(zone.records))
self.assertEquals(20, len(zone.records))
source.populate(dynamic_zone)
self.assertEquals(5, len(dynamic_zone.records))
@@ -245,18 +257,19 @@ class TestSplitYamlProvider(TestCase):
with TemporaryDirectory() as td:
# Add some subdirs to make sure that it can create them
directory = join(td.dirname, 'sub', 'dir')
zone_dir = join(directory, 'unit.tests.')
dynamic_zone_dir = join(directory, 'dynamic.tests.')
target = SplitYamlProvider('test', directory)
zone_dir = join(directory, 'unit.tests.tst')
dynamic_zone_dir = join(directory, 'dynamic.tests.tst')
target = SplitYamlProvider('test', directory,
extension='.tst')
# We add everything
plan = target.plan(zone)
self.assertEquals(15, len([c for c in plan.changes
self.assertEquals(17, len([c for c in plan.changes
if isinstance(c, Create)]))
self.assertFalse(isdir(zone_dir))
# Now actually do it
self.assertEquals(15, target.apply(plan))
self.assertEquals(17, target.apply(plan))
# Dynamic plan
plan = target.plan(dynamic_zone)
@@ -279,7 +292,7 @@ class TestSplitYamlProvider(TestCase):
# A 2nd sync should still create everything
plan = target.plan(zone)
self.assertEquals(15, len([c for c in plan.changes
self.assertEquals(17, len([c for c in plan.changes
if isinstance(c, Create)]))
yaml_file = join(zone_dir, '$unit.tests.yaml')
@@ -294,7 +307,8 @@ class TestSplitYamlProvider(TestCase):
# These records are stored as plural "values." Check each file to
# ensure correctness.
for record_name in ('_srv._tcp', 'mx', 'naptr', 'sub', 'txt'):
for record_name in ('_srv._tcp', 'mx', 'naptr', 'sub', 'txt',
'urlfwd'):
yaml_file = join(zone_dir, '{}.yaml'.format(record_name))
self.assertTrue(isfile(yaml_file))
with open(yaml_file) as fh:
@@ -302,8 +316,8 @@ class TestSplitYamlProvider(TestCase):
self.assertTrue('values' in data.pop(record_name))
# These are stored as singular "value." Again, check each file.
for record_name in ('aaaa', 'cname', 'included', 'ptr', 'spf',
'www.sub', 'www'):
for record_name in ('aaaa', 'cname', 'dname', 'included', 'ptr',
'spf', 'www.sub', 'www'):
yaml_file = join(zone_dir, '{}.yaml'.format(record_name))
self.assertTrue(isfile(yaml_file))
with open(yaml_file) as fh:
@@ -334,7 +348,8 @@ class TestSplitYamlProvider(TestCase):
def test_empty(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'))
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
zone = Zone('empty.', [])
@@ -344,7 +359,8 @@ class TestSplitYamlProvider(TestCase):
def test_unsorted(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'))
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
zone = Zone('unordered.', [])
@@ -355,14 +371,15 @@ class TestSplitYamlProvider(TestCase):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'),
enforce_order=False)
extension='.tst', enforce_order=False)
# no exception
source.populate(zone)
self.assertEqual(2, len(zone.records))
def test_subzone_handling(self):
source = SplitYamlProvider(
'test', join(dirname(__file__), 'config/split'))
'test', join(dirname(__file__), 'config/split'),
extension='.tst')
# If we add `sub` as a sub-zone we'll reject `www.sub`
zone = Zone('unit.tests.', ['sub'])
@@ -386,8 +403,8 @@ class TestOverridingYamlProvider(TestCase):
# Load the base, should see the 5 records
base.populate(zone)
got = {r.name: r for r in zone.records}
self.assertEquals(5, len(got))
# We get the "dynamic" A from the bae config
self.assertEquals(6, len(got))
# We get the "dynamic" A from the base config
self.assertTrue('dynamic' in got['a'].data)
# No added
self.assertFalse('added' in got)
@@ -395,7 +412,7 @@ class TestOverridingYamlProvider(TestCase):
# Load the overrides, should replace one and add 1
override.populate(zone)
got = {r.name: r for r in zone.records}
self.assertEquals(6, len(got))
self.assertEquals(7, len(got))
# 'a' was replaced with a generic record
self.assertEquals({
'ttl': 3600,
File diff suppressed because it is too large Load Diff
+34 -5
View File
@@ -9,6 +9,8 @@ import dns.zone
from dns.exception import DNSException
from mock import patch
from os.path import exists
from shutil import copyfile
from six import text_type
from unittest import TestCase
@@ -21,7 +23,7 @@ from octodns.record import ValidationError
class TestAxfrSource(TestCase):
source = AxfrSource('test', 'localhost')
forward_zonefile = dns.zone.from_file('./tests/zones/unit.tests.',
forward_zonefile = dns.zone.from_file('./tests/zones/unit.tests.tst',
'unit.tests', relativize=False)
@patch('dns.zone.from_xfr')
@@ -34,7 +36,7 @@ class TestAxfrSource(TestCase):
]
self.source.populate(got)
self.assertEquals(11, len(got.records))
self.assertEquals(15, len(got.records))
with self.assertRaises(AxfrSourceZoneTransferFailed) as ctx:
zone = Zone('unit.tests.', [])
@@ -44,18 +46,45 @@ class TestAxfrSource(TestCase):
class TestZoneFileSource(TestCase):
source = ZoneFileSource('test', './tests/zones')
source = ZoneFileSource('test', './tests/zones', file_extension='.tst')
def test_zonefiles_with_extension(self):
source = ZoneFileSource('test', './tests/zones', '.extension')
# Load zonefiles with a specified file extension
valid = Zone('ext.unit.tests.', [])
source.populate(valid)
self.assertEquals(1, len(valid.records))
def test_zonefiles_without_extension(self):
# Windows doesn't let files end with a `.` so we add a .tst to them in
# the repo and then try and create the `.` version we need for the
# default case (no extension.)
copyfile('./tests/zones/unit.tests.tst', './tests/zones/unit.tests.')
# Unfortunately copyfile silently works and create the file without
# the `.` so we have to check to see if it did that
if exists('./tests/zones/unit.tests'):
# It did so we need to skip this test, that means windows won't
# have full code coverage, but skipping the test is going out of
# our way enough for a os-specific/oddball case.
self.skipTest('Unable to create unit.tests. (ending with .) so '
'skipping default filename testing.')
source = ZoneFileSource('test', './tests/zones')
# Load zonefiles without a specified file extension
valid = Zone('unit.tests.', [])
source.populate(valid)
self.assertEquals(15, len(valid.records))
def test_populate(self):
# Valid zone file in directory
valid = Zone('unit.tests.', [])
self.source.populate(valid)
self.assertEquals(11, len(valid.records))
self.assertEquals(15, len(valid.records))
# 2nd populate does not read file again
again = Zone('unit.tests.', [])
self.source.populate(again)
self.assertEquals(11, len(again.records))
self.assertEquals(15, len(again.records))
# bust the cache
del self.source._zone_records[valid.name]
+1 -1
View File
@@ -1,6 +1,6 @@
from mock import patch
from six import text_type
from unittest import TestCase
from unittest.mock import patch
from octodns.source.envvar import EnvVarSource
from octodns.source.envvar import EnvironmentVariableNotFoundException
+56
View File
@@ -355,3 +355,59 @@ class TestZone(TestCase):
self.assertTrue(zone_missing.changes(zone_normal, provider))
self.assertFalse(zone_missing.changes(zone_included, provider))
def assertEqualsNameAndValues(self, a, b):
a = dict([(r.name, r.values[0]) for r in a])
b = dict([(r.name, r.values[0]) for r in b])
self.assertEquals(a, b)
def test_copy(self):
zone = Zone('unit.tests.', [])
a = ARecord(zone, 'a', {'ttl': 42, 'value': '1.1.1.1'})
zone.add_record(a)
b = ARecord(zone, 'b', {'ttl': 42, 'value': '1.1.1.2'})
zone.add_record(b)
# Sanity check
self.assertEqualsNameAndValues(set((a, b)), zone.records)
copy = zone.copy()
# We have an origin set and it is the source/original zone
self.assertEquals(zone, copy._origin)
# Our records are zone's records to start (references)
self.assertEqualsNameAndValues(zone.records, copy.records)
# If we try and change something that's already there we realize and
# then get an error about a duplicate
b_prime = ARecord(zone, 'b', {'ttl': 42, 'value': '1.1.1.3'})
with self.assertRaises(DuplicateRecordException):
copy.add_record(b_prime)
self.assertIsNone(copy._origin)
# Unchanged, straight copies
self.assertEqualsNameAndValues(zone.records, copy.records)
# If we add with replace things will be realized and the record will
# have changed
copy = zone.copy()
copy.add_record(b_prime, replace=True)
self.assertIsNone(copy._origin)
self.assertEqualsNameAndValues(set((a, b_prime)), copy.records)
# If we add another record, things are reliazed and it has been added
copy = zone.copy()
c = ARecord(zone, 'c', {'ttl': 42, 'value': '1.1.1.3'})
copy.add_record(c)
self.assertEqualsNameAndValues(set((a, b, c)), copy.records)
# If we remove a record, things are reliazed and it has been removed
copy = zone.copy()
copy.remove_record(a)
self.assertEqualsNameAndValues(set((b,)), copy.records)
# Re-realizing is a noop
copy = zone.copy()
# Happens the first time
self.assertTrue(copy.hydrate())
# Doesn't the second
self.assertFalse(copy.hydrate())
+12
View File
@@ -0,0 +1,12 @@
$ORIGIN ext.unit.tests.
@ 3600 IN SOA ns1.ext.unit.tests. root.ext.unit.tests. (
2018071501 ; Serial
3600 ; Refresh (1 hour)
600 ; Retry (10 minutes)
604800 ; Expire (1 week)
3600 ; NXDOMAIN ttl (1 hour)
)
; NS Records
@ 3600 IN NS ns1.ext.unit.tests.
@ 3600 IN NS ns2.ext.unit.tests.
@@ -13,9 +13,16 @@ $ORIGIN unit.tests.
under 3600 IN NS ns1.unit.tests.
under 3600 IN NS ns2.unit.tests.
; CAA Records
caa 1800 IN CAA 0 issue "ca.unit.tests"
caa 1800 IN CAA 0 iodef "mailto:[email protected]"
; SRV Records
_srv._tcp 600 IN SRV 10 20 30 foo-1.unit.tests.
_srv._tcp 600 IN SRV 10 20 30 foo-2.unit.tests.
; NULL SRV Records
_pop3._tcp 600 IN SRV 0 0 0 .
_imap._tcp 600 IN SRV 0 0 0 .
; TXT Records
txt 600 IN TXT "Bah bah black sheep"
@@ -28,6 +35,10 @@ mx 300 IN MX 20 smtp-2.unit.tests.
mx 300 IN MX 30 smtp-3.unit.tests.
mx 300 IN MX 40 smtp-1.unit.tests.
; LOC Records
loc 300 IN LOC 31 58 52.1 S 115 49 11.7 E 20m 10m 10m 2m
loc 300 IN LOC 53 14 10 N 2 18 26 W 20m 10m 1000m 2m
; A Records
@ 300 IN A 1.2.3.4
@ 300 IN A 1.2.3.5