-
-
Notifications
You must be signed in to change notification settings - Fork 392
/
meta.py
150 lines (130 loc) · 4.88 KB
/
meta.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
#
#
#
from datetime import datetime
from logging import getLogger
from uuid import uuid4
from .. import __version__
from ..record import Record
from .base import BaseProcessor
# TODO: remove once we require python >= 3.11
try: # pragma: no cover
from datetime import UTC
except ImportError: # pragma: no cover
from datetime import timedelta, timezone
UTC = timezone(timedelta())
def _keys(values):
return set(v.split('=', 1)[0] for v in values)
class MetaProcessor(BaseProcessor):
'''
Add a special metadata record with timestamps, UUIDs, versions, and/or
provider name. Will only be updated when there are other changes being made.
A useful tool to aid in debugging and monitoring of DNS infrastructure.
Timestamps or UUIDs can be useful in checking whether changes are
propagating, either from a provider's backend to their servers or via AXFRs.
Provider can be utilized to determine which DNS system responded to a query
when things are operating in dual authority or split horizon setups.
Creates a TXT record with the name configured with values based on processor
settings. Values are in the form `key=<value>`, e.g.
`time=2023-09-10T05:49:04.246953`
processors:
meta:
class: octodns.processor.meta.MetaProcessor
# The name to use for the meta record.
# (default: meta)
record_name: meta
# Include a timestamp with a UTC value indicating the timeframe when the
# last change was made.
# (default: true)
include_time: true
# Include a UUID that can be utilized to uniquely identify the run
# pushing data
# (default: false)
include_uuid: false
# Include the provider id for the target where data is being pushed
# (default: false)
include_provider: false
# Include the octoDNS version being used
# (default: false)
include_version: false
'''
@classmethod
def get_time(cls):
return datetime.now(UTC).isoformat()
@classmethod
def get_uuid(cls):
return str(uuid4())
def __init__(
self,
id,
record_name='meta',
include_time=True,
include_uuid=False,
include_version=False,
include_provider=False,
ttl=60,
):
self.log = getLogger(f'MetaSource[{id}]')
super().__init__(id)
self.log.info(
'__init__: record_name=%s, include_time=%s, include_uuid=%s, include_version=%s, include_provider=%s, ttl=%d',
record_name,
include_time,
include_uuid,
include_version,
include_provider,
ttl,
)
self.record_name = record_name
self.time = self.get_time() if include_time else None
self.uuid = self.get_uuid() if include_uuid else None
self.include_version = include_version
self.include_provider = include_provider
self.ttl = ttl
def values(self, target_id):
ret = []
if self.include_version:
ret.append(f'octodns-version={__version__}')
if self.include_provider:
ret.append(f'provider={target_id}')
if self.time:
ret.append(f'time={self.time}')
if self.uuid:
ret.append(f'uuid={self.uuid}')
return ret
def process_source_and_target_zones(self, desired, existing, target):
meta = Record.new(
desired,
self.record_name,
{'ttl': self.ttl, 'type': 'TXT', 'values': self.values(target.id)},
# we may be passing in empty values here to be filled out later in
# process_source_and_target_zones
lenient=True,
)
desired.add_record(meta)
return desired, existing
def _is_up_to_date_meta(self, change, target_id):
# always something so we can see if its type and name
record = change.record
# existing state, if there is one
existing = getattr(change, 'existing', None)
return (
record._type == 'TXT'
and record.name == self.record_name
and existing is not None
# don't care about the values here, just the fields/keys
and _keys(self.values(target_id)) == _keys(existing.values)
)
def process_plan(self, plan, sources, target):
if (
plan
and len(plan.changes) == 1
and self._is_up_to_date_meta(plan.changes[0], target.id)
):
# the only change is the meta record, and it's not meaningfully
# changing so we don't actually want to make the update, meta should
# only be enough to cause a plan on its own if the fields changed
return None
# There's more than one thing changing so meta should update and/or meta
# is meaningfully changing or being created...
return plan