From 1dffd522ae2c463ec5743cf9d2b0629e86890704 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sun, 17 Aug 2025 17:44:04 -0700 Subject: [PATCH] Add a custom source example to the doc --- .../5557cbfa56bb40bfbb97511af4659996.md | 4 + docs/examples/README.rst | 1 + docs/examples/custom/config/dns.math.yaml | 5 + docs/examples/custom/config/octodns.yaml | 32 ++++ docs/examples/custom/fibonacci.py | 77 ++++++++++ docs/examples/custom/index.rst | 142 ++++++++++++++++++ docs/examples/custom/pyproject.toml | 1 + docs/examples/custom/requirements.txt | 2 + 8 files changed, 264 insertions(+) create mode 100644 .changelog/5557cbfa56bb40bfbb97511af4659996.md create mode 100644 docs/examples/custom/config/dns.math.yaml create mode 100644 docs/examples/custom/config/octodns.yaml create mode 100644 docs/examples/custom/fibonacci.py create mode 100644 docs/examples/custom/index.rst create mode 120000 docs/examples/custom/pyproject.toml create mode 100644 docs/examples/custom/requirements.txt diff --git a/.changelog/5557cbfa56bb40bfbb97511af4659996.md b/.changelog/5557cbfa56bb40bfbb97511af4659996.md new file mode 100644 index 0000000..a97cc07 --- /dev/null +++ b/.changelog/5557cbfa56bb40bfbb97511af4659996.md @@ -0,0 +1,4 @@ +--- +type: none +--- +Add a custom source example to the doc \ No newline at end of file diff --git a/docs/examples/README.rst b/docs/examples/README.rst index 949814c..a6f0730 100644 --- a/docs/examples/README.rst +++ b/docs/examples/README.rst @@ -45,3 +45,4 @@ and running. basic/README.md migrating-to-octodns/README.md + custom/index.rst diff --git a/docs/examples/custom/config/dns.math.yaml b/docs/examples/custom/config/dns.math.yaml new file mode 100644 index 0000000..e0a033d --- /dev/null +++ b/docs/examples/custom/config/dns.math.yaml @@ -0,0 +1,5 @@ +--- +'': + # just going to put something in here for the hell of it + - type: TXT + value: Try querying for TXT records named fibonacci-N where N is an integer 0-25 diff --git a/docs/examples/custom/config/octodns.yaml b/docs/examples/custom/config/octodns.yaml new file mode 100644 index 0000000..539b191 --- /dev/null +++ b/docs/examples/custom/config/octodns.yaml @@ -0,0 +1,32 @@ +--- +# see the basic example for detailed infomation on this file +providers: + config: + class: octodns.provider.yaml.YamlProvider + directory: ./config + + powerdns: + class: octodns_powerdns.PowerDnsProvider + host: 127.0.0.1 + port: 8081 + api_key: env/POWERDNS_API_KEY + timeout: 10 + + # So this is our custom/local source, see fibonacci.py. It works just like + # any other souce/provider. When sync is run the directory where that file + # lives will need to be included in your PYTHONPATH, likely ., so that + # octoDNS can find it. + fibonacci: + class: fibonacci.FibonacciProvider + n: 25 + +zones: + + dns.math.: + sources: + # Statically condifured records (yaml) will be included in this zone + - config + # as well as records generated by our simple custom provider + - fibonacci + targets: + - powerdns diff --git a/docs/examples/custom/fibonacci.py b/docs/examples/custom/fibonacci.py new file mode 100644 index 0000000..0416e15 --- /dev/null +++ b/docs/examples/custom/fibonacci.py @@ -0,0 +1,77 @@ +# +# +# + +from logging import getLogger + +from octodns.record import Record +from octodns.source.base import BaseSource + + +# Based on https://stackoverflow.com/a/3954407 +def _fibonacci(n): + a, b = 0, 1 + for _ in range(n): + yield a + a, b = b, a + b + + +class FibonacciProvider(BaseSource): + # Here only TXT records are supported + SUPPORTS = ('TXT',) + # Do-not support (deprecated) GEO records + SUPPORTS_GEO = False + + # This is pretty much boilerplate, create a logger with a standard name, + # call up to the parent class BaseSource's __init__, store our + # attributes, and then calculate our fibonacci sequence + def __init__(self, id, n, ttl=3600): + klass = self.__class__.__name__ + self.log = getLogger(f'{klass}[{id}]') + self.log.debug( + '__init__: id=%s, variable=%s, name=%s, ttl=%d', id, n, ttl + ) + super().__init__(id) + self.n = n + self.ttl = ttl + + # calculate the requested number of values + self.values = list(_fibonacci(n)) + + def populate(self, zone, target=False, lenient=False): + # This is the method adding records to the zone. For a source it's the + # only thing that needs to be implemented. Again there's some best + # practices wrapping our custom logic, mostly for logging/debug + # purposes. + self.log.debug( + 'populate: name=%s, target=%s, lenient=%s', + zone.name, + target, + lenient, + ) + + before = len(zone.records) + + # This is where the logic of the source lives. Here it's simply + # translated the previously calculated fibonacci sequence into + # corresponding TXT records. It could be anything: reading data from + # disk, calling APIs, ... + for i, value in enumerate(self.values): + # If there's a chance the generated record may result in validation + # errors it would make sense to pass lenient to new + txt = Record.new( + zone, + f'fibonacci-{i}', + {'value': str(value), 'ttl': self.ttl, 'type': 'TXT'}, + ) + # lenient should always be passed to add_record. If there's a + # chance the source will create records that conflict with + # something previously added by another provider it may make sense + # to include `replace` here, possibly with an additional provider + # parameter `populate_should_replace` + zone.add_record(txt, lenient=lenient) + + self.log.info( + 'populate: found %s records, exists=False', + len(zone.records) - before, + ) diff --git a/docs/examples/custom/index.rst b/docs/examples/custom/index.rst new file mode 100644 index 0000000..2a44efd --- /dev/null +++ b/docs/examples/custom/index.rst @@ -0,0 +1,142 @@ +Writing a Custom Source +======================= + +Introduction +------------ + +Creating a custom source of record data for octoDNS is pretty simple and +involves a bit of boilerplate and then filling in a single method, +:py:meth:`octodns.source.base.BaseSource.populate`, with any logic required +to fetch or create the desired records. In this example records will be created +for the first 25 elements of the `Fibonacci Sequence`_. While contrived it +should illustrate the process and requirements. + +.. _Fibonacci Sequence: https://en.wikipedia.org/wiki/Fibonacci_sequence + +Some relevant documentation for this example is in comments in the YAML +configuration files and python code. + +* :download:`config/octodns.yaml` +* :download:`config/dns.math.yaml` +* :download:`fibonacci.py` + +From here on this README focuses on the custom source and the process of +running octoDNS with access to it. + +Checking out the code and setting up the environment +---------------------------------------------------- + +You would not normally need to check out octoDNS itself, you instead would have +a git repo with only your configuration files. Here we're cloning the repo only +to get a copy of the example files:: + + $ git clone https://github.com/octodns/octodns.git + $ cd octodns/examples/basic/ + $ python3 -mvenv env + $ source ../env.sh + $ source env/bin/activate + (env) $ pip install -r requirements.txt + +Finally check out :ref:`Running PowerDNS` to get a local instance of PowerDNS +up and going before continuing. + +Running octoDNS sync +-------------------- + +Once you have your custom source, configuration files, and octoDNS installed +you're ready to run the sync command to get it to plan an initial set of +changes. The main difference here compared to the :ref:`basic-setup` is setting +``PYTHONPATH`` so the source file can be located:: + + (env) $ export PYTHONPATH=. + (env) $ octodns-sync --config-file config/octodns.yaml + 2025-08-17T17:14:58 [140224800168896] INFO Manager __init__: config_file=config/octodns.yaml, (octoDNS 1.13.0) + 2025-08-17T17:14:58 [140224800168896] INFO Manager _config_executor: max_workers=1 + 2025-08-17T17:14:58 [140224800168896] INFO Manager _config_include_meta: include_meta=False + 2025-08-17T17:14:58 [140224800168896] INFO Manager _config_enable_checksum: enable_checksum=False + 2025-08-17T17:14:58 [140224800168896] INFO Manager _config_auto_arpa: auto_arpa=False + 2025-08-17T17:14:58 [140224800168896] INFO Manager __init__: global_processors=[] + 2025-08-17T17:14:58 [140224800168896] INFO Manager __init__: global_post_processors=[] + 2025-08-17T17:14:58 [140224800168896] INFO Manager __init__: provider=config (octodns.provider.yaml 1.13.0) + 2025-08-17T17:14:58 [140224800168896] INFO Manager __init__: provider=powerdns (octodns_powerdns 1.0.0) + 2025-08-17T17:14:58 [140224800168896] INFO Manager __init__: provider=fibonacci (fibonacci n/a) + 2025-08-17T17:14:58 [140224800168896] INFO Manager sync: eligible_zones=[], eligible_targets=[], dry_run=True, force=False, plan_output_fh=, checksum=None + 2025-08-17T17:14:58 [140224800168896] INFO Manager sync: zone=dns.math. + 2025-08-17T17:14:58 [140224800168896] INFO Manager sync: sources=['config', 'fibonacci'] + 2025-08-17T17:14:58 [140224800168896] INFO Manager sync: processors=[] + 2025-08-17T17:14:58 [140224800168896] INFO Manager sync: targets=['powerdns'] + 2025-08-17T17:14:58 [140224800168896] INFO YamlProvider[config] populate: found 1 records, exists=True + 2025-08-17T17:14:58 [140224800168896] INFO FibonacciProvider[fibonacci] populate: found 25 records, exists=False + 2025-08-17T17:14:58 [140224800168896] INFO PowerDnsProvider[powerdns] plan: desired=dns.math. + 2025-08-17T17:14:58 [140224800168896] INFO PowerDnsProvider[powerdns] populate: found 1 records, exists=True + 2025-08-17T17:14:58 [140224800168896] WARNING PowerDnsProvider[powerdns] root NS record supported, but no record is configured for dns.math. + 2025-08-17T17:14:58 [140224800168896] INFO PowerDnsProvider[powerdns] plan: Creates=25, Updates=0, Deletes=0, Existing=1, Meta=False + 2025-08-17T17:14:58 [140224800168896] INFO Plan + ******************************************************************************** + * dns.math. + ******************************************************************************** + * powerdns (PowerDnsProvider) + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Create () + * Summary: Creates=25, Updates=0, Deletes=0, Existing=1, Meta=False + ******************************************************************************** + +The log output +.............. + +Everything here matches the output and meaning of the first run in +:ref:`basic-setup`, with the important difference that both the statically +configured and dynamically generated records are listed as planned changes. +From here a ``--doit`` run can be executed to create the records in the +PowerDNS server, which can then be queried:: + +Viewing the results +------------------- + +``dig`` can now be run to query for the records:: + + $ dig +short TXT dns.math. + "Try querying for TXT records named fibonacci-N where N is an integer 0-25" + $ dig +short TXT fibonacci-0.dns.math. + "0" + $ dig +short TXT fibonacci-23.dns.math. + "28657" + $ dig +short TXT fibonacci-99.dns.math. + +Next Steps +---------- + +If the source will only be used in a single octoDNS setup and you're OK with it +living alongside your config, as was done in this example the +:download:`fibonacci.py` file can be used as a starting point. It is also +possible to create custom processors and providers that work in the same +manner, though the details of the code involved are outside the scope of this +example. + +If the provider will be used more widely or published for others to use, see +`octodns-template`_. + +.. _octodns-template: https://github.com/octodns/octodns-template diff --git a/docs/examples/custom/pyproject.toml b/docs/examples/custom/pyproject.toml new file mode 120000 index 0000000..7aa7944 --- /dev/null +++ b/docs/examples/custom/pyproject.toml @@ -0,0 +1 @@ +../../../pyproject.toml \ No newline at end of file diff --git a/docs/examples/custom/requirements.txt b/docs/examples/custom/requirements.txt new file mode 100644 index 0000000..42c4fd2 --- /dev/null +++ b/docs/examples/custom/requirements.txt @@ -0,0 +1,2 @@ +octodns>=1.0.1 +octodns_powerdns>=0.0.5