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={}&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
+ "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)