octodns-cloudns 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from logging import getLogger
|
|
3
|
+
from requests import Session
|
|
4
|
+
from octodns.provider import ProviderException
|
|
5
|
+
import logging
|
|
6
|
+
from octodns.provider.base import BaseProvider
|
|
7
|
+
from octodns.record import Record
|
|
8
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
__version__ = __VERSION__ = '0.0.2'
|
|
12
|
+
|
|
13
|
+
class ClouDNSClientException(ProviderException):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ClouDNSClientBadRequest(ClouDNSClientException):
|
|
18
|
+
def __init__(self, r):
|
|
19
|
+
super().__init__(r.text)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ClouDNSClientUnauthorized(ClouDNSClientException):
|
|
23
|
+
def __init__(self, r):
|
|
24
|
+
super().__init__(r.text)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClouDNSClientForbidden(ClouDNSClientException):
|
|
28
|
+
def __init__(self, r):
|
|
29
|
+
super().__init__(r.text)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ClouDNSClientNotFound(ClouDNSClientException):
|
|
33
|
+
def __init__(self, r):
|
|
34
|
+
super().__init__(r.text)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ClouDNSClientUnknownDomainName(ClouDNSClientException):
|
|
38
|
+
def __init__(self, msg):
|
|
39
|
+
super().__init__(msg)
|
|
40
|
+
|
|
41
|
+
class ClouDNSClientGeoDNSNotSupported(ClouDNSClientException):
|
|
42
|
+
def __init__(self, msg):
|
|
43
|
+
super().__init__(msg)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ClouDNSClient(object):
|
|
47
|
+
def __init__(self, auth_id, auth_password, sub_auth=False):
|
|
48
|
+
session = Session()
|
|
49
|
+
session.headers.update(
|
|
50
|
+
{
|
|
51
|
+
"Authorization": f"Bearer {auth_id}:{auth_password}",
|
|
52
|
+
"User-Agent": f"cloudns/{__version__} octodns-cloudns/{__VERSION__}",
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
self._session = session
|
|
56
|
+
if sub_auth:
|
|
57
|
+
self._auth_type = 'sub-auth-id'
|
|
58
|
+
else:
|
|
59
|
+
self._auth_type = 'auth-id'
|
|
60
|
+
|
|
61
|
+
self.auth_id = auth_id
|
|
62
|
+
self.auth_password = auth_password
|
|
63
|
+
|
|
64
|
+
# Currently hard-coded, but could offer XML in the future
|
|
65
|
+
self._type = 'json'
|
|
66
|
+
|
|
67
|
+
self._urlbase = 'https://api.cloudns.net/{0}.{1}?{4}={2}&auth-password={3}&{0}'.format(
|
|
68
|
+
'{}', self._type, self.auth_id, self.auth_password, self._auth_type)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _request(self, function, params=''):
|
|
72
|
+
response = self._raw_request(function, params)
|
|
73
|
+
if self._type == 'json':
|
|
74
|
+
return response.json()
|
|
75
|
+
|
|
76
|
+
def _raw_request(self, function, params=''):
|
|
77
|
+
url = self._urlbase.format(function, params)
|
|
78
|
+
logger.debug(f"Request URL: {url}")
|
|
79
|
+
response = self._session.get(url)
|
|
80
|
+
logger.debug(f"Request Response: {response.text}")
|
|
81
|
+
return response
|
|
82
|
+
|
|
83
|
+
def _handle_response(self, response):
|
|
84
|
+
status_code = response.status_code
|
|
85
|
+
if status_code == 400:
|
|
86
|
+
raise ClouDNSClientBadRequest(response)
|
|
87
|
+
elif status_code == 401:
|
|
88
|
+
raise ClouDNSClientUnauthorized(response)
|
|
89
|
+
elif status_code == 403:
|
|
90
|
+
raise ClouDNSClientForbidden(response)
|
|
91
|
+
elif status_code == 404:
|
|
92
|
+
raise ClouDNSClientNotFound(response)
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
def checkDot(self, domain_name):
|
|
95
|
+
if domain_name.endswith('.'):
|
|
96
|
+
domain_name = domain_name[:-1]
|
|
97
|
+
return domain_name
|
|
98
|
+
|
|
99
|
+
def zone_create(self, domain_name, zone_type, master_ip=''):
|
|
100
|
+
params = 'domain-name={}&zone-type={}&master-ip={}'.format(domain_name, zone_type, master_ip)
|
|
101
|
+
return self._request('dns/register', params)
|
|
102
|
+
|
|
103
|
+
def zone(self, domain_name):
|
|
104
|
+
params = 'domain-name={}'.format(domain_name)
|
|
105
|
+
return self._request('dns/get-zone-info', params)
|
|
106
|
+
|
|
107
|
+
def zone_records(self, domain_name):
|
|
108
|
+
params = 'domain-name={}'.format(domain_name)
|
|
109
|
+
return self._request('dns/records', params)
|
|
110
|
+
|
|
111
|
+
def record_create(self, domain_name, rrset_type, rrset_name, rrset_values, rrset_ttl=3600, geodns=False, rrset_locations = None, status=1):
|
|
112
|
+
if (rrset_name == '@'):
|
|
113
|
+
rrset_name = ''
|
|
114
|
+
|
|
115
|
+
params = 'domain-name={}&record-type={}&host={}&ttl={}&status={}'.format(
|
|
116
|
+
domain_name, rrset_type, rrset_name, rrset_ttl, status)
|
|
117
|
+
|
|
118
|
+
single_types = ['CNAME', 'A', 'AAAA', 'DNAME', 'ALIAS', 'NS', 'PTR', 'SPF', 'TXT']
|
|
119
|
+
if rrset_type in single_types:
|
|
120
|
+
params += '&record={}'.format(rrset_values[0].replace('\;', ';'))
|
|
121
|
+
|
|
122
|
+
if(geodns is True):
|
|
123
|
+
for location in rrset_locations:
|
|
124
|
+
params += '&geodns-code={}'.format(location)
|
|
125
|
+
self._request('dns/add-record', params)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
if rrset_type == 'MX':
|
|
129
|
+
values = rrset_values[0]
|
|
130
|
+
|
|
131
|
+
priority = values.preference
|
|
132
|
+
record = values.exchange
|
|
133
|
+
|
|
134
|
+
record = self.checkDot(record)
|
|
135
|
+
params += '&priority={}&record={}'.format(priority,record)
|
|
136
|
+
|
|
137
|
+
if rrset_type == 'SSHFP':
|
|
138
|
+
sshfp_value = rrset_values[0]
|
|
139
|
+
algorithm = sshfp_value.algorithm
|
|
140
|
+
fptype = sshfp_value.fingerprint_type
|
|
141
|
+
record = sshfp_value.fingerprint
|
|
142
|
+
|
|
143
|
+
params += '&algorithm={}&fptype={}&record={}'.format(algorithm, fptype, record)
|
|
144
|
+
|
|
145
|
+
if rrset_type == 'SRV':
|
|
146
|
+
values = rrset_values[0]
|
|
147
|
+
|
|
148
|
+
srv_value = rrset_values[0]
|
|
149
|
+
priority = srv_value.priority
|
|
150
|
+
weight = srv_value.weight
|
|
151
|
+
port = srv_value.port
|
|
152
|
+
record = srv_value.target
|
|
153
|
+
|
|
154
|
+
params += '&priority={}&weight={}&port={}&record={}'.format(priority, weight, port,record)
|
|
155
|
+
|
|
156
|
+
if rrset_type == 'CAA':
|
|
157
|
+
caa_value = rrset_values[0]
|
|
158
|
+
|
|
159
|
+
flag = caa_value.flags
|
|
160
|
+
caa_type = caa_value.tag
|
|
161
|
+
caa_value = caa_value.value
|
|
162
|
+
params += '&caa_flag={}&caa_type={}&caa_value={}'.format(flag, caa_type, caa_value)
|
|
163
|
+
|
|
164
|
+
if rrset_type == 'LOC':
|
|
165
|
+
values = rrset_values[0]
|
|
166
|
+
|
|
167
|
+
loc_value = rrset_values[0]
|
|
168
|
+
lat_deg = loc_value.lat_degrees
|
|
169
|
+
lat_min = loc_value.lat_minutes
|
|
170
|
+
lat_sec = loc_value.lat_seconds
|
|
171
|
+
lat_dir = loc_value.lat_direction
|
|
172
|
+
long_deg = loc_value.long_degrees
|
|
173
|
+
long_min = loc_value.long_minutes
|
|
174
|
+
long_sec = loc_value.long_seconds
|
|
175
|
+
long_dir = loc_value.long_direction
|
|
176
|
+
altitude = loc_value.altitude
|
|
177
|
+
size = loc_value.size
|
|
178
|
+
h_precision = loc_value.precision_horz
|
|
179
|
+
v_precision = loc_value.precision_vert
|
|
180
|
+
|
|
181
|
+
params += '&lat-deg={}&lat-min={}&lat-sec={}&lat-dir={}&long-deg={}&long-min={}&long-sec={}&long-dir={}&altitude={}&size={}&h-precision={}&v-precision={}'.format(
|
|
182
|
+
lat_deg, lat_min, lat_sec, lat_dir, long_deg, long_min, long_sec, long_dir, altitude, size, h_precision, v_precision)
|
|
183
|
+
|
|
184
|
+
if rrset_type == 'NAPTR':
|
|
185
|
+
values = rrset_values[0]
|
|
186
|
+
|
|
187
|
+
naptr_value = rrset_values[0]
|
|
188
|
+
order = naptr_value.order
|
|
189
|
+
pref = naptr_value.preference
|
|
190
|
+
flag = naptr_value.flags
|
|
191
|
+
params_naptr = naptr_value.service
|
|
192
|
+
|
|
193
|
+
params += '&order={}&pref={}&flag={}¶ms={}'.format(order, pref, flag, params_naptr)
|
|
194
|
+
if hasattr(naptr_value, 'replacement'):
|
|
195
|
+
replace = naptr_value.replacement
|
|
196
|
+
params += '&replace={}'.format(replace)
|
|
197
|
+
|
|
198
|
+
if hasattr(naptr_value, 'regexp'):
|
|
199
|
+
regexp = naptr_value.regexp
|
|
200
|
+
params += '®exp={}'.format(regexp)
|
|
201
|
+
|
|
202
|
+
if rrset_type == 'TLSA':
|
|
203
|
+
tlsa_values = rrset_values[0]
|
|
204
|
+
|
|
205
|
+
record = tlsa_values.certificate_association_data
|
|
206
|
+
tlsa_usage = tlsa_values.certificate_usage
|
|
207
|
+
tlsa_selector = tlsa_values.selector
|
|
208
|
+
tlsa_matching_type = tlsa_values.matching_type
|
|
209
|
+
|
|
210
|
+
params += '&record={}&tlsa_usage={}&tlsa_selector={}&tlsa_matching_type={}'.format(record, tlsa_usage, tlsa_selector, tlsa_matching_type)
|
|
211
|
+
|
|
212
|
+
return self._request('dns/add-record', params)
|
|
213
|
+
|
|
214
|
+
def record_delete(self, domain_name, record_id):
|
|
215
|
+
params = 'domain-name={}&record-id={}'.format(domain_name, record_id)
|
|
216
|
+
return self._request('dns/delete-record', params)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class ClouDNSProvider(BaseProvider):
|
|
220
|
+
SUPPORTS_GEO = True
|
|
221
|
+
SUPPORTS_DYNAMIC = False
|
|
222
|
+
SUPPORTS_ROOT_NS = True
|
|
223
|
+
SUPPORTS = set(
|
|
224
|
+
[
|
|
225
|
+
"A",
|
|
226
|
+
"AAAA",
|
|
227
|
+
"ALIAS",
|
|
228
|
+
"CAA",
|
|
229
|
+
"CNAME",
|
|
230
|
+
"DNAME",
|
|
231
|
+
"MX",
|
|
232
|
+
"NS",
|
|
233
|
+
"PTR",
|
|
234
|
+
"SPF",
|
|
235
|
+
"SRV",
|
|
236
|
+
"SSHFP",
|
|
237
|
+
"TXT",
|
|
238
|
+
"TLSA",
|
|
239
|
+
"LOC",
|
|
240
|
+
"NAPTR",
|
|
241
|
+
]
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
def __init__(self, id, auth_id, auth_password, *args, **kwargs):
|
|
245
|
+
self.log = getLogger(f"ClouDNSProvider[{id}]")
|
|
246
|
+
self.log.debug("__init__: id=%s, auth_id=***", id)
|
|
247
|
+
super().__init__(id, *args, **kwargs)
|
|
248
|
+
self._client = ClouDNSClient(auth_id, auth_password)
|
|
249
|
+
|
|
250
|
+
self._zone_records = {}
|
|
251
|
+
|
|
252
|
+
def _data_for_multiple(self, _type, records):
|
|
253
|
+
return {
|
|
254
|
+
"ttl": records[0]["ttl"],
|
|
255
|
+
"type": _type,
|
|
256
|
+
"values": [v["record"] + "." if v["type"] not in ["A", "AAAA", "TXT", "SPF"] else v["record"] for v in records],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
def _data_for_TXT(self, _type, records):
|
|
260
|
+
return {
|
|
261
|
+
"ttl": records[0]["ttl"],
|
|
262
|
+
"type": _type,
|
|
263
|
+
"value": records[0]["record"].replace(';', '\\;') + ".",
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
_data_for_A = _data_for_multiple
|
|
268
|
+
_data_for_AAAA = _data_for_multiple
|
|
269
|
+
_data_for_SPF = _data_for_multiple
|
|
270
|
+
_data_for_NS = _data_for_multiple
|
|
271
|
+
|
|
272
|
+
def zone(self, zone_name):
|
|
273
|
+
return self._client.zone(zone_name)
|
|
274
|
+
|
|
275
|
+
def zone_create(self, zone_name, zone_type, master_ip=None):
|
|
276
|
+
return self._client.zone_create(zone_name, zone_type, master_ip=master_ip)
|
|
277
|
+
|
|
278
|
+
def _data_for_CAA(self, _type, records):
|
|
279
|
+
values = []
|
|
280
|
+
for record in records:
|
|
281
|
+
values.append(
|
|
282
|
+
{
|
|
283
|
+
"flags": record['caa_flag'],
|
|
284
|
+
"tag": record['caa_type'],
|
|
285
|
+
"value": record['caa_value']
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return {"ttl": records[0]["ttl"], "type": _type, "values": values}
|
|
290
|
+
|
|
291
|
+
def _data_for_single(self, _type, records):
|
|
292
|
+
return {
|
|
293
|
+
"ttl": records[0]["ttl"],
|
|
294
|
+
"type": _type,
|
|
295
|
+
"value": records[0]["record"] + ".",
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
_data_for_ALIAS = _data_for_single
|
|
299
|
+
_data_for_CNAME = _data_for_single
|
|
300
|
+
_data_for_DNAME = _data_for_single
|
|
301
|
+
_data_for_PTR = _data_for_single
|
|
302
|
+
|
|
303
|
+
def _data_for_MX(self, _type, records):
|
|
304
|
+
values = []
|
|
305
|
+
for record in records:
|
|
306
|
+
if 'priority' in record and 'record' in record:
|
|
307
|
+
values.append({"preference": record['priority'], "exchange": record['record'] + '.'})
|
|
308
|
+
return {"ttl": records[0]["ttl"], "type": _type, "values": values}
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _data_for_SRV(self, _type, records):
|
|
313
|
+
values = []
|
|
314
|
+
for record in records:
|
|
315
|
+
values.append({"priority": record['priority'], "weight": record['weight'] ,"port": record['port'], "target": record['record'] + '.'})
|
|
316
|
+
return {"ttl": record["ttl"], "type": _type, "values": values}
|
|
317
|
+
|
|
318
|
+
def _data_for_LOC(self, _type, records):
|
|
319
|
+
values = []
|
|
320
|
+
for record in records:
|
|
321
|
+
values.append({"lat_degrees": record['lat_deg'], "lat_minutes": record['lat_min'] ,"lat_seconds": record['lat_sec'], "lat_direction": record['lat_dir'],
|
|
322
|
+
"long_degrees": record['long_deg'], "long_minutes": record['long_min'], "long_seconds": record['long_sec'], "long_direction": record['long_dir'],
|
|
323
|
+
"altitude": record['altitude'], "size": record['size'], "precision_horz": record['h_precision'], "precision_vert": record['v_precision']})
|
|
324
|
+
return {"ttl": record["ttl"], "type": _type, "values": values}
|
|
325
|
+
|
|
326
|
+
def _data_for_SSHFP(self, _type, records):
|
|
327
|
+
values = []
|
|
328
|
+
for record in records:
|
|
329
|
+
values.append({"algorithm": record['algorithm'], "fingerprint_type": record['fp_type'] ,"fingerprint": record['record']})
|
|
330
|
+
return {"ttl": records[0]["ttl"], "type": _type, "values": values}
|
|
331
|
+
|
|
332
|
+
def _data_for_NAPTR(self, _type, records):
|
|
333
|
+
values = []
|
|
334
|
+
for record in records:
|
|
335
|
+
values.append({"order": record['order'], "preference": record['pref'], "flags": record['flag'], "service": record['params'],
|
|
336
|
+
"regexp": record['regexp'], "replacement": record['replace']})
|
|
337
|
+
return {"ttl": records[0]["ttl"], "type": _type, "values": values}
|
|
338
|
+
|
|
339
|
+
def _data_for_TLSA(self, _type, records):
|
|
340
|
+
values = []
|
|
341
|
+
for record in records:
|
|
342
|
+
values.append({"certificate_association_data": record['record'], "certificate_usage": record['tlsa_usage'], "selector": record['tlsa_selector'],
|
|
343
|
+
"matching_type": record['tlsa_matching_type']})
|
|
344
|
+
return {"ttl": records[0]["ttl"], "type": _type, "values": values}
|
|
345
|
+
|
|
346
|
+
def zone_records(self, zone):
|
|
347
|
+
if zone.name not in self._zone_records:
|
|
348
|
+
try:
|
|
349
|
+
self._zone_records[zone.name] = self._client.zone_records(zone.name[:-1])
|
|
350
|
+
except ClouDNSClientNotFound:
|
|
351
|
+
return []
|
|
352
|
+
return self._zone_records[zone.name]
|
|
353
|
+
|
|
354
|
+
def isGeoDNS(self, statusDescription):
|
|
355
|
+
if statusDescription == 'Your plan supports only GeoDNS zones.':
|
|
356
|
+
return True
|
|
357
|
+
else:
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
def populate(self, zone, target=False, lenient=False):
|
|
361
|
+
self.log.debug(
|
|
362
|
+
"populate: name=%s, target=%s, lenient=%s",
|
|
363
|
+
zone.name,
|
|
364
|
+
target,
|
|
365
|
+
lenient,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
values = defaultdict(lambda: defaultdict(list))
|
|
369
|
+
records_data = self.zone_records(zone)
|
|
370
|
+
|
|
371
|
+
if 'status' in records_data and records_data['status'] == 'Failed':
|
|
372
|
+
self.log.info("populate: no existing zone, trying to create it")
|
|
373
|
+
response = self._client.zone_create(zone.name[:-1], 'master')
|
|
374
|
+
if 'id' in response and response['id'] == 'not_found':
|
|
375
|
+
e = ClouDNSClientUnknownDomainName(
|
|
376
|
+
'Missing domain name'
|
|
377
|
+
)
|
|
378
|
+
e.__cause__ = None
|
|
379
|
+
raise e
|
|
380
|
+
|
|
381
|
+
if (self.isGeoDNS(response['statusDescription'])):
|
|
382
|
+
response = self._client.zone_create(zone.name[:-1], 'geodns')
|
|
383
|
+
|
|
384
|
+
if(response['status'] == 'Failed'):
|
|
385
|
+
e = ClouDNSClientUnknownDomainName(f"{response['status']} : {response['statusDescription']}")
|
|
386
|
+
e.__cause__ = None
|
|
387
|
+
raise e
|
|
388
|
+
self.log.info("populate: zone has been successfully created")
|
|
389
|
+
records_data = self._client.zone_records(zone.name[:-1])
|
|
390
|
+
|
|
391
|
+
for record_id, record in records_data.items():
|
|
392
|
+
_type = record["type"]
|
|
393
|
+
|
|
394
|
+
if _type not in self.SUPPORTS:
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
values[record["host"]][_type].append(record)
|
|
398
|
+
before = len(records_data.items())
|
|
399
|
+
for name, types in values.items():
|
|
400
|
+
for _type, records in types.items():
|
|
401
|
+
data_for = getattr(self, f"_data_for_{_type}")
|
|
402
|
+
record = Record.new(
|
|
403
|
+
zone,
|
|
404
|
+
name,
|
|
405
|
+
data_for(_type, records),
|
|
406
|
+
source=self,
|
|
407
|
+
lenient=lenient,
|
|
408
|
+
)
|
|
409
|
+
zone.add_record(record, lenient=lenient)
|
|
410
|
+
exists = zone.name in self._zone_records
|
|
411
|
+
self.log.info(
|
|
412
|
+
"populate: found %s records, exists=%s",
|
|
413
|
+
len(zone.records) - before,
|
|
414
|
+
exists,
|
|
415
|
+
)
|
|
416
|
+
return exists
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _record_name(self, name):
|
|
420
|
+
return name if name else ""
|
|
421
|
+
|
|
422
|
+
def _params_for_multiple(self, record):
|
|
423
|
+
return {
|
|
424
|
+
"rrset_name": self._record_name(record.name),
|
|
425
|
+
"rrset_ttl": record.ttl,
|
|
426
|
+
"rrset_type": record._type,
|
|
427
|
+
"rrset_values": [str(v) for v in record.values]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
def _params_for_geo(self, record):
|
|
431
|
+
geo_location = record.geo
|
|
432
|
+
locations = []
|
|
433
|
+
for code, geo_value in geo_location.items():
|
|
434
|
+
continent_code = geo_value.continent_code
|
|
435
|
+
country_code = geo_value.country_code
|
|
436
|
+
subdivision_code = geo_value.subdivision_code
|
|
437
|
+
|
|
438
|
+
if subdivision_code is not None:
|
|
439
|
+
locations.append(subdivision_code)
|
|
440
|
+
elif country_code is not None:
|
|
441
|
+
locations.append(country_code)
|
|
442
|
+
elif continent_code is not None:
|
|
443
|
+
locations.append(continent_code)
|
|
444
|
+
else:
|
|
445
|
+
locations = 0
|
|
446
|
+
|
|
447
|
+
return{
|
|
448
|
+
"geodns": True,
|
|
449
|
+
"rrset_name": self._record_name(record.name),
|
|
450
|
+
"rrset_ttl": record.ttl,
|
|
451
|
+
"rrset_type": record._type,
|
|
452
|
+
"rrset_values": [str(v) for v in record.values],
|
|
453
|
+
"rrset_locations": [str(v) for v in locations]
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _params_for_A_AAAA(self, record):
|
|
458
|
+
if getattr(record, 'geo', False):
|
|
459
|
+
return self._params_for_geo(record)
|
|
460
|
+
return {
|
|
461
|
+
"rrset_name": self._record_name(record.name),
|
|
462
|
+
"rrset_ttl": record.ttl,
|
|
463
|
+
"rrset_type": record._type,
|
|
464
|
+
"rrset_values": [str(v) for v in record.values]
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_params_for_A = _params_for_A_AAAA
|
|
468
|
+
_params_for_AAAA = _params_for_A_AAAA
|
|
469
|
+
_params_for_NS = _params_for_multiple
|
|
470
|
+
_params_for_TXT = _params_for_multiple
|
|
471
|
+
_params_for_SPF = _params_for_multiple
|
|
472
|
+
|
|
473
|
+
def _params_for_CAA(self, record):
|
|
474
|
+
return {
|
|
475
|
+
"rrset_name": self._record_name(record.name),
|
|
476
|
+
"rrset_ttl": record.ttl,
|
|
477
|
+
"rrset_type": record._type,
|
|
478
|
+
"rrset_values": [f'{v.flags} {v.tag} "{v.value}"' for v in record.values],
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
def _params_for_single(self, record):
|
|
482
|
+
return {
|
|
483
|
+
"rrset_name": self._record_name(record.name),
|
|
484
|
+
"rrset_ttl": record.ttl,
|
|
485
|
+
"rrset_type": record._type,
|
|
486
|
+
"rrset_values": [record.value],
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
_params_for_ALIAS = _params_for_single
|
|
490
|
+
_params_for_CNAME = _params_for_single
|
|
491
|
+
_params_for_DNAME = _params_for_single
|
|
492
|
+
_params_for_PTR = _params_for_single
|
|
493
|
+
|
|
494
|
+
def _params_for_MX(self, record):
|
|
495
|
+
return {
|
|
496
|
+
"rrset_name": self._record_name(record.name),
|
|
497
|
+
"rrset_ttl": record.ttl,
|
|
498
|
+
"rrset_type": record._type,
|
|
499
|
+
"rrset_values": [f"{v.preference} {v.exchange}" for v in record.values],
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
def _params_for_SRV(self, record):
|
|
503
|
+
return {
|
|
504
|
+
"rrset_name": self._record_name(record.name),
|
|
505
|
+
"rrset_ttl": record.ttl,
|
|
506
|
+
"rrset_type": record._type,
|
|
507
|
+
"rrset_values": [
|
|
508
|
+
f"{v.priority} {v.weight} {v.port} {v.target}" for v in record.values
|
|
509
|
+
],
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
def _params_for_SSHFP(self, record):
|
|
513
|
+
return {
|
|
514
|
+
"rrset_name": self._record_name(record.name),
|
|
515
|
+
"rrset_ttl": record.ttl,
|
|
516
|
+
"rrset_type": record._type,
|
|
517
|
+
"rrset_values": [
|
|
518
|
+
f"{v.algorithm} {v.fingerprint_type} " f"{v.fingerprint}"
|
|
519
|
+
for v in record.values
|
|
520
|
+
],
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
def _params_for_LOC(self, record):
|
|
524
|
+
return {
|
|
525
|
+
"rrset_name": self._record_name(record.name),
|
|
526
|
+
"rrset_ttl": record.ttl,
|
|
527
|
+
"rrset_type": record._type,
|
|
528
|
+
"rrset_values": [
|
|
529
|
+
f"{v.lat_degrees} {v.lat_minutes} {v.lat_seconds} {v.lat_direction} "
|
|
530
|
+
f"{v.long_degrees} {v.long_minutes} {v.long_seconds} {v.long_direction} {v.altitude} {v.size} {v.precision_horz} {v.precision_vert} "
|
|
531
|
+
for v in record.values
|
|
532
|
+
],
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
def _params_for_NAPTR(self, record):
|
|
536
|
+
return {
|
|
537
|
+
"rrset_name": self._record_name(record.name),
|
|
538
|
+
"rrset_ttl": record.ttl,
|
|
539
|
+
"rrset_type": record._type,
|
|
540
|
+
"rrset_values": [
|
|
541
|
+
f"{v.order} {v.preference} {v.flags} {v.service} {v.regexp} {v.replacement}"
|
|
542
|
+
for v in record.values
|
|
543
|
+
],
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
def _params_for_TLSA(self, record):
|
|
547
|
+
return {
|
|
548
|
+
"rrset_name": self._record_name(record.name),
|
|
549
|
+
"rrset_ttl": record.ttl,
|
|
550
|
+
"rrset_type": record._type,
|
|
551
|
+
"rrset_values": [
|
|
552
|
+
f"{v.certificate_association_data} {v.certificate_usage} {v.selector} {v.matching_type}"
|
|
553
|
+
for v in record.values
|
|
554
|
+
],
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
def _apply_create(self, change):
|
|
558
|
+
new = change.new
|
|
559
|
+
if hasattr(new, 'values'):
|
|
560
|
+
for value in new.values:
|
|
561
|
+
data = getattr(self, f"_params_for_{new._type}")(new)
|
|
562
|
+
if ('rrset_values' in data):
|
|
563
|
+
data['rrset_values'] = [value]
|
|
564
|
+
self._client.record_create(new.zone.name[:-1], **data)
|
|
565
|
+
else:
|
|
566
|
+
data = getattr(self, f"_params_for_{new._type}")(new)
|
|
567
|
+
else:
|
|
568
|
+
data = getattr(self, f"_params_for_{new._type}")(new)
|
|
569
|
+
self._client.record_create(new.zone.name[:-1], **data)
|
|
570
|
+
|
|
571
|
+
def _apply_update(self, change):
|
|
572
|
+
self._apply_delete(change)
|
|
573
|
+
self._apply_create(change)
|
|
574
|
+
|
|
575
|
+
def records_are_same(self, existing):
|
|
576
|
+
zone = existing.zone
|
|
577
|
+
record_ids = []
|
|
578
|
+
for record_id, record in self.zone_records(zone).items():
|
|
579
|
+
for value in existing.values:
|
|
580
|
+
if existing._type == 'NAPTR' and record['type'] == 'NAPTR':
|
|
581
|
+
if (
|
|
582
|
+
existing.name == record['host']
|
|
583
|
+
and value.order == int(record['order'])
|
|
584
|
+
and value.preference == int(record['pref'])
|
|
585
|
+
and value.flags == record['flag']
|
|
586
|
+
):
|
|
587
|
+
record_ids.append(record_id)
|
|
588
|
+
elif existing._type == 'SSHFP' and record['type'] == 'SSHFP':
|
|
589
|
+
if (
|
|
590
|
+
existing.name == record['host']
|
|
591
|
+
and value.fingerprint_type == int(record['fp_type'])
|
|
592
|
+
and value.algorithm == int(record['algorithm'])
|
|
593
|
+
and value.fingerprint == record['record']
|
|
594
|
+
):
|
|
595
|
+
record_ids.append(record_id)
|
|
596
|
+
elif existing._type == 'SRV' and record['type'] == 'SRV':
|
|
597
|
+
if (
|
|
598
|
+
existing.name == record['host']
|
|
599
|
+
and value.priority == int(record['priority'])
|
|
600
|
+
and value.weight == int(record['weight'])
|
|
601
|
+
and value.port == record['port']
|
|
602
|
+
and value.target == record['record']
|
|
603
|
+
):
|
|
604
|
+
record_ids.append(record_id)
|
|
605
|
+
elif existing._type == 'CAA' and record['type'] == 'CAA':
|
|
606
|
+
if (
|
|
607
|
+
existing.name == record['host']
|
|
608
|
+
and value.flags == record['caa_flag']
|
|
609
|
+
and value.tag == record['caa_type']
|
|
610
|
+
and value.value == record['caa_value']
|
|
611
|
+
):
|
|
612
|
+
record_ids.append(record_id)
|
|
613
|
+
elif existing._type == 'MX' and record['type'] == 'MX':
|
|
614
|
+
if (
|
|
615
|
+
existing.name == record['host']
|
|
616
|
+
and value.preference == int(record['priority'])
|
|
617
|
+
and value.exchange == record['record']
|
|
618
|
+
):
|
|
619
|
+
record_ids.append(record_id)
|
|
620
|
+
|
|
621
|
+
elif existing._type == 'LOC' and record['type'] == 'LOC':
|
|
622
|
+
if (
|
|
623
|
+
existing.name == record['host']
|
|
624
|
+
and value.lat_degrees == record['lat_deg']
|
|
625
|
+
and value.lat_minutes == record['lat_min']
|
|
626
|
+
and value.lat_seconds == record['lat_sec']
|
|
627
|
+
and value.lat_direction == record['lat_dir']
|
|
628
|
+
and value.long_degrees == record['long_deg']
|
|
629
|
+
and value.long_minutes == record['long_min']
|
|
630
|
+
and value.long_seconds == record['long_sec']
|
|
631
|
+
and value.long_direction == record['long_dir']
|
|
632
|
+
and value.altitude == record['altitude']
|
|
633
|
+
and value.size == record['size']
|
|
634
|
+
and value.precision_horz == record['h_precision']
|
|
635
|
+
and value.precision_vert == record['v_precision']
|
|
636
|
+
):
|
|
637
|
+
record_ids.append(record_id)
|
|
638
|
+
else:
|
|
639
|
+
if (record == 'Failed' or record == 'Missing domain-name'):
|
|
640
|
+
continue
|
|
641
|
+
if (
|
|
642
|
+
existing.name == record['host']
|
|
643
|
+
and existing._type == record['type']
|
|
644
|
+
and value == record['record']
|
|
645
|
+
):
|
|
646
|
+
record_ids.append(record_id)
|
|
647
|
+
return record_ids
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _apply_delete(self, change):
|
|
651
|
+
existing = change.existing
|
|
652
|
+
zone = existing.zone
|
|
653
|
+
record_ids = self.records_are_same(existing)
|
|
654
|
+
|
|
655
|
+
for record_id in record_ids:
|
|
656
|
+
self._client.record_delete(zone.name[:-1], record_id)
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _apply(self, plan):
|
|
660
|
+
desired = plan.desired
|
|
661
|
+
|
|
662
|
+
changes = plan.changes
|
|
663
|
+
zone = desired.name[:-1]
|
|
664
|
+
self.log.debug("_apply: zone=%s, len(changes)=%d", desired.name, len(changes))
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
self._client.zone(zone)
|
|
668
|
+
except ClouDNSClientNotFound:
|
|
669
|
+
self.log.info("_apply: no existing zone, trying to create it")
|
|
670
|
+
try:
|
|
671
|
+
self._client.zone_create(zone, 'master')
|
|
672
|
+
self.log.info("_apply: zone has been successfully created")
|
|
673
|
+
except ClouDNSClientNotFound:
|
|
674
|
+
e = ClouDNSClientUnknownDomainName(
|
|
675
|
+
"Domain " + zone + " is not "
|
|
676
|
+
"registered at ClouDNS. "
|
|
677
|
+
"Please register or "
|
|
678
|
+
"transfer it here "
|
|
679
|
+
"to be able to manage its "
|
|
680
|
+
"DNS zone."
|
|
681
|
+
)
|
|
682
|
+
e.__cause__ = None
|
|
683
|
+
raise e
|
|
684
|
+
|
|
685
|
+
for change in changes:
|
|
686
|
+
class_name = change.__class__.__name__
|
|
687
|
+
getattr(self, f"_apply_{class_name.lower()}")(change)
|
|
688
|
+
|
|
689
|
+
# Clear out the cache if any
|
|
690
|
+
self._zone_records.pop(desired.name, None)
|