You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

150 lines
5.7 KiB

#
#
#
from octodns.processor.base import BaseProcessor
class TemplatingError(Exception):
def __init__(self, record, msg):
self.record = record
msg = f'Invalid record "{record.fqdn}", {msg}'
super().__init__(msg)
class Templating(BaseProcessor):
'''
Record templating using python format. For simple records like TXT and CAA
that is the value itself. For multi-field records like MX or SRV it's the
text portions, exchange and target respectively.
Example Processor Config:
templating:
class: octodns.processor.templating.Templating
# When `trailing_dots` is disabled, trailing dots are removed from all
# built-in variables values who represent a FQDN, like `{zone_name}`
# or `{record_fqdn}`. Optional. Default to `True`.
trailing_dots: False
# Any k/v present in context will be passed into the .format method and
# thus be available as additional variables in the template. This is all
# optional.
context:
key: value
another: 42
Example Records:
foo:
type: TXT
value: The zone this record lives in is {zone_name}. There are {zone_num_records} record(s).
bar:
type: MX
values:
- preference: 1
exchange: mx1.{zone_name}.mail.mx.
- preference: 1
exchange: mx2.{zone_name}.mail.mx.
Note that validations for some types reject values with {}. When
encountered the best option is to use record level `lenient: true`
https://github.com/octodns/octodns/blob/main/docs/records.md#lenience
Note that if you need to add dynamic context you can create a custom
processor that inherits from Templating and passes them into the call to
super, e.g.
class MyTemplating(Templating):
def __init__(self, *args, context={}, **kwargs):
context['year'] = lambda desired, sources: datetime.now().strftime('%Y')
super().__init__(*args, context, **kwargs)
See https://docs.python.org/3/library/string.html#custom-string-formatting
for details on formatting options. Anything possible in an `f-string` or
`.format` should work here.
'''
def __init__(self, id, *args, trailing_dots=True, context={}, **kwargs):
super().__init__(id, *args, **kwargs)
self.trailing_dots = trailing_dots
self.context = context
def process_source_and_target_zones(self, desired, existing, provider):
zone_name = desired.decoded_name
zone_decoded_name = desired.decoded_name
zone_encoded_name = desired.name
if not self.trailing_dots:
zone_name = zone_name[:-1]
zone_decoded_name = zone_decoded_name[:-1]
zone_encoded_name = zone_encoded_name[:-1]
zone_params = {
'zone_name': zone_name,
'zone_decoded_name': zone_decoded_name,
'zone_encoded_name': zone_encoded_name,
'zone_num_records': len(desired.records),
# add any extra context provided to us, if the value is a callable
# object call it passing our params so that arbitrary dynamic
# context can be added for use in formatting
**{
k: (v(desired, provider) if callable(v) else v)
for k, v in self.context.items()
},
}
def build_params(record):
record_fqdn = record.decoded_fqdn
record_decoded_fqdn = record.decoded_fqdn
record_encoded_fqdn = record.fqdn
if not self.trailing_dots:
record_fqdn = record_fqdn[:-1]
record_decoded_fqdn = record_decoded_fqdn[:-1]
record_encoded_fqdn = record_encoded_fqdn[:-1]
return {
'record_name': record.decoded_name,
'record_decoded_name': record.decoded_name,
'record_encoded_name': record.name,
'record_fqdn': record_fqdn,
'record_decoded_fqdn': record_decoded_fqdn,
'record_encoded_fqdn': record_encoded_fqdn,
'record_type': record._type,
'record_ttl': record.ttl,
'record_source_id': record.source.id if record.source else None,
**zone_params,
}
def template(value, params, record):
try:
return value.template(params)
except KeyError as e:
raise TemplatingError(
record,
f'undefined template parameter "{e.args[0]}" in value',
) from e
for record in desired.records:
params = build_params(record)
if hasattr(record, 'values'):
if record.values and not hasattr(record.values[0], 'template'):
# the (custom) value type does not support templating
continue
new_values = [
template(v, params, record) for v in record.values
]
if record.values != new_values:
new = record.copy()
new.values = new_values
desired.add_record(new, replace=True)
else:
if not hasattr(record.value, 'template'):
# the (custom) value type does not support templating
continue
new_value = template(record.value, params, record)
if record.value != new_value:
new = record.copy()
new.value = new_value
desired.add_record(new, replace=True)
return desired, existing