-
-
Notifications
You must be signed in to change notification settings - Fork 12
/
__init__.py
2091 lines (1767 loc) · 73.6 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#
#
#
from collections import defaultdict
from copy import deepcopy
from functools import reduce
from ipaddress import ip_address, ip_network
from logging import getLogger
from azure.core.pipeline.policies import RetryPolicy
from azure.identity import AzureCliCredential, ClientSecretCredential
from azure.mgmt.dns import DnsManagementClient
from azure.mgmt.dns.models import (
AaaaRecord,
ARecord,
CaaRecord,
CnameRecord,
MxRecord,
NsRecord,
PtrRecord,
SrvRecord,
TxtRecord,
Zone,
)
from azure.mgmt.privatedns import PrivateDnsManagementClient
from azure.mgmt.privatedns.models import PrivateZone
from azure.mgmt.trafficmanager import TrafficManagerManagementClient
from azure.mgmt.trafficmanager.models import (
AlwaysServe,
DnsConfig,
Endpoint,
EndpointPropertiesSubnetsItem,
EndpointStatus,
MonitorConfig,
MonitorConfigCustomHeadersItem,
Profile,
)
from octodns.provider import ProviderException, SupportsException
from octodns.provider.base import BaseProvider
from octodns.record import GeoCodes, Record, Update
# TODO: remove __VERSION__ with the next major version release
__version__ = __VERSION__ = '0.0.9'
class AzureException(ProviderException):
pass
def escape_semicolon(s):
assert s
return s.replace(';', '\\;')
def unescape_semicolon(s):
assert s
return s.replace('\\;', ';')
def azure_chunked_value(val):
CHUNK_SIZE = 255
val_replace = val.replace('"', '\\"')
value = unescape_semicolon(val_replace)
if len(val) > CHUNK_SIZE:
vs = [
value[i : i + CHUNK_SIZE] for i in range(0, len(value), CHUNK_SIZE)
]
else:
vs = value
return vs
def azure_chunked_values(s):
values = []
for v in s:
values.append(azure_chunked_value(v))
return values
class _AzureRecord(object):
'''Wrapper for OctoDNS record for AzureProvider to make dns_client calls.
azuredns.py:
class: octodns.provider.azuredns._AzureRecord
An _AzureRecord is easily accessible to Azure DNS Management library
functions and is used to wrap all relevant data to create a record in
Azure.
'''
TYPE_MAP = {
'A': ARecord,
'AAAA': AaaaRecord,
'CAA': CaaRecord,
'CNAME': CnameRecord,
'MX': MxRecord,
'SRV': SrvRecord,
'NS': NsRecord,
'PTR': PtrRecord,
'TXT': TxtRecord,
}
def __init__(
self, resource_group, record, delete=False, traffic_manager=None
):
'''Constructor for _AzureRecord.
Notes on Azure records: An Azure record set has the form
RecordSet(name=<...>, type=<...>, a_records=[...],
aaaa_records=[...], ...)
When constructing an azure record as done in self._apply_Create,
the argument parameters for an A record would be
parameters={'ttl': <int>, 'a_records': [ARecord(<str ip>),]}.
As another example for CNAME record:
parameters={'ttl': <int>, 'cname_record': CnameRecord(<str>)}.
Below, key_name and class_name are the dictionary key and Azure
Record class respectively.
:param resource_group: The name of resource group in Azure
:type resource_group: str
:param record: An OctoDNS record
:type record: ..record.Record
:param delete: If true, omit data parsing; not needed to delete
:type delete: bool
:type return: _AzureRecord
'''
self.log = getLogger('AzureRecord')
self.resource_group = resource_group
self.zone_name = record.zone.name[:-1]
self.relative_record_set_name = record.name or '@'
self.record_type = record._type
self._record = record
self.traffic_manager = traffic_manager
if delete:
return
# Refer to function docstring for key_name and class_name.
key_name = f'{self.record_type}_records'.lower()
if record._type == 'CNAME':
key_name = key_name[:-1]
azure_class = self.TYPE_MAP[self.record_type]
params_for = getattr(self, f'_params_for_{record._type}')
self.params = params_for(record.data, key_name, azure_class)
self.params['ttl'] = record.ttl
def _params_for_A(self, data, key_name, azure_class):
if self._record.dynamic and self.traffic_manager:
return {'target_resource': self.traffic_manager}
try:
values = data['values']
except KeyError:
values = [data['value']]
return {key_name: [azure_class(ipv4_address=v) for v in values]}
def _params_for_AAAA(self, data, key_name, azure_class):
if self._record.dynamic and self.traffic_manager:
return {'target_resource': self.traffic_manager}
try:
values = data['values']
except KeyError:
values = [data['value']]
return {key_name: [azure_class(ipv6_address=v) for v in values]}
def _params_for_CAA(self, data, key_name, azure_class):
params = []
if 'values' in data:
for vals in data['values']:
params.append(
azure_class(
flags=vals['flags'],
tag=vals['tag'],
value=vals['value'],
)
)
else: # Else there is a singular data point keyed by 'value'.
params.append(
azure_class(
flags=data['value']['flags'],
tag=data['value']['tag'],
value=data['value']['value'],
)
)
return {key_name: params}
def _params_for_CNAME(self, data, key_name, azure_class):
if self._record.dynamic and self.traffic_manager:
return {'target_resource': self.traffic_manager}
return {key_name: azure_class(cname=data['value'])}
def _params_for_MX(self, data, key_name, azure_class):
params = []
if 'values' in data:
for vals in data['values']:
params.append(
azure_class(
preference=vals['preference'], exchange=vals['exchange']
)
)
else: # Else there is a singular data point keyed by 'value'.
params.append(
azure_class(
preference=data['value']['preference'],
exchange=data['value']['exchange'],
)
)
return {key_name: params}
def _params_for_SRV(self, data, key_name, azure_class):
params = []
if 'values' in data:
for vals in data['values']:
params.append(
azure_class(
priority=vals['priority'],
weight=vals['weight'],
port=vals['port'],
target=vals['target'],
)
)
else: # Else there is a singular data point keyed by 'value'.
params.append(
azure_class(
priority=data['value']['priority'],
weight=data['value']['weight'],
port=data['value']['port'],
target=data['value']['target'],
)
)
return {key_name: params}
def _params_for_NS(self, data, key_name, azure_class):
try:
values = data['values']
except KeyError:
values = [data['value']]
return {key_name: [azure_class(nsdname=v) for v in values]}
def _params_for_PTR(self, data, key_name, azure_class):
try:
values = data['values']
except KeyError:
values = [data['value']]
return {key_name: [azure_class(ptrdname=v) for v in values]}
def _params_for_TXT(self, data, key_name, azure_class):
params = []
try: # API for TxtRecord has list of str, even for singleton
values = [v for v in azure_chunked_values(data['values'])]
except KeyError:
values = [azure_chunked_value(data['value'])]
for v in values:
if isinstance(v, list):
params.append(azure_class(value=v))
else:
params.append(azure_class(value=[v]))
return {key_name: params}
def _equals(self, b):
'''Checks whether two records are equal by comparing all fields.
:param b: Another _AzureRecord object
:type b: _AzureRecord
:type return: bool
'''
def key_dict(d):
return sum([hash(f'{k}:{v}') for k, v in d.items()])
def parse_dict(params):
vals = []
for char in params:
if char != 'ttl':
list_records = params[char]
try:
for record in list_records:
vals.append(record.__dict__)
except:
vals.append(list_records.__dict__)
vals.sort(key=key_dict)
return vals
return (
(self.resource_group == b.resource_group)
& (self.zone_name == b.zone_name)
& (self.record_type == b.record_type)
& (self.params['ttl'] == b.params['ttl'])
& (parse_dict(self.params) == parse_dict(b.params))
& (self.relative_record_set_name == b.relative_record_set_name)
)
def _check_endswith_dot(string):
return string if string.endswith('.') else string + '.'
def _parse_azure_type(string):
'''Converts string representing an Azure RecordSet type to usual type.
:param string: the Azure type. eg: <Microsoft.Network/dnszones/A>
:type string: str
:type return: str
'''
return string.split('/')[-1]
def _root_traffic_manager_name(record):
# ATM names can only have letters, numbers and hyphens
# replace dots with double hyphens to ensure unique mapping,
# hoping that real life FQDNs won't have double hyphens
name = record.fqdn[:-1].replace('.', '--')
if record._type != 'CNAME':
name += f'-{record._type}'
return name
def _geo_traffic_manager_name(record):
prefix = _root_traffic_manager_name(record)
return f'{prefix}-geo'
def _rule_traffic_manager_name(pool, record):
prefix = _root_traffic_manager_name(record)
return f'{prefix}-rule-{pool}'
def _pool_traffic_manager_name(pool, record):
prefix = _root_traffic_manager_name(record)
return f'{prefix}-pool-{pool}'
def _healthcheck_num_failures(record):
return (
record._octodns.get('azuredns', {})
.get('healthcheck', {})
.get('num_failures', 3)
)
def _healthcheck_interval(record):
return (
record._octodns.get('azuredns', {})
.get('healthcheck', {})
.get('interval', 30)
)
def _healthcheck_timeout(record):
default = 10 if _healthcheck_interval(record) > 10 else 9
return (
record._octodns.get('azuredns', {})
.get('healthcheck', {})
.get('timeout', default)
)
def _get_monitor(record):
monitor = MonitorConfig(
protocol=record.healthcheck_protocol,
port=record.healthcheck_port,
path=record.healthcheck_path,
interval_in_seconds=_healthcheck_interval(record),
timeout_in_seconds=_healthcheck_timeout(record),
tolerated_number_of_failures=_healthcheck_num_failures(record),
)
host = record.healthcheck_host()
if host:
monitor.custom_headers = [
MonitorConfigCustomHeadersItem(name='Host', value=host)
]
return monitor
def _check_valid_dynamic(record):
typ = record._type
if typ in ['A', 'AAAA']:
if len(record.values) > 1:
# we don't yet support multi-value defaults
raise AzureException(
f'{record.fqdn} {record._type}: Dynamic records do not support multiple top-level values'
)
elif typ != 'CNAME':
# dynamic records of unsupported type
raise AzureException(
f'{record.fqdn}: Dynamic records in Azure must '
'be of type A/AAAA/CNAME'
)
def _profile_is_match(have, desired):
if have is None or desired is None:
return False
log = getLogger('azuredns._profile_is_match').debug
def false(have, desired, name=None):
prefix = f'profile={name}' if name else ''
attr = have.__class__.__name__
log('%s have.%s = %s', prefix, attr, have)
log('%s desired.%s = %s', prefix, attr, desired)
return False
# compare basic attributes
if (
have.name != desired.name
or have.traffic_routing_method != desired.traffic_routing_method
or len(have.endpoints) != len(desired.endpoints)
):
return false(have, desired)
# compare dns config
dns_have = have.dns_config
dns_desired = desired.dns_config
if (
dns_have.ttl != dns_desired.ttl
or dns_have.relative_name is None
or dns_desired.relative_name is None
or dns_have.relative_name != dns_desired.relative_name
):
return false(dns_have, dns_desired, have.name)
# compare monitoring configuration
monitor_have = have.monitor_config
monitor_desired = desired.monitor_config
if (
monitor_have.protocol != monitor_desired.protocol
or monitor_have.port != monitor_desired.port
or monitor_have.path != monitor_desired.path
or monitor_have.tolerated_number_of_failures
!= monitor_desired.tolerated_number_of_failures
or monitor_have.interval_in_seconds
!= monitor_desired.interval_in_seconds
or monitor_have.timeout_in_seconds != monitor_desired.timeout_in_seconds
or monitor_have.custom_headers != monitor_desired.custom_headers
):
return false(monitor_have, monitor_desired, have.name)
# compare endpoints
method = have.traffic_routing_method
if method == 'Priority':
have_endpoints = sorted(have.endpoints, key=lambda e: e.priority)
desired_endpoints = sorted(desired.endpoints, key=lambda e: e.priority)
elif method == 'Weighted':
have_endpoints = sorted(have.endpoints, key=lambda e: e.target)
desired_endpoints = sorted(desired.endpoints, key=lambda e: e.target)
else:
have_endpoints = have.endpoints
desired_endpoints = desired.endpoints
endpoints = zip(have_endpoints, desired_endpoints)
for have_endpoint, desired_endpoint in endpoints:
have_status = have_endpoint.endpoint_status or EndpointStatus.ENABLED
desired_status = (
desired_endpoint.endpoint_status or EndpointStatus.ENABLED
)
have_always_serve = have_endpoint.always_serve or AlwaysServe.DISABLED
desired_always_serve = (
desired_endpoint.always_serve or AlwaysServe.DISABLED
)
# compare basic attributes
if (
have_endpoint.name != desired_endpoint.name
or have_endpoint.type != desired_endpoint.type
or have_status != desired_status
):
return false(have_endpoint, desired_endpoint, have.name)
# check always_serve only if endpoint is enabled, otherwise it doesn't matter
if (
have_status == EndpointStatus.ENABLED
and have_always_serve != desired_always_serve
):
return false(have_endpoint, desired_endpoint, have.name)
# compare geos
if method == 'Geographic':
have_geos = sorted(have_endpoint.geo_mapping)
desired_geos = sorted(desired_endpoint.geo_mapping)
if have_geos != desired_geos:
return false(have_endpoint, desired_endpoint, have.name)
# compare subnets
if method == 'Subnet':
have_subnets = sorted(
_parse_azure_subnets(have_endpoint.subnets or [])
)
desired_subnets = sorted(
_parse_azure_subnets(desired_endpoint.subnets or [])
)
if have_subnets != desired_subnets:
return false(have_endpoint, desired_endpoint, have.name)
# compare priorities
if (
method == 'Priority'
and have_endpoint.priority != desired_endpoint.priority
):
return false(have_endpoint, desired_endpoint, have.name)
# compare weights
if (
method == 'Weighted'
and have_endpoint.weight != desired_endpoint.weight
):
return false(have_endpoint, desired_endpoint, have.name)
# compare targets
target_type = have_endpoint.type.split('/')[-1]
if target_type == 'externalEndpoints':
if have_endpoint.target != desired_endpoint.target:
return false(have_endpoint, desired_endpoint, have.name)
elif target_type == 'nestedEndpoints':
if (
have_endpoint.target_resource_id
!= desired_endpoint.target_resource_id
):
return false(have_endpoint, desired_endpoint, have.name)
else:
# unexpected, give up
return False
return True
def _endpoint_flags_to_value_status(endpoint_status, always_serve):
"""Convert between azure endpoint's endpoint_status and always_serve flags and octo's pool status flag"""
if endpoint_status is None:
endpoint_status = EndpointStatus.ENABLED
if always_serve is None:
always_serve = AlwaysServe.DISABLED
if endpoint_status == EndpointStatus.DISABLED:
# It doesn't matter what always_serve is if endpoint is disabled
return 'down'
elif always_serve == AlwaysServe.ENABLED:
return 'up'
else:
return 'obey'
def _value_status_to_endpoint_flags(value_status):
"""Convert between octo's pool status flag and azure endpoint's endpoint_status and always_serve flags"""
status_map = {
'down': (EndpointStatus.DISABLED, AlwaysServe.DISABLED),
'up': (EndpointStatus.ENABLED, AlwaysServe.ENABLED),
'obey': (EndpointStatus.ENABLED, AlwaysServe.DISABLED),
}
return status_map[value_status]
def _format_azure_subnets(subnets):
az_subnets = []
for subnet in subnets:
network = ip_network(subnet)
az_subnets.append(
EndpointPropertiesSubnetsItem(
first=network[0], scope=network.prefixlen
)
)
return az_subnets
def _parse_azure_subnets(az_subnets):
subnets = []
for az_subnet in az_subnets:
prefix = ip_address(az_subnet.first)
prefix_len = az_subnet.scope
subnets.append(f'{prefix}/{prefix_len}')
return subnets
class AzureBaseProvider(BaseProvider):
SUPPORTS_GEO = False
SUPPORTS_POOL_VALUE_STATUS = True
SUPPORTS_MULTIVALUE_PTR = True
SUPPORTS = set(
('A', 'AAAA', 'CAA', 'CNAME', 'MX', 'NS', 'PTR', 'SRV', 'TXT')
)
CREDENTIAL_METHOD_CLIENT_SECRET = "client_secret"
CREDENTIAL_METHOD_CLI = "cli"
def __init__(
self,
id,
sub_id,
resource_group,
directory_id=None,
client_id=None,
key=None,
client_credential_method=CREDENTIAL_METHOD_CLIENT_SECRET,
client_total_retries=10,
client_status_retries=3,
client_retry_policy=None,
authority="https://login.microsoftonline.com",
base_url="https://management.azure.com",
top=100,
*args,
**kwargs,
):
self.log = getLogger(f'{self.__class__.__name__}[{id}]')
self.log.debug(
'__init__: id=%s, client_id=%s, '
'key=***, directory_id:%s, authority:%s, '
'base_url:%s, client_total_retries:%d, '
'client_status_retries:%d, client_retry_policy:%s, top:%d',
id,
client_id,
directory_id,
authority,
base_url,
client_total_retries,
client_status_retries,
client_retry_policy,
top,
)
super().__init__(id, *args, **kwargs)
# Store necessary initialization params
self._authority = authority
self._base_url = base_url
self._client_method = client_credential_method
self._client_client_id = client_id
self._client_key = key
self._client_directory_id = directory_id
self._client_subscription_id = sub_id
self.__client_credential = None
self._dns_client = None
self._dns_client_top = top
self._resource_group = resource_group
self._traffic_managers = dict()
self.__azure_zones = None
self._required_root_ns_values = {}
if client_retry_policy is None:
# we didn't get a full retry_policy, so use the params to create one
client_retry_policy = {
'total_retries': client_total_retries,
'status_retries': client_status_retries,
}
# RetryPolicy does not take it's configuration values as params, they
# have to be explicitly set as properties, see examples:
# https://learn.microsoft.com/en-us/python/api/azure-core/azure.core.pipeline.policies.retrypolicy?view=azure-python#examples
retry_policy = RetryPolicy()
for k, v in client_retry_policy.items():
setattr(retry_policy, k, v)
self._dns_client_retry_policy = retry_policy
@property
def _client_credential(self):
if self.__client_credential is None:
# Azure's logger spits out a lot of debug messages at 'INFO'
# level, override it by re-assigning `info` method to `debug`
# (ugly hack until I find a better way)
logger_name = 'azure.core.pipeline.policies.http_logging_policy'
logger = getLogger(logger_name)
logger.info = logger.debug
if self._client_method == self.CREDENTIAL_METHOD_CLIENT_SECRET:
self.__client_credential = ClientSecretCredential(
client_id=self._client_client_id,
client_secret=self._client_key,
tenant_id=self._client_directory_id,
authority=self._authority,
logger=logger,
)
elif self._client_method == self.CREDENTIAL_METHOD_CLI:
self.__client_credential = AzureCliCredential()
else:
raise AzureException(
f'Unknown credential method: {self._client_method}'
)
return self.__client_credential
@property
def _azure_zones(self):
if self.__azure_zones is None:
self.log.debug('_azure_zones: loading')
zones = set()
list_zones = self._dns_client_zones().list_by_resource_group
for zone in list_zones(self._resource_group):
zones.add(zone.name.rstrip('.'))
self.__azure_zones = zones
return self.__azure_zones
def _check_zone(self, name, create=False):
'''Checks whether a zone specified in a source exist in Azure server.
Note that Azure zones omit end '.' eg: contoso.com vs contoso.com.
Returns the name if it exists.
:param name: Name of a zone to checks
:type name: str
:param create: If True, creates the zone of that name.
:type create: bool
:type return: str or None
'''
self.log.debug('_check_zone: name=%s create=%s', name, create)
# Check if the zone already exists in our set
if name in self._azure_zones:
return name
# If not, and its time to create, lets do it.
if create:
self.log.debug('_check_zone:no matching zone; creating %s', name)
zone = self._create_zone(name)
self._azure_zones.add(name)
# we create the zone so we should now be able to get its root ns
# records
self._required_root_ns_values[name] = set(zone.name_servers)
return name
else:
# Else return nothing (aka false)
return
def list_zones(self):
return sorted([f'{z}.' for z in self._azure_zones])
def populate(self, zone, target=False, lenient=False):
'''Required function of manager.py to collect records from zone.
Special notes for Azure.
Azure zone names omit final '.'
Azure root records names are represented by '@'. OctoDNS uses ''
Azure records created through online interface may have null values
(eg, no IP address for A record).
Azure online interface allows constructing records with null values
which are destroyed by _apply.
Specific quirks such as these are responsible for any non-obvious
parsing in this function and the functions '_params_for_*'.
:param zone: A dns zone
:type zone: octodns.zone.Zone
:param target: Checks if Azure is source or target of config.
Currently only supports as a target. Unused.
:type target: bool
:param lenient: Unused. Check octodns.manager for usage.
:type lenient: bool
:type return: void
'''
self.log.debug('populate: name=%s', zone.name)
exists = False
before = len(zone.records)
zone_name = zone.name[:-1]
if self._check_zone(zone_name):
exists = True
rg = self._resource_group
top = self._dns_client_top
for azrecord in self._zone_records(rg, zone_name, top):
typ = _parse_azure_type(azrecord.type)
if typ not in self.SUPPORTS:
continue
record = self._populate_record(zone, azrecord, lenient)
zone.add_record(record, lenient=lenient)
if record._type == 'NS' and record.name == '':
# we have the root NS record, record its azure-dns values
required_values = set(
[v for v in record.values if 'azure-dns' in v]
)
self._required_root_ns_values[zone_name] = required_values
self.log.info(
'populate: found %s records, exists=%s',
len(zone.records) - before,
exists,
)
return exists
def _populate_record(self, zone, azrecord, lenient=False):
record_name = azrecord.name if azrecord.name != '@' else ''
typ = _parse_azure_type(azrecord.type)
data_for = getattr(self, f'_data_for_{typ}')
data = data_for(azrecord)
data['type'] = typ
data['ttl'] = azrecord.ttl
return Record.new(zone, record_name, data, source=self, lenient=lenient)
def _data_for_A(self, azrecord):
return {'values': [ar.ipv4_address for ar in azrecord.a_records]}
def _data_for_AAAA(self, azrecord):
return {'values': [ar.ipv6_address for ar in azrecord.aaaa_records]}
def _data_for_CAA(self, azrecord):
return {
'values': [
{'flags': ar.flags, 'tag': ar.tag, 'value': ar.value}
for ar in azrecord.caa_records
]
}
def _data_for_CNAME(self, azrecord):
'''Parsing data from Azure DNS Client record call
:param azrecord: a return of a call to list azure records
:type azrecord: azure.mgmt.dns.models.RecordSet
:type return: dict
'''
return {'value': _check_endswith_dot(azrecord.cname_record.cname)}
def _data_for_MX(self, azrecord):
return {
'values': [
{'preference': ar.preference, 'exchange': ar.exchange}
for ar in azrecord.mx_records
]
}
def _data_for_NS(self, azrecord):
vals = [ar.nsdname for ar in azrecord.ns_records]
return {'values': [_check_endswith_dot(val) for val in vals]}
def _data_for_PTR(self, azrecord):
vals = [ar.ptrdname for ar in azrecord.ptr_records]
return {'values': [_check_endswith_dot(val) for val in vals]}
def _data_for_SRV(self, azrecord):
return {
'values': [
{
'priority': ar.priority,
'weight': ar.weight,
'port': ar.port,
'target': ar.target,
}
for ar in azrecord.srv_records
]
}
def _data_for_TXT(self, azrecord):
return {
'values': [
escape_semicolon(reduce((lambda a, b: a + b), ar.value))
for ar in azrecord.txt_records
]
}
def _apply_Delete(self, change):
'''A record from change must be deleted.
:param change: a change object
:type change: octodns.record.Change
:type return: void
'''
record = change.existing
ar = _AzureRecord(self._resource_group, record, delete=True)
self._delete_record(
self._resource_group,
ar.zone_name,
ar.relative_record_set_name,
ar.record_type,
)
self.log.debug('* Success Delete: %s', record)
def _apply(self, plan):
'''Required function of manager.py to actually apply a record change.
:param plan: Contains the zones and changes to be made
:type plan: octodns.provider.base.Plan
:type return: void
'''
desired = plan.desired
changes = plan.changes
self.log.debug(
'_apply: zone=%s, len(changes)=%d', desired.name, len(changes)
)
azure_zone_name = desired.name[: len(desired.name) - 1]
self._check_zone(azure_zone_name, create=True)
'''
Force the operation order to be Delete() before all other operations.
Helps avoid problems in updating
- a CNAME record into an A record.
- an A record into a CNAME record.
'''
for change in changes:
class_name = change.__class__.__name__
if class_name == 'Delete':
self._apply_Delete(change)
for change in changes:
class_name = change.__class__.__name__
if class_name == 'Delete':
continue
getattr(self, f'_apply_{class_name}')(change)
class AzureProvider(AzureBaseProvider):
'''
Azure DNS Provider
azuredns.py:
class: octodns_azure.AzureProvider
# Current support of authentication of access to Azure services only
# includes using a Service Principal:
# https://docs.microsoft.com/en-us/azure/azure-resource-manager/
# resource-group-create-service-principal-portal
# The Azure Active Directory Application ID (aka client ID):
client_id:
# Authentication Key Value: (note this should be secret)
key:
# Directory ID (aka tenant ID):
directory_id:
# Subscription ID:
sub_id:
# Resource Group name:
resource_group:
# All are required to authenticate.
#
# The maximum number of record sets to return per page.
# https://learn.microsoft.com/en-us/rest/api/dns/record-sets/list-by-dns-zone
# Top default 100
top: 100
Example config file with variables:
"
---
providers:
config:
class: octodns.provider.yaml.YamlProvider
directory: ./config (example path to directory of zone files)
azuredns:
class: octodns_azure.AzureProvider
client_id: env/AZURE_APPLICATION_ID
key: env/AZURE_AUTHENTICATION_KEY
directory_id: env/AZURE_DIRECTORY_ID
sub_id: env/AZURE_SUBSCRIPTION_ID
resource_group: 'TestResource1'
top: 500
zones:
example.com.:
sources:
- config
targets:
- azuredns
"
The first four variables above can be hidden in environment variables
and octoDNS will automatically search for them in the shell. It is
possible to also hard-code into the config file: eg, resource_group.
Please read https://github.com/octodns/octodns/pull/706 for an overview
of how dynamic records are designed and caveats of using them.
'''
SUPPORTS_ROOT_NS = True
SUPPORTS_DYNAMIC = True
SUPPORTS_DYNAMIC_SUBNETS = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__tm_client = None
@property
def dns_client(self):
if self._dns_client is None:
self._dns_client = DnsManagementClient(
credential=self._client_credential,
subscription_id=self._client_subscription_id,
retry_policy=self._dns_client_retry_policy,
base_url=self._base_url,
)
return self._dns_client
@property
def _tm_client(self):
if self.__tm_client is None: