Technical workbook

Provider conventions

The conventions described in this section must be followed by any provider implementation.

Contract of a Lexicon record

A Lexicon record is the internal representation of a DNS entry fetched or pushed to a DNS provider API. These records are JSON objects that must follows the given contract.

Required fields

  • name Clients should provide FQDN. Providers should handle both FQDN and relative names.

  • ttl Reasonable default is 6 hours since it’s supported by most services. Any service that does not support this must be explicitly mentioned somewhere.

  • record All provider/API records must be translated to the following format:

Example of a Lexicon record

{
    'id': string, // optional, provider specified unique id. Clients to treat this as opaque.
    'type': string, // upper case, valid record type. eg. A, CNAME, TXT
    'name': string, // lowercase, FQDN. eg. test.record.example.com
    'ttl': integer, // positive integer, in seconds. eg. 3600
    'content': string, //double quoted/escaped values should be unescaped. eg. "\"TXT content\"" should become "TXT content"
    'options': {
        'mx': { // MX options
            'priority': integer
        }
    }
}

DNS operations

A Lexicon provider will have to make operations against a DNS provider API. Here are the 5 possible operations, and the behavior each operation must follow.

authenticate

  • Normal Behavior Execute all required operations to authenticate against the provider API, then retrieves the identifier of the domain and assign it to the self.domain_id property of the Provider instance.

  • Authentication failure In case of authentication failure, the method must raise a lexicon.exceptions.AuthenticationError exception and break the flow.

create_record

  • Normal Behavior Create a new DNS record. Return a boolean True if successful.

  • If Record Already Exists Do nothing. DO NOT throw exception.

  • TTL If not specified or set to 0, use reasonable default.

  • Record Sets If service supports record sets, create new record set or append value to existing record set as required.

list_record

  • Normal Behaviour List all records. If filters are provided, send to the API if possible, else apply filter locally. Return value should be a list of records.

  • Record Sets Ungroup record sets into individual records. Eg: If a record set contains 3 values, provider ungroup them into 3 different records.

  • Linked Records For services that support some form of linked record, do not resolve, treat as CNAME.

update_record

  • Normal Behaviour Update a record. Record to be updated can be specified by providing id OR name, type and content. Return a boolean True if successful.

  • Record Sets If matched record is part of a record set, only update the record that matches. Update the record set so that records other than the matched one are unmodified.

  • TTL

    • If not specified, do not modify ttl.

    • If set to 0, reset to reasonable default.

  • No Match Throw exception?

delete_record

  • Normal Behaviour Remove a record. Record to be deleted can be specified by providing id OR name, type and content. Return a boolean True if successful.

  • Record sets Remove only the record that matches all the filters.

    • If content is not specified, remove the record set.

    • If length of record set becomes 0 after removing record, remove the record set.

    • Otherwise, remove only the value that matches and leave other records as-is.

  • No Match Do nothing. DO NOT throw exception

Code documentation

This section describes the public API of Lexicon code (classes, methods, functions) useful to implement a new provider, or to interface Lexicon as a library to another project.

Module lexicon.client

Main module of Lexicon. Defines the Client class, that holds all Lexicon logic.

class lexicon.client.Client(config: ConfigResolver | dict[str, Any] | None = None)

This is the Lexicon client, that will execute all the logic.

execute() bool | list[dict[str, Any]]

(deprecated) Execute provided configuration in class constructor to the DNS records

Module lexicon.interfaces

Base provider module for all Lexicon providers

class lexicon.interfaces.Provider(config: ConfigResolver | dict[str, Any])

This is the abstract class for all lexicon Providers. It provides common functionality and ensures that all implemented Providers follow a standard ducktype. All standardized options will be provided here as defaults, but can be overwritten by environmental variables and cli arguments.

Common options are:

action domain type name content ttl priority identifier

The provider_env_cli_options will also contain any Provider specific options:

auth_username auth_token auth_password …

Parameters:

config – is a ConfigResolver object that contains all the options for this provider, merged from CLI and Env variables.

abstract authenticate() None

Authenticate against provider, Make any requests required to get the domain’s id for this provider, so it can be used in subsequent calls. Should throw AuthenticationError or requests.HTTPError if authentication fails for any reason, of if the domain does not exist.

cleanup() None

Clean any relevant resource before this provider instance is closed.

abstract static configure_parser(parser: ArgumentParser) None

Configure the given parser for the provider needs (e.g. specific CLI flags for auth)

abstract create_record(rtype: str, name: str, content: str) bool

Create record. If record already exists with the same content, do nothing.

abstract delete_record(identifier: str | None = None, rtype: str | None = None, name: str | None = None, content: str | None = None) bool

Delete an existing record. If record does not exist, do nothing. If an identifier is specified, use it, otherwise do a lookup using type, name and content.

abstract static get_nameservers() list[str] | list[Pattern]

Return the list of nameservers for this DNS provider

abstract list_records(rtype: str | None = None, name: str | None = None, content: str | None = None) list[dict[str, Any]]

List all records. Return an empty list if no records found type, name and content are used to filter records. If possible filter during the query, otherwise filter after response is received.

abstract update_record(identifier: str | None = None, rtype: str | None = None, name: str | None = None, content: str | None = None) bool

Update a record. Identifier must be specified.

Module lexicon.config

Definition of the ConfigResolver to configure Lexicon, and convenient classes to build various configuration sources.

class lexicon.config.ArgsConfigSource(namespace: Namespace)

ConfigSource that resolve configuration against an argparse namespace.

resolve(config_key: str) str | None

Using the given config_parameter value (in the form of lexicon:config_key or lexicon:[provider]:config_key), try to get the associated value.

None must be returned if no value could be found.

Must be implemented by each ConfigSource concrete child class.

class lexicon.config.ConfigResolver

Highly customizable configuration resolver object, that gets configuration parameters from various sources with a precedence order. Sources and their priority are configured by calling the with* methods of this object, in the decreasing priority order.

A configuration parameter can be retrieved using the resolve() method. The configuration parameter key needs to conform to a namespace, whose delimeters is :. Two namespaces will be used in the context of Lexicon:

  • the parameters relevant for Lexicon itself: lexicon:global_parameter

  • the parameters specific to a DNS provider: lexicon:cloudflare:cloudflare_parameter

Example:

# This will resolve configuration parameters from environment variables,
# then from a configuration file named ``/my/path/to/lexicon.yml``.
from lexicon.config import ConfigResolver

config = ConfigResolver()
config.with_env().with_config_file()

print(config.resolve('lexicon:delegated'))
print(config.resolve('lexicon:cloudflare:auth_token'))
Config can resolve parameters for Lexicon and providers from:
  • environment variables

  • arguments parsed by ArgParse library

  • YAML configuration files, generic or specific to a provider

  • any object implementing the underlying ConfigSource class

Each parameter will be resolved against each source, and value from the higher priority source is returned. If a parameter could not be resolved by any source, then None will be returned.

add_config_source(config_source: ConfigSource, position: int | None = None) None

Add a config source to the current ConfigResolver instance. If position is not set, this source will be inserted with the lowest priority.

resolve(config_key: str) str | None

Resolve the value of the given config parameter key. Key must be correctly scoped for Lexicon, and optionally for the DNS provider for which the parameter is consumed.

For instance:
  • config.resolve('lexicon:delegated') will get the delegated parameter for Lexicon

  • config.resolve('lexicon:cloudflare:auth_token') will get the auth_token parameter consumed by cloudflare DNS provider.

Value is resolved against each configured source, and value from the highest priority source is returned. None will be returned if the given config parameter key could not be resolved from any source.

with_args(argparse_namespace: Namespace) ConfigResolver

Configure current resolver to use a Namespace object given by a ArgParse instance using arg_parse() as a source. This method is typically used to allow a ConfigResolver to get parameters from the command line.

It is assumed that the argument parser have already checked that provided arguments are valid for Lexicon or the current provider. No further namespace check on parameter keys will be done here. Meaning that if lexicon:cloudflare:auth_token is asked, any auth_token present in the given Namespace object will be returned.

with_config_dir(dir_path: str | PathLike[str]) ConfigResolver

Configure current resolver to use every valid YAML configuration files available in the given directory path. To be taken into account, a configuration file must conform to the following naming convention:

  • lexicon.yml for a global Lexicon config file (see with_config_file doc)

  • lexicon_[provider].yml for a DNS provider specific configuration file, with [provider]

    equals to the DNS provider name (see with_provider_config_file doc)

Example:

$ ls /etc/lexicon
lexicon.yml # global Lexicon configuration file
lexicon_cloudflare.yml # specific configuration file for clouflare DNS provder
with_config_file(file_path: str | bytes | PathLike[str] | PathLike[bytes]) ConfigResolver

Configure current resolver to use a YAML configuration file specified on the given path. This file provides configuration parameters for Lexicon and any DNS provider.

Typical format is:

$ cat lexicon.yml
# Will define properties 'lexicon:delegated' and 'lexicon:cloudflare:auth_token'
delegated: 'onedelegated'
cloudflare:
auth_token: SECRET_TOKEN
with_config_source(config_source: ConfigSource) ConfigResolver

Configure current resolver to use the provided ConfigSource instance to be used as a source. See documentation of ConfigSource to see how to implement correctly a ConfigSource.

with_dict(dict_object: dict[str, Any]) ConfigResolver

Configure current resolver to use the given dict object, scoped to lexicon namespace.

Example of valid dict object for lexicon, defining properties lexicon:delegated and lexicon:cloudflare:auth_token

{
    "delegated": "onedelegated",
    "cloudflare": {
        "auth_token": "SECRET_TOKEN"
    }
}
with_env() ConfigResolver

Configure current resolver to use available environment variables as a source. Only environment variables starting with ‘LEXICON’ or ‘LEXICON_[PROVIDER]’ will be taken into account.

with_legacy_dict(legacy_dict_object: dict[str, Any]) ConfigResolver

Configure a source that consumes the dict that where used on Lexicon 2.x

with_provider_config_file(provider_name: str, file_path: str | bytes | PathLike[str] | PathLike[bytes]) ConfigResolver

Configure current resolver to use a YAML configuration file specified on the given path. This file provides configuration parameters for a DNS provider exclusively.

Typical format is:

$ cat lexicon_cloudflare.yml
auth_token: SECRET_TOKEN  # Will define property 'lexicon:cloudflare:auth_token'
auth_username: USERNAME  # Will define property 'lexicon:cloudflare:auth_username'

NB: If file_path is not specified, /etc/lexicon/lexicon_[provider].yml will be taken by default, with [provider] equals to the given provider_name parameter.

class lexicon.config.ConfigSource

Base class to implement a configuration source for a ConfigResolver. The relevant method to override is resolve(self, config_parameter).

resolve(config_key: str) str | None

Using the given config_parameter value (in the form of lexicon:config_key or lexicon:[provider]:config_key), try to get the associated value.

None must be returned if no value could be found.

Must be implemented by each ConfigSource concrete child class.

class lexicon.config.DictConfigSource(dict_object: dict[str, Any])

ConfigSource that resolve configuration against a dict object.

resolve(config_key: str) str | None

Using the given config_parameter value (in the form of lexicon:config_key or lexicon:[provider]:config_key), try to get the associated value.

None must be returned if no value could be found.

Must be implemented by each ConfigSource concrete child class.

class lexicon.config.EnvironmentConfigSource

ConfigSource that resolve configuration against existing environment variables.

resolve(config_key: str) str | None

Using the given config_parameter value (in the form of lexicon:config_key or lexicon:[provider]:config_key), try to get the associated value.

None must be returned if no value could be found.

Must be implemented by each ConfigSource concrete child class.

class lexicon.config.FileConfigSource(file_path: str | bytes | PathLike[str] | PathLike[bytes])

ConfigSource that resolve configuration against a lexicon config file.

class lexicon.config.LegacyDictConfigSource(dict_object: dict[str, Any])

ConfigSource that resolve configuration against a legacy Lexicon 2.x dict object.

class lexicon.config.ProviderFileConfigSource(provider_name: str, file_path: str | bytes | PathLike[str] | PathLike[bytes])

ConfigSource that resolve configuration against a provider config file.

lexicon.config.legacy_config_resolver(legacy_dict: dict[str, Any]) ConfigResolver

With the old legacy approach, we juste got a plain configuration dict object. Custom logic was to enrich this configuration with env variables.

This function create a resolve that respect the expected behavior, by using the relevant ConfigSources, and we add the config files from working directory.

lexicon.config.non_interactive_config_resolver() ConfigResolver

Create a typical config resolver in a non-interactive context (e.g. lexicon used as a library). Configuration will be resolved against env variables and lexicon config files in working dir.

Module lexicon.exceptions

Lexicon exceptions module

exception lexicon.exceptions.AuthenticationError

Authentication to the provider failed, likely username, password or domain mismatch

exception lexicon.exceptions.LexiconError

Base Class for the Lexicon Exception hierarchy

exception lexicon.exceptions.ProviderNotAvailableError

Custom exception to raise when a provider is not available, typically because some optional dependencies are missing