octodns-cloudns 0.0.5__py3-none-any.whl → 0.0.7__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.
@@ -1,709 +1,709 @@
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.5'
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('\;', ';').replace('+', '%2B'))
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={}&params={}'.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 += '&regexp={}'.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
- "values": [
264
- (record["record"].replace(';', '\\;').rstrip('.'))
265
- for record in records
266
- ]
267
- }
268
-
269
-
270
-
271
- _data_for_A = _data_for_multiple
272
- _data_for_AAAA = _data_for_multiple
273
- _data_for_SPF = _data_for_multiple
274
- _data_for_NS = _data_for_multiple
275
-
276
- def zone(self, zone_name):
277
- return self._client.zone(zone_name)
278
-
279
- def zone_create(self, zone_name, zone_type, master_ip=None):
280
- return self._client.zone_create(zone_name, zone_type, master_ip=master_ip)
281
-
282
- def _data_for_CAA(self, _type, records):
283
- values = []
284
- for record in records:
285
- values.append(
286
- {
287
- "flags": record['caa_flag'],
288
- "tag": record['caa_type'],
289
- "value": record['caa_value']
290
- }
291
- )
292
-
293
- return {"ttl": records[0]["ttl"], "type": _type, "values": values}
294
-
295
- def _data_for_single(self, _type, records):
296
- return {
297
- "ttl": records[0]["ttl"],
298
- "type": _type,
299
- "value": records[0]["record"] + ".",
300
- }
301
-
302
- _data_for_ALIAS = _data_for_single
303
- _data_for_CNAME = _data_for_single
304
- _data_for_DNAME = _data_for_single
305
- _data_for_PTR = _data_for_single
306
-
307
- def _data_for_MX(self, _type, records):
308
- values = []
309
- for record in records:
310
- if 'priority' in record and 'record' in record:
311
- values.append({"preference": record['priority'], "exchange": record['record'] + '.'})
312
- return {"ttl": records[0]["ttl"], "type": _type, "values": values}
313
-
314
-
315
-
316
- def _data_for_SRV(self, _type, records):
317
- values = []
318
- for record in records:
319
- values.append({"priority": record['priority'], "weight": record['weight'] ,"port": record['port'], "target": record['record'] + '.'})
320
- return {"ttl": record["ttl"], "type": _type, "values": values}
321
-
322
- def _data_for_LOC(self, _type, records):
323
- values = []
324
- for record in records:
325
- values.append({"lat_degrees": record['lat_deg'], "lat_minutes": record['lat_min'] ,"lat_seconds": record['lat_sec'], "lat_direction": record['lat_dir'],
326
- "long_degrees": record['long_deg'], "long_minutes": record['long_min'], "long_seconds": record['long_sec'], "long_direction": record['long_dir'],
327
- "altitude": record['altitude'], "size": record['size'], "precision_horz": record['h_precision'], "precision_vert": record['v_precision']})
328
- return {"ttl": record["ttl"], "type": _type, "values": values}
329
-
330
- def _data_for_SSHFP(self, _type, records):
331
- values = []
332
- for record in records:
333
- values.append({"algorithm": record['algorithm'], "fingerprint_type": record['fp_type'] ,"fingerprint": record['record']})
334
- return {"ttl": records[0]["ttl"], "type": _type, "values": values}
335
-
336
- def _data_for_NAPTR(self, _type, records):
337
- values = []
338
- for record in records:
339
- values.append({"order": record['order'], "preference": record['pref'], "flags": record['flag'], "service": record['params'],
340
- "regexp": record['regexp'], "replacement": record['replace']})
341
- return {"ttl": records[0]["ttl"], "type": _type, "values": values}
342
-
343
- def _data_for_TLSA(self, _type, records):
344
- values = []
345
- for record in records:
346
- values.append({"certificate_association_data": record['record'], "certificate_usage": record['tlsa_usage'], "selector": record['tlsa_selector'],
347
- "matching_type": record['tlsa_matching_type']})
348
- return {"ttl": records[0]["ttl"], "type": _type, "values": values}
349
-
350
- def zone_records(self, zone):
351
- if zone.name not in self._zone_records:
352
- try:
353
- self._zone_records[zone.name] = self._client.zone_records(zone.name[:-1])
354
- except ClouDNSClientNotFound:
355
- return []
356
- return self._zone_records[zone.name]
357
-
358
- def isGeoDNS(self, statusDescription):
359
- if statusDescription == 'Your plan supports only GeoDNS zones.':
360
- return True
361
- else:
362
- return False
363
-
364
- def populate(self, zone, target=False, lenient=False):
365
- self.log.debug(
366
- "populate: name=%s, target=%s, lenient=%s",
367
- zone.name,
368
- target,
369
- lenient,
370
- )
371
-
372
- values = defaultdict(lambda: defaultdict(list))
373
- records_data = self.zone_records(zone)
374
-
375
- if 'status' in records_data and records_data['status'] == 'Failed':
376
- self.log.info("populate: no existing zone, trying to create it")
377
- response = self._client.zone_create(zone.name[:-1], 'master')
378
- if 'id' in response and response['id'] == 'not_found':
379
- e = ClouDNSClientUnknownDomainName(
380
- 'Missing domain name'
381
- )
382
- e.__cause__ = None
383
- raise e
384
-
385
- if (self.isGeoDNS(response['statusDescription'])):
386
- response = self._client.zone_create(zone.name[:-1], 'geodns')
387
-
388
- if(response['status'] == 'Failed'):
389
- e = ClouDNSClientUnknownDomainName(f"{response['status']} : {response['statusDescription']}")
390
- e.__cause__ = None
391
- raise e
392
- self.log.info("populate: zone has been successfully created")
393
- records_data = self._client.zone_records(zone.name[:-1])
394
-
395
- for record_id, record in records_data.items():
396
- _type = record["type"]
397
-
398
- if _type not in self.SUPPORTS:
399
- continue
400
-
401
- values[record["host"]][_type].append(record)
402
- before = len(records_data.items())
403
- for name, types in values.items():
404
- for _type, records in types.items():
405
- data_for = getattr(self, f"_data_for_{_type}")
406
- record = Record.new(
407
- zone,
408
- name,
409
- data_for(_type, records),
410
- source=self,
411
- lenient=lenient,
412
- )
413
- zone.add_record(record, lenient=lenient)
414
- exists = zone.name in self._zone_records
415
- print(
416
- "populate: found %s records, exists=%s",
417
- len(zone.records) - before,
418
- exists,
419
- )
420
- return exists
421
-
422
-
423
- def _record_name(self, name):
424
- return name if name else ""
425
-
426
- def _params_for_multiple(self, record):
427
- return {
428
- "rrset_name": self._record_name(record.name),
429
- "rrset_ttl": record.ttl,
430
- "rrset_type": record._type,
431
- "rrset_values": [str(v) for v in record.values]
432
- }
433
-
434
- def _params_for_geo(self, record):
435
- geo_location = record.geo
436
- locations = []
437
- for code, geo_value in geo_location.items():
438
- continent_code = geo_value.continent_code
439
- country_code = geo_value.country_code
440
- subdivision_code = geo_value.subdivision_code
441
-
442
- if subdivision_code is not None:
443
- locations.append(subdivision_code)
444
- elif country_code is not None:
445
- locations.append(country_code)
446
- elif continent_code is not None:
447
- locations.append(continent_code)
448
- else:
449
- locations = 0
450
-
451
- return{
452
- "geodns": True,
453
- "rrset_name": self._record_name(record.name),
454
- "rrset_ttl": record.ttl,
455
- "rrset_type": record._type,
456
- "rrset_values": [str(v) for v in record.values],
457
- "rrset_locations": [str(v) for v in locations]
458
- }
459
-
460
-
461
- def _params_for_A_AAAA(self, record):
462
- if getattr(record, 'geo', False):
463
- return self._params_for_geo(record)
464
- return {
465
- "rrset_name": self._record_name(record.name),
466
- "rrset_ttl": record.ttl,
467
- "rrset_type": record._type,
468
- "rrset_values": [str(v) for v in record.values]
469
- }
470
-
471
- _params_for_A = _params_for_A_AAAA
472
- _params_for_AAAA = _params_for_A_AAAA
473
- _params_for_NS = _params_for_multiple
474
- _params_for_TXT = _params_for_multiple
475
- _params_for_SPF = _params_for_multiple
476
-
477
- def _params_for_CAA(self, record):
478
- return {
479
- "rrset_name": self._record_name(record.name),
480
- "rrset_ttl": record.ttl,
481
- "rrset_type": record._type,
482
- "rrset_values": [f'{v.flags} {v.tag} "{v.value}"' for v in record.values],
483
- }
484
-
485
- def _params_for_single(self, record):
486
- return {
487
- "rrset_name": self._record_name(record.name),
488
- "rrset_ttl": record.ttl,
489
- "rrset_type": record._type,
490
- "rrset_values": [record.value],
491
- }
492
-
493
- _params_for_ALIAS = _params_for_single
494
- _params_for_CNAME = _params_for_single
495
- _params_for_DNAME = _params_for_single
496
- _params_for_PTR = _params_for_single
497
-
498
- def _params_for_MX(self, record):
499
- return {
500
- "rrset_name": self._record_name(record.name),
501
- "rrset_ttl": record.ttl,
502
- "rrset_type": record._type,
503
- "rrset_values": [f"{v.preference} {v.exchange}" for v in record.values],
504
- }
505
-
506
- def _params_for_SRV(self, record):
507
- return {
508
- "rrset_name": self._record_name(record.name),
509
- "rrset_ttl": record.ttl,
510
- "rrset_type": record._type,
511
- "rrset_values": [
512
- f"{v.priority} {v.weight} {v.port} {v.target}" for v in record.values
513
- ],
514
- }
515
-
516
- def _params_for_SSHFP(self, record):
517
- return {
518
- "rrset_name": self._record_name(record.name),
519
- "rrset_ttl": record.ttl,
520
- "rrset_type": record._type,
521
- "rrset_values": [
522
- f"{v.algorithm} {v.fingerprint_type} " f"{v.fingerprint}"
523
- for v in record.values
524
- ],
525
- }
526
-
527
- def _params_for_LOC(self, record):
528
- return {
529
- "rrset_name": self._record_name(record.name),
530
- "rrset_ttl": record.ttl,
531
- "rrset_type": record._type,
532
- "rrset_values": [
533
- f"{v.lat_degrees} {v.lat_minutes} {v.lat_seconds} {v.lat_direction} "
534
- f"{v.long_degrees} {v.long_minutes} {v.long_seconds} {v.long_direction} {v.altitude} {v.size} {v.precision_horz} {v.precision_vert} "
535
- for v in record.values
536
- ],
537
- }
538
-
539
- def _params_for_NAPTR(self, record):
540
- return {
541
- "rrset_name": self._record_name(record.name),
542
- "rrset_ttl": record.ttl,
543
- "rrset_type": record._type,
544
- "rrset_values": [
545
- f"{v.order} {v.preference} {v.flags} {v.service} {v.regexp} {v.replacement}"
546
- for v in record.values
547
- ],
548
- }
549
-
550
- def _params_for_TLSA(self, record):
551
- return {
552
- "rrset_name": self._record_name(record.name),
553
- "rrset_ttl": record.ttl,
554
- "rrset_type": record._type,
555
- "rrset_values": [
556
- f"{v.certificate_association_data} {v.certificate_usage} {v.selector} {v.matching_type}"
557
- for v in record.values
558
- ],
559
- }
560
-
561
- def _apply_create(self, change):
562
- new = change.new
563
- if hasattr(new, 'values'):
564
- for value in new.values:
565
- data = getattr(self, f"_params_for_{new._type}")(new)
566
- if ('rrset_values' in data):
567
- data['rrset_values'] = [value]
568
- self._client.record_create(new.zone.name[:-1], **data)
569
- else:
570
- data = getattr(self, f"_params_for_{new._type}")(new)
571
- else:
572
- data = getattr(self, f"_params_for_{new._type}")(new)
573
- self._client.record_create(new.zone.name[:-1], **data)
574
-
575
- def _apply_update(self, change):
576
- self._apply_delete(change)
577
- self._apply_create(change)
578
-
579
- def records_are_same(self, existing):
580
- zone = existing.zone
581
- record_ids = []
582
- for record_id, record in self.zone_records(zone).items():
583
- if existing._type == 'NAPTR' and record['type'] == 'NAPTR':
584
- for value in existing.values:
585
- if (
586
- existing.name == record['host']
587
- and value.order == int(record['order'])
588
- and value.preference == int(record['pref'])
589
- and value.flags == record['flag']
590
- ):
591
- record_ids.append(record_id)
592
- elif existing._type == 'SSHFP' and record['type'] == 'SSHFP':
593
- for value in existing.values:
594
- if (
595
- existing.name == record['host']
596
- and value.fingerprint_type == int(record['fp_type'])
597
- and value.algorithm == int(record['algorithm'])
598
- and value.fingerprint == record['record']
599
- ):
600
- record_ids.append(record_id)
601
- elif existing._type == 'SRV' and record['type'] == 'SRV':
602
- for value in existing.values:
603
- if (
604
- existing.name == record['host']
605
- and value.priority == int(record['priority'])
606
- and value.weight == int(record['weight'])
607
- and value.port == record['port']
608
- and value.target == record['record']
609
- ):
610
- record_ids.append(record_id)
611
- elif existing._type == 'CAA' and record['type'] == 'CAA':
612
- for value in existing.values:
613
- if (
614
- existing.name == record['host']
615
- and value.flags == record['caa_flag']
616
- and value.tag == record['caa_type']
617
- and value.value == record['caa_value']
618
- ):
619
- record_ids.append(record_id)
620
- elif existing._type == 'MX' and record['type'] == 'MX':
621
- for value in existing.values:
622
- if (
623
- existing.name == record['host']
624
- and value.preference == int(record['priority'])
625
- and value.exchange == record['record']
626
- ):
627
- record_ids.append(record_id)
628
-
629
- elif existing._type == 'LOC' and record['type'] == 'LOC':
630
- for value in existing.values:
631
- if (
632
- existing.name == record['host']
633
- and value.lat_degrees == record['lat_deg']
634
- and value.lat_minutes == record['lat_min']
635
- and value.lat_seconds == record['lat_sec']
636
- and value.lat_direction == record['lat_dir']
637
- and value.long_degrees == record['long_deg']
638
- and value.long_minutes == record['long_min']
639
- and value.long_seconds == record['long_sec']
640
- and value.long_direction == record['long_dir']
641
- and value.altitude == record['altitude']
642
- and value.size == record['size']
643
- and value.precision_horz == record['h_precision']
644
- and value.precision_vert == record['v_precision']
645
- ):
646
- record_ids.append(record_id)
647
- else:
648
- if (record == 'Failed' or record == 'Missing domain-name'):
649
- continue
650
-
651
- if hasattr(existing, 'value'):
652
- if (
653
- existing.name == record['host']
654
- and existing._type == record['type']
655
- and existing.value == record['record']
656
- ):
657
- record_ids.append(record_id)
658
- elif hasattr(existing, 'values'):
659
- for value in existing.values:
660
- if (
661
- existing.name == record['host']
662
- and existing._type == record['type']
663
- and value == record['record']
664
- ):
665
- record_ids.append(record_id)
666
- return record_ids
667
-
668
-
669
- def _apply_delete(self, change):
670
- existing = change.existing
671
- zone = existing.zone
672
- record_ids = self.records_are_same(existing)
673
-
674
- for record_id in record_ids:
675
- self._client.record_delete(zone.name[:-1], record_id)
676
-
677
-
678
- def _apply(self, plan):
679
- desired = plan.desired
680
-
681
- changes = plan.changes
682
- zone = desired.name[:-1]
683
- self.log.debug("_apply: zone=%s, len(changes)=%d", desired.name, len(changes))
684
-
685
- try:
686
- self._client.zone(zone)
687
- except ClouDNSClientNotFound:
688
- self.log.info("_apply: no existing zone, trying to create it")
689
- try:
690
- self._client.zone_create(zone, 'master')
691
- self.log.info("_apply: zone has been successfully created")
692
- except ClouDNSClientNotFound:
693
- e = ClouDNSClientUnknownDomainName(
694
- "Domain " + zone + " is not "
695
- "registered at ClouDNS. "
696
- "Please register or "
697
- "transfer it here "
698
- "to be able to manage its "
699
- "DNS zone."
700
- )
701
- e.__cause__ = None
702
- raise e
703
-
704
- for change in changes:
705
- class_name = change.__class__.__name__
706
- getattr(self, f"_apply_{class_name.lower()}")(change)
707
-
708
- # Clear out the cache if any
709
- self._zone_records.pop(desired.name, None)
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.7'
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('\;', ';').replace('+', '%2B'))
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={}&params={}'.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 += '&regexp={}'.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
+ "values": [
264
+ (record["record"].replace(';', '\\;').rstrip('.'))
265
+ for record in records
266
+ ]
267
+ }
268
+
269
+
270
+
271
+ _data_for_A = _data_for_multiple
272
+ _data_for_AAAA = _data_for_multiple
273
+ _data_for_SPF = _data_for_multiple
274
+ _data_for_NS = _data_for_multiple
275
+
276
+ def zone(self, zone_name):
277
+ return self._client.zone(zone_name)
278
+
279
+ def zone_create(self, zone_name, zone_type, master_ip=None):
280
+ return self._client.zone_create(zone_name, zone_type, master_ip=master_ip)
281
+
282
+ def _data_for_CAA(self, _type, records):
283
+ values = []
284
+ for record in records:
285
+ values.append(
286
+ {
287
+ "flags": record['caa_flag'],
288
+ "tag": record['caa_type'],
289
+ "value": record['caa_value']
290
+ }
291
+ )
292
+
293
+ return {"ttl": records[0]["ttl"], "type": _type, "values": values}
294
+
295
+ def _data_for_single(self, _type, records):
296
+ return {
297
+ "ttl": records[0]["ttl"],
298
+ "type": _type,
299
+ "value": records[0]["record"] + ".",
300
+ }
301
+
302
+ _data_for_ALIAS = _data_for_single
303
+ _data_for_CNAME = _data_for_single
304
+ _data_for_DNAME = _data_for_single
305
+ _data_for_PTR = _data_for_single
306
+
307
+ def _data_for_MX(self, _type, records):
308
+ values = []
309
+ for record in records:
310
+ if 'priority' in record and 'record' in record:
311
+ values.append({"preference": record['priority'], "exchange": record['record'] + '.'})
312
+ return {"ttl": records[0]["ttl"], "type": _type, "values": values}
313
+
314
+
315
+
316
+ def _data_for_SRV(self, _type, records):
317
+ values = []
318
+ for record in records:
319
+ values.append({"priority": record['priority'], "weight": record['weight'] ,"port": record['port'], "target": record['record'] + '.'})
320
+ return {"ttl": record["ttl"], "type": _type, "values": values}
321
+
322
+ def _data_for_LOC(self, _type, records):
323
+ values = []
324
+ for record in records:
325
+ values.append({"lat_degrees": record['lat_deg'], "lat_minutes": record['lat_min'] ,"lat_seconds": record['lat_sec'], "lat_direction": record['lat_dir'],
326
+ "long_degrees": record['long_deg'], "long_minutes": record['long_min'], "long_seconds": record['long_sec'], "long_direction": record['long_dir'],
327
+ "altitude": record['altitude'], "size": record['size'], "precision_horz": record['h_precision'], "precision_vert": record['v_precision']})
328
+ return {"ttl": record["ttl"], "type": _type, "values": values}
329
+
330
+ def _data_for_SSHFP(self, _type, records):
331
+ values = []
332
+ for record in records:
333
+ values.append({"algorithm": record['algorithm'], "fingerprint_type": record['fp_type'] ,"fingerprint": record['record']})
334
+ return {"ttl": records[0]["ttl"], "type": _type, "values": values}
335
+
336
+ def _data_for_NAPTR(self, _type, records):
337
+ values = []
338
+ for record in records:
339
+ values.append({"order": record['order'], "preference": record['pref'], "flags": record['flag'], "service": record['params'],
340
+ "regexp": record['regexp'], "replacement": record['replace']})
341
+ return {"ttl": records[0]["ttl"], "type": _type, "values": values}
342
+
343
+ def _data_for_TLSA(self, _type, records):
344
+ values = []
345
+ for record in records:
346
+ values.append({"certificate_association_data": record['record'], "certificate_usage": record['tlsa_usage'], "selector": record['tlsa_selector'],
347
+ "matching_type": record['tlsa_matching_type']})
348
+ return {"ttl": records[0]["ttl"], "type": _type, "values": values}
349
+
350
+ def zone_records(self, zone):
351
+ if zone.name not in self._zone_records:
352
+ try:
353
+ self._zone_records[zone.name] = self._client.zone_records(zone.name[:-1])
354
+ except ClouDNSClientNotFound:
355
+ return []
356
+ return self._zone_records[zone.name]
357
+
358
+ def isGeoDNS(self, statusDescription):
359
+ if statusDescription == 'Your plan supports only GeoDNS zones.':
360
+ return True
361
+ else:
362
+ return False
363
+
364
+ def populate(self, zone, target=False, lenient=False):
365
+ self.log.debug(
366
+ "populate: name=%s, target=%s, lenient=%s",
367
+ zone.name,
368
+ target,
369
+ lenient,
370
+ )
371
+
372
+ values = defaultdict(lambda: defaultdict(list))
373
+ records_data = self.zone_records(zone)
374
+
375
+ if 'status' in records_data and records_data['status'] == 'Failed':
376
+ self.log.info("populate: no existing zone, trying to create it")
377
+ response = self._client.zone_create(zone.name[:-1], 'master')
378
+ if 'id' in response and response['id'] == 'not_found':
379
+ e = ClouDNSClientUnknownDomainName(
380
+ 'Missing domain name'
381
+ )
382
+ e.__cause__ = None
383
+ raise e
384
+
385
+ if (self.isGeoDNS(response['statusDescription'])):
386
+ response = self._client.zone_create(zone.name[:-1], 'geodns')
387
+
388
+ if(response['status'] == 'Failed'):
389
+ e = ClouDNSClientUnknownDomainName(f"{response['status']} : {response['statusDescription']}")
390
+ e.__cause__ = None
391
+ raise e
392
+ self.log.info("populate: zone has been successfully created")
393
+ records_data = self._client.zone_records(zone.name[:-1])
394
+
395
+ for record_id, record in records_data.items():
396
+ _type = record["type"]
397
+
398
+ if _type not in self.SUPPORTS:
399
+ continue
400
+
401
+ values[record["host"]][_type].append(record)
402
+ before = len(records_data.items())
403
+ for name, types in values.items():
404
+ for _type, records in types.items():
405
+ data_for = getattr(self, f"_data_for_{_type}")
406
+ record = Record.new(
407
+ zone,
408
+ name,
409
+ data_for(_type, records),
410
+ source=self,
411
+ lenient=lenient,
412
+ )
413
+ zone.add_record(record, lenient=lenient)
414
+ exists = zone.name in self._zone_records
415
+ print(
416
+ "populate: found %s records, exists=%s",
417
+ len(zone.records) - before,
418
+ exists,
419
+ )
420
+ return exists
421
+
422
+
423
+ def _record_name(self, name):
424
+ return name if name else ""
425
+
426
+ def _params_for_multiple(self, record):
427
+ return {
428
+ "rrset_name": self._record_name(record.name),
429
+ "rrset_ttl": record.ttl,
430
+ "rrset_type": record._type,
431
+ "rrset_values": [str(v) for v in record.values]
432
+ }
433
+
434
+ def _params_for_geo(self, record):
435
+ geo_location = record.geo
436
+ locations = []
437
+ for code, geo_value in geo_location.items():
438
+ continent_code = geo_value.continent_code
439
+ country_code = geo_value.country_code
440
+ subdivision_code = geo_value.subdivision_code
441
+
442
+ if subdivision_code is not None:
443
+ locations.append(subdivision_code)
444
+ elif country_code is not None:
445
+ locations.append(country_code)
446
+ elif continent_code is not None:
447
+ locations.append(continent_code)
448
+ else:
449
+ locations = 0
450
+
451
+ return{
452
+ "geodns": True,
453
+ "rrset_name": self._record_name(record.name),
454
+ "rrset_ttl": record.ttl,
455
+ "rrset_type": record._type,
456
+ "rrset_values": [str(v) for v in record.values],
457
+ "rrset_locations": [str(v) for v in locations]
458
+ }
459
+
460
+
461
+ def _params_for_A_AAAA(self, record):
462
+ if getattr(record, 'geo', False):
463
+ return self._params_for_geo(record)
464
+ return {
465
+ "rrset_name": self._record_name(record.name),
466
+ "rrset_ttl": record.ttl,
467
+ "rrset_type": record._type,
468
+ "rrset_values": [str(v) for v in record.values]
469
+ }
470
+
471
+ _params_for_A = _params_for_A_AAAA
472
+ _params_for_AAAA = _params_for_A_AAAA
473
+ _params_for_NS = _params_for_multiple
474
+ _params_for_TXT = _params_for_multiple
475
+ _params_for_SPF = _params_for_multiple
476
+
477
+ def _params_for_CAA(self, record):
478
+ return {
479
+ "rrset_name": self._record_name(record.name),
480
+ "rrset_ttl": record.ttl,
481
+ "rrset_type": record._type,
482
+ "rrset_values": [f'{v.flags} {v.tag} "{v.value}"' for v in record.values],
483
+ }
484
+
485
+ def _params_for_single(self, record):
486
+ return {
487
+ "rrset_name": self._record_name(record.name),
488
+ "rrset_ttl": record.ttl,
489
+ "rrset_type": record._type,
490
+ "rrset_values": [record.value],
491
+ }
492
+
493
+ _params_for_ALIAS = _params_for_single
494
+ _params_for_CNAME = _params_for_single
495
+ _params_for_DNAME = _params_for_single
496
+ _params_for_PTR = _params_for_single
497
+
498
+ def _params_for_MX(self, record):
499
+ return {
500
+ "rrset_name": self._record_name(record.name),
501
+ "rrset_ttl": record.ttl,
502
+ "rrset_type": record._type,
503
+ "rrset_values": [f"{v.preference} {v.exchange}" for v in record.values],
504
+ }
505
+
506
+ def _params_for_SRV(self, record):
507
+ return {
508
+ "rrset_name": self._record_name(record.name),
509
+ "rrset_ttl": record.ttl,
510
+ "rrset_type": record._type,
511
+ "rrset_values": [
512
+ f"{v.priority} {v.weight} {v.port} {v.target}" for v in record.values
513
+ ],
514
+ }
515
+
516
+ def _params_for_SSHFP(self, record):
517
+ return {
518
+ "rrset_name": self._record_name(record.name),
519
+ "rrset_ttl": record.ttl,
520
+ "rrset_type": record._type,
521
+ "rrset_values": [
522
+ f"{v.algorithm} {v.fingerprint_type} " f"{v.fingerprint}"
523
+ for v in record.values
524
+ ],
525
+ }
526
+
527
+ def _params_for_LOC(self, record):
528
+ return {
529
+ "rrset_name": self._record_name(record.name),
530
+ "rrset_ttl": record.ttl,
531
+ "rrset_type": record._type,
532
+ "rrset_values": [
533
+ f"{v.lat_degrees} {v.lat_minutes} {v.lat_seconds} {v.lat_direction} "
534
+ f"{v.long_degrees} {v.long_minutes} {v.long_seconds} {v.long_direction} {v.altitude} {v.size} {v.precision_horz} {v.precision_vert} "
535
+ for v in record.values
536
+ ],
537
+ }
538
+
539
+ def _params_for_NAPTR(self, record):
540
+ return {
541
+ "rrset_name": self._record_name(record.name),
542
+ "rrset_ttl": record.ttl,
543
+ "rrset_type": record._type,
544
+ "rrset_values": [
545
+ f"{v.order} {v.preference} {v.flags} {v.service} {v.regexp} {v.replacement}"
546
+ for v in record.values
547
+ ],
548
+ }
549
+
550
+ def _params_for_TLSA(self, record):
551
+ return {
552
+ "rrset_name": self._record_name(record.name),
553
+ "rrset_ttl": record.ttl,
554
+ "rrset_type": record._type,
555
+ "rrset_values": [
556
+ f"{v.certificate_association_data} {v.certificate_usage} {v.selector} {v.matching_type}"
557
+ for v in record.values
558
+ ],
559
+ }
560
+
561
+ def _apply_create(self, change):
562
+ new = change.new
563
+ if hasattr(new, 'values'):
564
+ for value in new.values:
565
+ data = getattr(self, f"_params_for_{new._type}")(new)
566
+ if ('rrset_values' in data):
567
+ data['rrset_values'] = [value]
568
+ self._client.record_create(new.zone.name[:-1], **data)
569
+ else:
570
+ data = getattr(self, f"_params_for_{new._type}")(new)
571
+ else:
572
+ data = getattr(self, f"_params_for_{new._type}")(new)
573
+ self._client.record_create(new.zone.name[:-1], **data)
574
+
575
+ def _apply_update(self, change):
576
+ self._apply_delete(change)
577
+ self._apply_create(change)
578
+
579
+ def records_are_same(self, existing):
580
+ zone = existing.zone
581
+ record_ids = []
582
+ for record_id, record in self.zone_records(zone).items():
583
+ if existing._type == 'NAPTR' and record['type'] == 'NAPTR':
584
+ for value in existing.values:
585
+ if (
586
+ existing.name == record['host']
587
+ and value.order == int(record['order'])
588
+ and value.preference == int(record['pref'])
589
+ and value.flags == record['flag']
590
+ ):
591
+ record_ids.append(record_id)
592
+ elif existing._type == 'SSHFP' and record['type'] == 'SSHFP':
593
+ for value in existing.values:
594
+ if (
595
+ existing.name == record['host']
596
+ and value.fingerprint_type == int(record['fp_type'])
597
+ and value.algorithm == int(record['algorithm'])
598
+ and value.fingerprint == record['record']
599
+ ):
600
+ record_ids.append(record_id)
601
+ elif existing._type == 'SRV' and record['type'] == 'SRV':
602
+ for value in existing.values:
603
+ if (
604
+ existing.name == record['host']
605
+ and value.priority == int(record['priority'])
606
+ and value.weight == int(record['weight'])
607
+ and value.port == record['port']
608
+ and value.target == record['record']
609
+ ):
610
+ record_ids.append(record_id)
611
+ elif existing._type == 'CAA' and record['type'] == 'CAA':
612
+ for value in existing.values:
613
+ if (
614
+ existing.name == record['host']
615
+ and value.flags == record['caa_flag']
616
+ and value.tag == record['caa_type']
617
+ and value.value == record['caa_value']
618
+ ):
619
+ record_ids.append(record_id)
620
+ elif existing._type == 'MX' and record['type'] == 'MX':
621
+ for value in existing.values:
622
+ if (
623
+ existing.name == record['host']
624
+ and value.preference == int(record['priority'])
625
+ and value.exchange == record['record']
626
+ ):
627
+ record_ids.append(record_id)
628
+
629
+ elif existing._type == 'LOC' and record['type'] == 'LOC':
630
+ for value in existing.values:
631
+ if (
632
+ existing.name == record['host']
633
+ and value.lat_degrees == record['lat_deg']
634
+ and value.lat_minutes == record['lat_min']
635
+ and value.lat_seconds == record['lat_sec']
636
+ and value.lat_direction == record['lat_dir']
637
+ and value.long_degrees == record['long_deg']
638
+ and value.long_minutes == record['long_min']
639
+ and value.long_seconds == record['long_sec']
640
+ and value.long_direction == record['long_dir']
641
+ and value.altitude == record['altitude']
642
+ and value.size == record['size']
643
+ and value.precision_horz == record['h_precision']
644
+ and value.precision_vert == record['v_precision']
645
+ ):
646
+ record_ids.append(record_id)
647
+ else:
648
+ if (record == 'Failed' or record == 'Missing domain-name'):
649
+ continue
650
+
651
+ if hasattr(existing, 'value'):
652
+ if (
653
+ existing.name == record['host']
654
+ and existing._type == record['type']
655
+ and existing.value == record['record']
656
+ ):
657
+ record_ids.append(record_id)
658
+ elif hasattr(existing, 'values'):
659
+ for value in existing.values:
660
+ if (
661
+ existing.name == record['host']
662
+ and existing._type == record['type']
663
+ and value == record['record']
664
+ ):
665
+ record_ids.append(record_id)
666
+ return record_ids
667
+
668
+
669
+ def _apply_delete(self, change):
670
+ existing = change.existing
671
+ zone = existing.zone
672
+ record_ids = self.records_are_same(existing)
673
+
674
+ for record_id in record_ids:
675
+ self._client.record_delete(zone.name[:-1], record_id)
676
+
677
+
678
+ def _apply(self, plan):
679
+ desired = plan.desired
680
+
681
+ changes = plan.changes
682
+ zone = desired.name[:-1]
683
+ self.log.debug("_apply: zone=%s, len(changes)=%d", desired.name, len(changes))
684
+
685
+ try:
686
+ self._client.zone(zone)
687
+ except ClouDNSClientNotFound:
688
+ self.log.info("_apply: no existing zone, trying to create it")
689
+ try:
690
+ self._client.zone_create(zone, 'master')
691
+ self.log.info("_apply: zone has been successfully created")
692
+ except ClouDNSClientNotFound:
693
+ e = ClouDNSClientUnknownDomainName(
694
+ "Domain " + zone + " is not "
695
+ "registered at ClouDNS. "
696
+ "Please register or "
697
+ "transfer it here "
698
+ "to be able to manage its "
699
+ "DNS zone."
700
+ )
701
+ e.__cause__ = None
702
+ raise e
703
+
704
+ for change in changes:
705
+ class_name = change.__class__.__name__
706
+ getattr(self, f"_apply_{class_name.lower()}")(change)
707
+
708
+ # Clear out the cache if any
709
+ self._zone_records.pop(desired.name, None)