For Developers¶
If you are a developer, you may be interested in extending Ruddr with your own notifier and/or updater modules. Or, you may want to integrate Ruddr’s functionality into a larger program. Or, you may just be looking for info about contributing to the project. In any case, this page is for you.
Note
If you have questions or comments on developing updaters, notifiers, or any other Ruddr development, feel free to start a discussion on GitHub. I especially welcome feedback about this developer documentation.
Writing Your Own Notifier¶
A notifier in Ruddr is a class that monitors a source of IP address information and calls a notify function from time to time with the current address. The goal, of course, is to tell Ruddr about a new IP address shortly after it changes.
Most notifiers will fall into one of a few categories:
- Event-based
Monitor some external source of events providing new IP addresses.
- Polling
Poll the current IP address periodically through some means and notify each time. For example, this is what the
iface
andweb
notifiers do.- Event-triggered lookup
Monitor some external source of events to know when the IP address changes, and then check what the current IP address is through some other means. For example, the
systemd
notifier does this: systemd-networkd sends a DBus event when there is a network status change, and when the notifier receives it, it checks the current address assigned to the network interface.
Note
There is no harm in notifying extra times, i.e. when the address hasn’t changed. In fact, polling-style notifiers rely on it. Ruddr keeps track of the address most recently sent to each provider and doesn’t send duplicates.
Ruddr provides a convenient base class, Notifier
, which can be used
to implement all three styles of notifier. It boils down to a few core methods:
setup()
This abstract method is called when it’s time to start the notifier. If the notifier needs to subscribe to any event sources, open any connections, start any background threads, etc., this is the place to do it.
teardown()
The opposite of
setup()
: This abstract method is called when it’s time to stop the notifier. It should clean up any connections, resources, threads, etc. that are no longer needed.check_once()
For notifiers that support checking the current IP address(es) on demand, this abstract method should do so. It’s called after
setup()
, when Ruddr receives SIGUSR1, and can be set to run periodically withset_check_intervals()
.set_check_intervals()
This provides a way to set
check_once()
to run automatically on an interval. It also allows you to set the retry delay for when it fails (whether it’s scheduled to run automatically or not). Call it in the constructor of your notifier, if necessary.
Any new notifier should begin by inheriting from Notifier
, and then
its constructor needs to be written. The constructor’s main job here is to
1) call the superclass constructor and 2) read any required parameters from the
configuration. The constructor signature must match this:
- Notifier.__init__(name, config)
- Parameters:
name (str) – Notifier name, taken from
[notifier.<name>]
in the Ruddr configconfig (Dict[str, str]) – A
dict
of this notifier’s configuration values, all as strings, plus any global configuration options that may be useful (currently, onlydatadir
, which notifiers may use to cache data if necessary).
If there are any errors in the configuration, catch them in the constructor and
raise ConfigError
. The constructor is also the place to call
set_check_intervals()
, if necessary (but more on that in the
following sections). Note that the constructor is not the place to do any
setup that should happen as part of notifier startup—that should happen in
setup()
.
After calling the superclass constructor, two member variables will be
available for your convenience: self.name
, containing the name of the
notifier, and self.log
, a Python logger named after the notifier. You are
encouraged to use self.log
often.
The rest of the implementation varies depending on the style of notifier. The
next three sections, one for each style, discuss that in more detail. Following
those is an API reference for the Notifier
class.
Note
Handling IPv4 vs. IPv6 Addressing
Different networks will have different requirements for IPv4 vs. IPv6
addressing: Some may require one or both, some may not, some may want to
ignore one or both. Notifiers must handle this properly, and the
Notifier
class has methods to help.
If your notifier has config that’s required only for IPv4 or only for IPv6, be sure to implement the
ipv4_ready()
andipv6_ready()
functions.Ruddr may not need both IPv4 and IPv6 addresses from your notifier. It should call
want_ipv4()
andwant_ipv6()
, and if either returnsFalse
, there is no need to notify for that type of address at all.Even if an address type is wanted, it may or may not be an error if your notifier can’t obtain it. If
need_ipv4()
returnsTrue
butcheck_once()
cannot currently obtain an IPv4 address, your notifier should raiseNotifyError
(after notifying for the other address type, if necessary). The same goes forneed_ipv6()
and IPv6 addressing. (This bullet point does not apply ifcheck_once()
is not implemented.)
Event-Based Notifiers¶
This style of notifier receives events from some external source with the
current IP address. Since it gets IP addresses from these events, it can’t
check the IP address on-demand, so this style of notifier will leave
check_once()
unimplemented and doesn’t need to call
set_check_intervals()
.
It should, however, implement setup()
and
teardown()
. Typically, setup()
would involve
setting up a thread to listen on a socket, or setting up a callback for some
event, or something along those lines. Then, teardown()
would
do the opposite.
Be sure to follow the guidelines in the note about IPv4 and IPv6
addressing above. Then, whenever an IPv4 address or
IPv6 prefix is received, call notify_ipv4()
or
notify_ipv6()
.
Be careful not to leave your notifier in an invalid state if
teardown()
happens at an inconvenient time. It’s guaranteed not
to be called before setup()
completes, but apart from that, it
is up to you to ensure that anything happening in a background thread isn’t
interrupted in a way that breaks it.
Polling Notifiers¶
This style of notifier is fairly simple. It periodically checks the current IP address and notifies each time.
Start by calling set_check_intervals()
in the constructor. That
will usually look something like this (but customize the values according to
your needs):
self.set_check_intervals(retry_min_interval=60,
retry_max_interval=86400,
success_interval=10800,
config=config)
The first three parameters set default values, and the last one provides the
config dict, which will override those defaults with any entries that match
(see the API documentation for set_check_interval()
).
The important part there is that success_interval
is set to a default other
than zero. That causes the notifier to automatically call
check_once()
periodically, waiting that many seconds between
calls (assuming there were no errors).
The retry_min_interval
and retry_max_interval
parameters control what
happens if there is an error in check_once()
(more
specifically, if it raises NotifyError
). Such an error triggers the
retry logic, which uses an exponential backoff. The first retry is after
retry_min_interval
seconds. If it fails again, each successive retry
interval is twice as long, maxing out at retry_max_interval
. Once the retry
interval reaches the max, it remains constant until the call succeeds. As soon
as a retry succeeds, the notifier returns to calling
check_once()
every success_interval
seconds.
With set_check_intervals()
out of the way, it’s time to
implement check_once()
. That’s where the core functionality of
a polling notifier happens. Taking care to follow the guidelines in the
note about IPv4 and IPv6 addressing above,
implement the logic to fetch the current IP address(es) and call
notify_ipv4()
and/or notify_ipv6()
.
For many polling notifiers, that will be the entire implementation. Since
setting success_interval
causes the checks to happen automatically, there’s
not usually a need to implement setup()
or
teardown()
. Nonetheless, they can be implemented if necessary.
For an example of this style of notifier, look at the sources for
ruddr.notifiers.web.WebNotifier
or
ruddr.notifiers.iface.IFaceNotifier
.
Event-Triggered Lookup Notifiers¶
This style of notifier is sort of a hybrid between the other two. It receives events when the current IP address may have changed, but still has to look up the IP address itself to find out what it is.
The strategy here is to implement the IP address lookup functionality in
check_once()
as with Polling Notifiers, but set
success_interval
to zero. That way, the retry logic is still there, and
on-demand notifying when Ruddr receives SIGUSR1 will work, but otherwise,
check_once()
will only run when the notifier triggers it by
calling check()
.
If you already read through the instructions for Polling Notifiers, the first part here is going to look pretty familiar.
Start by calling set_check_intervals()
in the constructor. That
will generally look something like this:
self.set_check_intervals(retry_min_interval=60,
retry_max_interval=86400,
success_interval=0,
config=config)
The important part there is that success_interval
is set to zero. That’s
what stops the notifier from automatically calling check_once()
except in the retry logic. (That being said, there is no reason a notifier
can’t have automatic polling and trigger extra checks itself, if that would
be useful. In fact, that’s what the systemd
notifier does.)
The retry_min_interval
and retry_max_interval
parameters control what
happens if there is an error in check_once()
(more
specifically, if it raises NotifyError
). Such an error triggers the
retry logic, which uses an exponential backoff. The first retry is after
retry_min_interval
seconds. If it fails again, each successive retry
interval is twice as long, maxing out at retry_max_interval
. Once the retry
interval reaches the max, it remains constant until the call succeeds.
The retry parameters directly passed in to the function act as defaults. If the
config dict contains keys matching the names retry_min_interval
or
retry_max_interval
, those take precedence.
Next, implement check_once()
. As mentioned, this is where the
logic to look up the current IP address should go. It should call
notify_ipv4()
and/or notify_ipv6()
with the
addresses it obtains. Make sure to follow the guidelines in the note
about IPv4 and IPv6 addressing.
That takes care of the IP address lookup part. Next is the events for changed
IP addresses. For that part, you will need to implement setup()
and teardown()
. As with the Event-Based Notifiers style
above, setup()
would typically involve setting up a thread to
listen on a socket, setting up a callback for some event, or something along
those lines, and teardown()
would do the opposite.
Finally, whenever your notifier becomes aware that the IP address may have
changed, call check()
. That will call
check_once()
, but will properly handle the retries for you.
For an example of this style of notifier, look at the sources for
ruddr.notifiers.systemd.SystemdNotifier
. (One caveat: That notifier
also uses polling, but setting success_interval=0
in the call to
set_check_intervals()
would disable that.)
Notifier Base Class¶
- class ruddr.Notifier(name, config)¶
A base class for notifiers. Supports a variety of notifier strategies, such as polling, event-based notifying, and hybrids. See the docs on writing your own notifier for more detail on what that means.
Also handles setting up a logger, setting some useful member variables for subclasses, and attaching to update functions.
- Parameters:
name (str) – Name of the notifier
config (Dict[str, str]) – Dict of config options for this notifier
- Raises:
ConfigError – if the configuration is invalid
- set_check_intervals(retry_min_interval=300, retry_max_interval=86400, success_interval=0, config=None)¶
Set the retry intervals for the
check_once()
function, and optionally setcheck_once()
to run periodically when successful.When
check_once()
fails (by raisingNotifyError
), the next invocation will be scheduled using an exponential backoff strategy, starting withretry_min_interval
seconds. Subsequent consecutive failures will be scheduled using successively longer intervals, until reaching the maximum failure interval,retry_max_interval
seconds.The
success_interval
parameter triggers some additional behavior:If
success_interval
is greater than zero, then whencheck_once()
succeeds, another invocation will be scheduled forsuccess_interval
seconds later. This is useful for notifiers that check the current address by polling, like theweb
notifier. Sincecheck()
runs at notifier startup, that meanssetup()
andteardown()
may not have to be implemented at all for these polling-style notifiers.If
success_interval
is zero,check_once()
will only be scheduled for future invocation for retries.
If the
config
parameter is provided, it will be checked for keysretry_min_interval
,retry_max_interval
, andinterval
. If either of theretry_*
keys are found, their values override the parameters passed into this function. If theinterval
key is found, its value overrides thesuccess_interval
parameter if and only if the parameter was already nonzero (preventing a configuration mistake from changing a non-polling notifier into a polling notifier).In practice, that means this function should be called using the notifier’s default values for
retry_min_interval
,retry_max_interval
, andsuccess_interval
(if it needs defaults other than the method defaults) and passing in the config to allow the user to override them.Subclasses should call this before returning from their constructor (if it needs to be called at all).
This has no effect if
check_once()
raisesNotImplementedError
.- Parameters:
retry_min_interval (int) – Minimum retry interval
retry_max_interval (int) – Maximum retry interval
success_interval (int) – Normal polling interval, or 0
config (Dict[str, str] | None) – Config dict for this updater
- Raises:
ConfigError – if the retry intervals are less than 1, the success interval is less than 0, or the values in the config cannot be converted to
int
- abstract setup()¶
Do any setup and start ongoing IP address notifications. Setup should be complete before this function returns (e.g. opening socket connections, etc.) but ongoing notifications should continue in the background, e.g. on a separate thread.
Should be overridden by subclasses if required.
- Raises:
NotifierSetupError – when there is a nonrecoverable error preventing notifier startup.
- abstract teardown()¶
Halt ongoing IP address notifications, do any teardown, and stop any non-daemon threads so Python may exit.
Should be overridden by subclasses if required.
When this is called, there will be no pending invocations of
check_once()
, and it’s guaranteed thatsetup()
is complete. Apart from that, it is up to the implementation to ensure that inconvenient timing won’t break any operations happening in background threads (e.g. that were started bysetup()
).This must not raise any exceptions (other than
NotImplementedError
if not implemented).
- abstract check_once()¶
Check the current IP address and do a notify, if possible.
Should be overridden by subclasses if supported.
Some notifiers do not support notifying on demand (for example, they get the current address from an event, thus they can only notify when such an event happens). For those updaters, this method should raise
NotImplementedError
when called (which is the default behavior when not overridden).For any notifier that does support obtaining the current IP address on demand, this function should do that immediately and notify using
notify_ipv4()
andnotify_ipv6()
. Some additional guidelines:Ruddr may not need both IPv4 and IPv6 addresses from this notifier. Call
want_ipv4()
andwant_ipv6()
to determine if either should be skipped.Even if Ruddr wants an address type, it may or may not be an error if it cannot be provided. If
need_ipv4()
returnsTrue
and an IPv4 address cannot be obtained, raiseNotifyError
(after notifying for the other address type, if necessary). The same goes forneed_ipv6()
and availability of an IPv6 prefix.
This is called at notifier startup, for on-demand notifies, and any time the notifier calls
check()
itself.- Raises:
NotifyError – if checking the current IP address failed (will trigger a retry after a delay)
NotImplementedError – if not supported by this notifier
- notify_ipv4(address)¶
Subclasses must call this to notify all the attached IPv4 updaters of a (possibly) new IPv4 address.
Subclasses may, but need not, call this if
want_ipv4()
is false.- Parameters:
address (IPv4Address) – The (possibly) new IPv4 address
- notify_ipv6(prefix)¶
Subclasses must call this to notify all the attached IPv6 updaters of a (possibly) new IPv6 prefix.
Subclasses may, but need not, call this if
want_ipv6()
is false.- Parameters:
prefix (IPv6Network) – The (possibly) new IPv6 network prefix
- want_ipv4()¶
Subclasses should call this to determine whether to check for current IPv4 addresses at all.
- Returns:
True
if so,False
if not- Return type:
bool
- want_ipv6()¶
Subclasses should call this to determine whether to check for current IPv6 addresses at all.
- Returns:
True
if so,False
if not- Return type:
bool
- need_ipv4()¶
Subclasses must call this to determine if a lack of IPv4 addressing is an error.
- Returns:
True
if so,False
if not- Return type:
bool
- need_ipv6()¶
Subclasses must call this to determine if a lack of IPv6 addressing is an error.
- Returns:
True
if so,False
if not- Return type:
bool
- abstract ipv4_ready()¶
Check if all configuration required for IPv4 notifying is present.
Subclasses must override if there is any configuration only required for IPv4.
- Returns:
True
if so,False
if not- Return type:
bool
- abstract ipv6_ready()¶
Check if all configuration required for IPv6 notifying is present.
Subclasses must override if there is any configuration only required for IPv6.
- Returns:
True
if so,False
if not- Return type:
bool
Writing Your Own Updater¶
An updater in Ruddr is, at its core, a class that provides two methods: one to update the IPv4 address and one to update the IPv6 address(es). That being said, those methods are actually responsible for quite a bit, such as detecting duplicate notifies, working with the addrfile, and retrying failed updates. Ruddr provides a few base classes that handle all those responsibilities and lay the groundwork for several common types of dynamic DNS provider APIs. Each of them provides certain abstract methods appropriate to the specific style of API they support.
To create an updater, create a class that inherits from one of those base classes, listed below, and implement its abstract methods as described under High-Level Updater Base Classes.
OneWayUpdater
A base class for providers with “one way” protocols. That is, protocols that allow domain updates but have no way to check the current address(es) assigned to domains. It obtains the current address using DNS lookups instead.
TwoWayUpdater
A base class for providers with “two way” protocols. That is, protocols that allow updating the address(es) at a domain name as well as querying the current address at a domain name. It’s best suited for providers whose API has no concept of zones (e.g. there’s no API calls related to zones, nor does any operation require a zone as a parameter).
TwoWayZoneUpdater
This is like
TwoWayUpdater
, except it’s well-suited for providers whose APIs do care about zones. For example, this is the base class to use if there is a way to query all records for a zone, or if domain updates require specifying the zone for the update.
Those three base classes should cover the vast majority of use cases. However,
if you need even more flexibility, you can inherit directly from the low-level
Updater
base class instead, or the most primitive, the
BaseUpdater
base class. These are described under Low-Level Updater Base Classes.
A few additional guidelines and tips:
All updaters must have a constructor that matches the following:
- Updater.__init__(name, addrfile, config)
- Parameters:
name (str) – Updater name, taken from
[updater.<name>]
in the Ruddr configaddrfile (ruddr.Addrfile) – The
Addrfile
object this updater should useconfig (Dict[str, str]) – A
dict
of this updater’s configuration values, all as strings, plus any global configuration options that may be useful (currently, onlydatadir
, which updaters may use to cache data if necessary).
The first two parameters (
name
andaddrfile
) can be passed directly to the super class constructor, and it’s strongly recommended that that be the first thing your constructor does (so the variables in the next bullet point will be initialized).The
BaseUpdater
class, which is a superclass of all updaters (directly or indirectly), makes theself.name
andself.log
member variables available.self.name
is astr
with the updater name andself.log
is a Python logger named after the updater. You may use either of these variables whenever convenient, but you are especially encouraged to useself.log
often.
High-Level Updater Base Classes¶
- class ruddr.OneWayUpdater(name, addrfile)¶
Base class for updaters supporting protocols that are one-way, that is, the API has no way to obtain the current address for a host. Ruddr requires the current address for IPv6 updates since it only updates the prefix. This class handles that requirement either by using hardcoded IPv6 addresses or by looking up the current IPv6 address in DNS.
The update process for this type of updater works as follows:
The list of hosts to be updated is fetched from config.
For an IPv4 update,
publish_ipv4_one_host()
is called for each host, and the process is done.For an IPv6 update, the process continues with the next steps.
For IPv6, Ruddr obtains a “current” address for each host (“current” in quotes because only the host portion of the address actually matters, since that’s the part it needs to reuse).
If an address for the host is hardcoded in config, it uses that.
If an FQDN was provided for the host, it looks up that domain name in DNS, optionally at a specific nameserver.
Otherwise, if neither was given, it skips this host for IPv6 updates.
Ruddr takes the host portion from each address in step 2 and combines it with the new prefix from the notifier to get the new address, then calls
publish_ipv6_one_host()
on each one.
- Parameters:
name – Name of the updater (from config section heading)
addrfile – The
Addrfile
object
- init_params(hosts, nameserver=None, min_retry=300)¶
Initialize the hosts list, nameserver, and min retry interval.
This is separate from
__init__()
so subclasses can rely on the logger while doing their config parsing, then pass the relevant config options in by calling this method. It must be called before your subclass’s constructor completes.- Parameters:
hosts (List[Tuple[str, IPv6Address | str | None]] | str) – A list of 2-tuples
(hostname, ipv6_src)
specifying which hosts should be updated and where their IPv6 addresses should come from.ipv6_src
can be anIPv6Address
to hardcode the host portion of the address, astr
containing an FQDN to look up in DNS, orNone
if this host should not get IPv6 updates at all. Alternatively, this entire parameter may be in unparsed string form—see the docs for thestandard
updater for the expected format.nameserver (str | None) – The nameserver to use to look up AAAA records for the FQDNs, if any. If
None
, system DNS is used.min_retry – The minimum retry interval after failed updates, in seconds. (There is an exponential backoff for subsequent retries.)
- abstract publish_ipv4_one_host(hostname, address)¶
Attempt to publish an IPv4 address for the given host.
Must be implemented by subclasses.
- Parameters:
hostname (str) – The host to publish for
address (IPv4Address) – The address to publish
- Raises:
PublishError – if publishing fails (will automatically retry after a delay)
FatalPublishError – if publishing fails in a non-recoverable way (all future publishing will halt)
- abstract publish_ipv6_one_host(hostname, address)¶
Attempt to publish an IPv6 address for the given host.
Must be implemented by subclasses.
- Parameters:
hostname (str) – The host to publish for
address (IPv6Address) – The address to publish
- Raises:
PublishError – if publishing fails (will automatically retry after a delay)
FatalPublishError – if publishing fails in a non-recoverable way (all future publishing will halt)
- class ruddr.TwoWayUpdater(name, addrfile, datadir)¶
Base class for updaters supporting protocols that are two-way and not zone-based, that is:
The API supports fetching the current address(es) for hosts, either individually or by fetching all domains in the account
The API has no concept of zones, meaning there are not zone-related API calls, nor is the zone required as a parameter for any other operation
It’s meant to be flexible enough for a variety of API styles. For example, most APIs will allow fetching and updating individual domains, but some may only provide a way to fetch or update all domains in the account at once. Still others may be a hybrid, requiring you to fetch all domains in the account but update domains individually. This class supports all of the above by allowing only the appropriate methods to be implemented.
The update process for this type of updater works as follows:
Fetch A/AAAA records
First try
fetch_all_ipv4s()
/fetch_all_ipv6s()
to fetch records for the entire account at once.If that’s not implemented, fetch records for each host using
fetch_domain_ipv4s()
/fetch_domain_ipv6s()
.
Create replacement records for the hosts to be updated. If there is not an existing record for a host, it’s a
PublishError
. If there are multiple existing records, they are replaced with a single one for IPv4, and they are all updated for IPv6. Other records (if there are any in the account) are left untouched.Write the A/AAAA records
Try
put_all_ipv4s()
/put_all_ipv6s()
to write all records at once, if implemented. (This is only tried iffetch_all_ipv4s()
/fetch_all_ipv6s()
was implemented.)Otherwise, use
put_domain_ipv4()
/put_domain_ipv6s()
to write each host’s records.
- Parameters:
name (str) – Name of the updater (from config section heading)
addrfile (Addrfile) – The
Addrfile
objectdatadir (str) – The configured data directory
- init_hosts(hosts)¶
Provide the list of hosts to be updated.
This is separate from
__init__()
so subclasses can rely on the logger while doing their config parsing, then pass the list of hosts in via this method after. It must be called before your subclass’s constructor completes.The list can be provided either as an unparsed
str
with a whitespace-separated list of domain names or as an actuallist
of domain names.- Parameters:
hosts (List[str] | str) – The list of hosts to be updated
- Raises:
ConfigError – if there is a duplicate
- abstract fetch_all_ipv4s()¶
Get a list of all A (IPv4) records in the account.
Implementing this method in subclasses is optional. If not implemented, then
fetch_domain_ipv4s()
andput_domain_ipv4()
must be implemented.If implemented, this function should return a list of A (IPv4) records in the form
(domain, addr, ttl)
wheredomain
is the domain name for the record,addr
is anIPv4Address
, andttl
is the TTL of the record.The
ttl
may be set toNone
if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.If there are multiple records/IPv4s for a single domain, return them as separate list items with the same
domain
. Note that if the domain needs to be updated by Ruddr, it will only produce a single record to replace them.- Returns:
A list of A records in the format described
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure, or the zone does not exist
- Return type:
List[Tuple[str, IPv4Address, int | None]]
- abstract fetch_all_ipv6s()¶
Get a list of all AAAA (IPv6) records in the account.
Implementing this method in subclasses is optional. If not implemented, then
fetch_domain_ipv6s()
andput_domain_ipv6s()
must be implemented.If implemented, this function should return a list of AAAA (IPv6) records in the form
(domain, addr, ttl)
wheredomain
is the domain name for the record,addr
is anIPv6Address
, andttl
is the TTL of the record.The
ttl
may be set toNone
if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.If there are multiple records/IPv6s for a single domain, return them as separate list items with the same
domain
. If the domain needs to be updated by Ruddr, it will update all of them.- Returns:
A list of AAAA records in the format described
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure, or the zone does not exist
- Return type:
List[Tuple[str, IPv6Address, int | None]]
- abstract fetch_domain_ipv4s(domain)¶
Get a list of A (IPv4) records for the given domain.
Implementing this method in subclasses is optional. It only needs to be implemented if
fetch_all_ipv4s()
is not implemented.This function should return a list of A (IPv4) records for the given domain. The return value is a list of tuples
(addr, ttl)
whereaddr
is anIPv4Address
andttl
is the TTL of the record.The
ttl
may be set toNone
if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.The return value is a list in case there is more than one A record associated with the domain; however, note that Ruddr will want to replace all of them with a single record.
- Parameters:
domain (str) – The domain to fetch records for
- Returns:
A list of A records in the format described
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure, or no such record exists
- Return type:
List[Tuple[IPv4Address, int | None]]
- abstract fetch_domain_ipv6s(domain)¶
Get a list of AAAA (IPv6) records for the given domain.
Implementing this method in subclasses is optional. It only needs to be implemented if
fetch_all_ipv6s()
is not implemented.This function should return a list of AAAA (IPv6) records for the given domain. The return value is a list of tuples
(addr, ttl)
whereaddr
is anIPv6Address
andttl
is the TTL of the record.The
ttl
may be set toNone
if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.The return value is a list in case there is more than one AAAA record associated with the domain. Ruddr will update all of them.
- Parameters:
domain (str) – The domain to fetch records for
- Returns:
A list of AAAA records in the format described
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure, or no such record exists
- Return type:
List[Tuple[IPv6Address, int | None]]
- abstract put_all_ipv4s(records)¶
Publish A (IPv4) records for the account.
Implementing this method in subclasses is optional. However, either this function or
put_domain_ipv4()
must be implemented. The latter must be implemented iffetch_all_ipv4s()
is not implemented.If implemented, this function should replace all the A (IPv4) records in the account with the records provided. The records are provided as a
dict
where the keys are the domain names and the values are 2-tuples(addrs, ttl)
whereaddrs
is a list ofIPv4Address
andttl
is anint
(orNone
if thefetch_all_ipv4s()
function didn’t provide any).Records that Ruddr is not configured to update will be passed through from
fetch_all_ipv4s()
unmodified.- Parameters:
records (Dict[str, Tuple[List[IPv4Address], int | None]]) – The records to publish
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure
- abstract put_all_ipv6s(records)¶
Publish AAAA (IPv6) records for the account.
Implementing this method in subclasses is optional. However, either this function or
put_domain_ipv6s()
must be implemented. The latter must be implemented iffetch_all_ipv6s()
is not implemented.If implemented, this function should replace all the AAAA (IPv6) records in the account with the records provided. The records are provided as a
dict
where the keys are the domain names and the values are 2-tuples(addrs, ttl)
whereaddrs
is a list ofIPv6Address
andttl
is anint
(orNone
if thefetch_all_ipv6s()
function didn’t provide any).Records that Ruddr is not configured to update will be passed through from
fetch_all_ipv6s()
unmodified.- Parameters:
records (Dict[str, Tuple[List[IPv6Address], int | None]]) – The records to publish
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure
- abstract put_domain_ipv4(domain, address, ttl)¶
Publish an A (IPv4) record for the given domain.
Implementing this method in subclasses is optional. However, it must be implemented if either
fetch_all_ipv4s()
orput_all_ipv4s()
are not implemented.This function should replace the A (IPv4) records for the given domain with a single A record matching the given parameters.
This will only be called for the domains Ruddr is configured to update.
- Parameters:
domain (str) – The domain to publish the record for
address (IPv4Address) – The address for the new record
ttl (int | None) – The TTL for the new record (or
None
if thefetch_*_ipv4s
functions didn’t provide any). Ruddr passes this through unchanged.
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure
- abstract put_domain_ipv6s(domain, addresses, ttl)¶
Publish AAAA (IPv6) records for the given domain.
Implementing this method in subclasses is optional. However, it must be implemented if either
fetch_all_ipv6s()
orput_all_ipv6s()
are not implemented.**This function should replace the AAAA (IPv6) records for the given domain with the records provided.
This will only be called for the domains Ruddr is configured to update.
- Parameters:
domain (str) – The domain to publish the records for
addresses (List[IPv6Address]) – The address for the new records
ttl (int | None) – The TTL for the new records (or
None
if thefetch_*_ipv6s
functions didn’t provide any). Ruddr passes this through unchanged.
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure
- class ruddr.TwoWayZoneUpdater(name, addrfile, datadir)¶
Base class for updaters supporting protocols that are two-way and zone-based, that is:
The API supports fetching the current address(es) for hosts, either individually or by fetching whole zones
The API involves zones in some way, e.g. entire zones can be fetched or updated at once, or fetching/updating a single domain requires specifying its zone
It’s meant to be flexible enough for a variety of API styles. For example, some APIs may be very flexible, allowing individual domains’ records to be fetched and updated. Others may be strictly zone-based, only providing APIs to fetch and replace entire zones. Still others may be a hybrid, with a way to fetch an entire zone but only update single domains. This class supports all of the above by allowing only the appropriate methods to be implemented.
The update process for this type of updater works as follows:
The list of hosts is organized into zones:
Hosts with hardcoded zone in the config are placed in that zone
If there are any hosts remaining, first try
get_zones()
to get a list of zones, and assign zones from that. If there are still any hosts that don’t fit into zones, it’s aPublishError
.If
get_zones()
is not implemented, any hosts without hardcoded zones are assigned to zones using the public suffix list.
For each zone:
Fetch A/AAAA records for the zone
First try
fetch_zone_ipv4s()
/fetch_zone_ipv6s()
to fetch records for the entire zone at once.If that’s not implemented, fetch records for each host using
fetch_subdomain_ipv4s()
/fetch_subdomain_ipv6s()
.
Create replacement records for the hosts to be updated. If there is not an existing record for a host, it’s a
PublishError
. If there are multiple existing records, they are replaced with a single one for IPv4, and they are all updated for IPv6. Other records are left untouched.Write the A/AAAA records for the zone
Try
put_zone_ipv4s()
/put_zone_ipv6s()
to write the entire zone at once, if implemented. (This is only tried iffetch_zone_ipv4s()
/fetch_zone_ipv6s()
was implemented.)Otherwise, use
put_subdomain_ipv4()
/put_subdomain_ipv6s()
to write each host’s records.
- Parameters:
name (str) – Name of the updater (from config section heading)
addrfile (Addrfile) – The
Addrfile
objectdatadir (str) – The configured data directory
- init_hosts_and_zones(hosts)¶
Provide the list of hosts to be updated, optionally with their zones if configured.
This is separate from
__init__()
so subclasses can rely on the logger while doing their config parsing, then pass the list of hosts in via this method after. It must be called before your subclass’s constructor completes.The list can be provided either as an unparsed
str
or as a list of 2-tuples(fqdn, zone)
:When provided as an unparsed
str
, it should be a whitespace-separated list whose items are in the formatfoo.example.com
orfoo.example.com/example.com
(the latter format explicitly setting the zone)When provided as a list of 2-tuples,
fqdn
is the FQDN for the host andzone
is eitherNone
or astr
explicitly specifying the zone for this host.
For hosts without a zone explicitly specified (which can be all of them), Ruddr will use
get_zones()
to determine the zone, or the public suffix list ifget_zones()
is not implemented.- Parameters:
hosts (List[Tuple[str, str | None]] | str) – The list of hosts to be updated
- Raises:
ConfigError – if an FQDN does not reside in the zone provided with it, or is a duplicate
- abstract get_zones()¶
Get a list of zones under the account.
Implementing this method in subclasses is optional.
If implemented, this function should return a list of zones (more specifically, the domain name for each zone). The FQDNs-to-be-updated will be compared against the zone list. This serves two purposes:
It allows better error checking. If any of the FQDNs do not fall into one of the available zones, Ruddr can catch that and log it for the user.
If any of the zones are not immediate subdomains of a public suffix (public suffix being .com, .co.uk, etc., see public suffix list), for example, myzone.provider.com, this allows Ruddr to get the correct zone without it being manually configured.
If not implemented, Ruddr uses the public suffix list to assign zones to any FQDNs without explicitly-configured zones.
- Returns:
A list of zones
- Raises:
NotImplementedError – if not implemented
PublishError – if fetching the zones is implemented, but failed
- Return type:
List[str]
- abstract fetch_zone_ipv4s(zone)¶
Get a list of A (IPv4) records for the given zone.
Implementing this method in subclasses is optional. If not implemented, then
fetch_subdomain_ipv4s()
andput_subdomain_ipv4()
must be implemented.If implemented, this function should return a list of A (IPv4) records in the given zone in the form
(name, addr, ttl)
wherename
is the subdomain portion (e.g. a record for “foo.bar.example.com” in zone “example.com” should return “foo.bar” as the name),addr
is anIPv4Address
, andttl
is the TTL of the record.Some notes:
name
should be empty for the root domain in the zoneThe
subdomain_of()
function may be helpful for thename
element if the provider’s API returns FQDNsThe
ttl
may be set toNone
if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.If there are multiple records/IPv4s for a single subdomain, return them as separate list items with the same
name
. Note that if the subdomain needs to be updated by Ruddr, it will only produce a single record to replace them.
- Parameters:
zone (str) – The zone to fetch records for
- Returns:
A list of A records in the format described
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure, or the zone does not exist
- Return type:
List[Tuple[str, IPv4Address, int | None]]
- abstract fetch_zone_ipv6s(zone)¶
Get a list of AAAA (IPv6) records for the given zone.
Implementing this method in subclasses is optional. If not implemented, then
fetch_subdomain_ipv6s()
andput_subdomain_ipv6s()
must be implemented.If implemented, this function should return a list of AAAA (IPv6) records in the given zone in the form
(name, addr, ttl)
wherename
is the subdomain portion (e.g. a record for “foo.bar.example.com” in zone “example.com” should return “foo.bar” as the name),addr
is anIPv6Address
, andttl
is the TTL of the record.Some notes:
name
should be empty for the root domain in the zoneThe
subdomain_of()
function may be helpful for thename
element if the provider’s API returns FQDNsThe
ttl
may be set toNone
if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.If there are multiple records/IPv6s for a single subdomain, return them as separate list items with the same
name
. If the subdomain needs to be updated by Ruddr, it will update all of them.
- Parameters:
zone (str) – The zone to fetch records for
- Returns:
A list of AAAA records in the format described
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure, or the zone does not exist
- Return type:
List[Tuple[str, IPv6Address, int | None]]
- abstract fetch_subdomain_ipv4s(subdomain, zone)¶
Get a list of A (IPv4) records for the given domain.
Implementing this method in subclasses is optional. It only needs to be implemented if
fetch_zone_ipv4s()
is not implemented.This function should return a list of A (IPv4) records for the given domain. If this provider’s API requires using the original FQDN (rather than separate subdomain and zone fields), use
fqdn_of()
on the parameters to obtain it.The return value is a list of tuples
(addr, ttl)
whereaddr
is anIPv4Address
andttl
is the TTL of the record. As withfetch_zone_ipv4s()
:The
ttl
may be set toNone
if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.The return value is a list in case there is more than one A record associated with the domain; however, note that Ruddr will want to replace all of them with a single record.
- Parameters:
subdomain (str) – The subdomain to fetch records for (only the subdomain portion), empty for the root domain of the zone
zone (str) – The zone the subdomain belongs to
- Returns:
A list of A records in the format described
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure, or no such record exists
- Return type:
List[Tuple[IPv4Address, int | None]]
- abstract fetch_subdomain_ipv6s(subdomain, zone)¶
Get a list of AAAA (IPv6) records for the given domain.
Implementing this method in subclasses is optional. It only needs to be implemented if
fetch_zone_ipv6s()
is not implemented.This function should return a list of AAAA (IPv6) records for the given domain. If this provider’s API requires using the original FQDN (rather than separate subdomain and zone fields), use
fqdn_of()
on the parameters to obtain it.The return value is a list of tuples
(addr, ttl)
whereaddr
is anIPv6Address
andttl
is the TTL of the record. As withfetch_zone_ipv6s()
:The
ttl
may be set toNone
if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.The return value is a list in case there is more than one AAAA record associated with the domain. Ruddr will update all of them.
- Parameters:
subdomain (str) – The subdomain to fetch records for (only the subdomain portion), empty for the root domain of the zone
zone (str) – The zone the subdomain belongs to
- Returns:
A list of AAAA records in the format described
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure, or no such record exists
- Return type:
List[Tuple[IPv6Address, int | None]]
- abstract put_zone_ipv4s(zone, records)¶
Publish A (IPv4) records for the given zone.
Implementing this method in subclasses is optional. However, either this function or
put_subdomain_ipv4()
must be implemented. The latter must be implemented iffetch_zone_ipv4s()
is not implemented.If implemented, this function should replace all the A records for the given zone with the records provided. The records are provided as a
dict
where the keys are the subdomain names and the values are 2-tuples(addrs, ttl)
whereaddrs
is a list ofIPv4Address
andttl
is anint
(orNone
if thefetch_zone_ipv4s()
function didn’t provide any).Records that Ruddr is not configured to update will be passed through from
fetch_zone_ipv4s()
unmodified.- Parameters:
zone (str) – The zone to publish records for
records (Dict[str, Tuple[List[IPv4Address], int | None]]) – The records to publish
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure
- abstract put_zone_ipv6s(zone, records)¶
Publish AAAA (IPv6) records for the given zone.
Implementing this method in subclasses is optional. However, either this function or
put_subdomain_ipv6s()
must be implemented. The latter must be implemented iffetch_zone_ipv6s()
is not implemented.If implemented, this function should replace all the AAAA records for the given zone with the records provided. The records are provided as a
dict
where the keys are the subdomain names and the values are 2-tuples(addrs, ttl)
whereaddrs
is a list ofIPv6Address
andttl
is anint
(orNone
if thefetch_zone_ipv6s()
function didn’t provide any).Records that Ruddr is not configured to update will be passed through from
fetch_zone_ipv6s()
unmodified.- Parameters:
zone (str) – The zone to publish records for
records (Dict[str, Tuple[List[IPv6Address], int | None]]) – The records to publish
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure
- abstract put_subdomain_ipv4(subdomain, zone, address, ttl)¶
Publish an A (IPv4) record for the given domain.
Implementing this method in subclasses is optional. However, it must be implemented if either
fetch_zone_ipv4s()
orput_zone_ipv4s()
are not implemented.This function should replace the A records for the given domain with a single A record matching the given parameters. If this provider’s API requires using the original FQDN (rather than separate subdomain and zone fields), use
fqdn_of()
on the parameters to obtain it.This will only be called for the domains Ruddr is configured to update.
- Parameters:
subdomain (str) – The subdomain to publish the record for (only the subdomain portion), empty for the root domain of the zone
zone (str) – The zone the subdomain belongs to
address (IPv4Address) – The address for the new record
ttl (int | None) – The TTL for the new record (or
None
if thefetch_*_ipv4s
functions didn’t provide any). Ruddr passes this through unchanged.
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure
- abstract put_subdomain_ipv6s(subdomain, zone, addresses, ttl)¶
Publish AAAA (IPv6) records for the given domain.
Implementing this method in subclasses is optional. However, it must be implemented if either
fetch_zone_ipv6s()
orput_zone_ipv6s()
are not implemented.This function should replace the AAAA records for the given domain with the records provided. If this provider’s API requires using the original FQDN (rather than separate subdomain and zone fields), use
fqdn_of()
on the parameters to obtain it.This will only be called for the domains Ruddr is configured to update.
- Parameters:
subdomain (str) – The subdomain to publish the records for (only the subdomain portion), empty for the root domain of the zone
zone (str) – The zone the subdomain belongs to
addresses (List[IPv6Address]) – The addresses for the new records
ttl (int | None) – The TTL for the new records (or
None
if thefetch_*_ipv6s
functions didn’t provide any). Ruddr passes this through unchanged.
- Raises:
NotImplementedError – if not implemented
PublishError – if implemented, but there is a failure
- static subdomain_of(fqdn, zone)¶
Return the subdomain portion of the given FQDN.
- Parameters:
fqdn (str) – The FQDN to get the subdomain of (without trailing dot)
zone (str) – The zone this FQDN belongs to (empty for root zone)
- Returns:
The subdomain portion, e.g. “foo.bar” for FQDN “foo.bar.example.com” with zone “example.com”, or the empty string if the FQDN is the zone’s root domain
- Raises:
ValueError – if the FQDN is not in the given zone
- Return type:
str
- static fqdn_of(subdomain, zone)¶
Return an FQDN for the given subdomain in the given zone.
- Parameters:
subdomain (str) – The subdomain to return an FQDN for, or the empty string for the zone’s root domain
zone (str) – The zone the subdomain resides in (empty for root zone)
- Returns:
An FQDN, without trailing dot
- Return type:
str
Low-Level Updater Base Classes¶
- class ruddr.Updater(name, addrfile)¶
Base class for Ruddr updaters. Handles setting up logging, retries, the initial update, and working with the addrfile.
- Parameters:
name (str) – Name of the updater (from config section heading)
addrfile (Addrfile) – The
Addrfile
object
- publish_ipv4(address)¶
Publish a new IPv4 address to the appropriate DDNS provider. Will only be called if an update contains a new address or a previous update failed.
Must be implemented by subclasses if they support IPv4 updates.
- Parameters:
address (IPv4Address) –
IPv4Address
to publish- Raises:
PublishError – when publishing fails (will retry automatically after a delay)
- publish_ipv6(network)¶
Publish a new IPv6 prefix to the appropriate DDNS provider. Will only be called if an update contains a new address or a previous update failed.
Must be implemented by subclasses if they support IPv6 updates.
- Parameters:
network (IPv6Network) –
IPv6Network
with the prefix to publish- Raises:
PublishError – when publishing fails (will retry automatically after a delay)
- static replace_ipv6_prefix(network, address)¶
Replace the prefix portion of the given IPv6 address with the network prefix provided and return the result
- Parameters:
network (IPv6Network) – The network prefix to set
address (IPv6Address) – The address to set the network prefix on
- Returns:
The modified address
- Return type:
IPv6Address
- class ruddr.BaseUpdater(name, addrfile)¶
Skeletal superclass for
Updater
. It sets up the logger, sets up some useful member variables, and little else. Custom updaters can opt to override this instead if the default logic in Updater does not suit their needs (e.g. if the protocol requires IPv4 and IPv6 updates to be sent simultaneously, custom retry logic, etc.).- Parameters:
name (str) – Name of the updater (from config section heading)
addrfile (Addrfile) – The
Addrfile
object
- name: str¶
Updater name (from config section heading)
- addrfile: Addrfile¶
Addrfile for avoiding duplicate updates
- min_retry_interval: int¶
Minimum retry interval (some providers may require a minimum delay when there are server errors, in which case, subclasses can modify this)
- halt: bool¶
@Retry
will set this toTrue
when there has been a fatal error and no more updates should be issued.
- initial_update(ipv4_attached, ipv6_attached)¶
Do the initial update: Check the addrfile, and if either address is defunct but has a last-attempted-address, try to publish it again.
- Parameters:
ipv4_attached (bool) – Whether this updater should do an initial update for IPv4 (that is, whether it is attached to a notifier for IPv4)
ipv6_attached (bool) – Whether this updater should do an initial update for IPv6 (that is, whether it is attached to a notifier for IPv6)
- update_ipv4(address)¶
Receive a new IPv4 address from the attached notifier. If it does not match the current address, call the subclass’ publish function, update the addrfile if successful, and retry if not.
- Parameters:
address (IPv4Address) –
IPv4Address
to update with
- update_ipv6(address)¶
Receive a new IPv6 prefix from the attached notifier. If it does not match the current prefix, call the subclass’ publish function, update the addrfile if successful, and retry if not.
- Parameters:
address (IPv6Network) –
IPv6Network
to update with
- static replace_ipv6_prefix(network, address)¶
Replace the prefix portion of the given IPv6 address with the network prefix provided and return the result
- Parameters:
network (IPv6Network) – The network prefix to set
address (IPv6Address) – The address to set the network prefix on
- Returns:
The modified address
- Return type:
IPv6Address
- static pick_error(curr_err, new_err)¶
Return the current error, unless there isn’t one or new_err is a higher priority error
Using your Custom Updater or Notifier¶
Once you have a custom updater or notifier class, there are two ways to start using it.
The first way is by module name and class name. For this to work, the
module containing your updater/notifier class must be in the module search
path. Typically, this means you’ll either have to install it or make sure the
PYTHONPATH
environment variable includes the path to your module when Ruddr
is run. Then, you can use the module
and type
options in your updater
or notifier config, and Ruddr will import and use it.
For example, suppose you have an updater class MyUpdater
in a file named
myupdater.py
. Assuming that Python file is in some directory in your
PYTHONPATH
, you can use an updater configuration like this:
[updater.main]
module = myupdater
type = MyUpdater
# ...
The second way to start using it is by creating a Python package with a
ruddr.updater
or ruddr.notifier
entry point. This requires slightly
more work upfront (you have to create a pyproject.toml
), but has the
advantage that it becomes very easy to publish your updater or notifier to PyPI
for others to use, if you so choose. If you want to go this route, you can
follow these steps:
Set up an empty directory for your package and put the module containing your updater or notifier inside. In the simplest case, the module may be a single
.py
file, but it can be a package with submodules, etc. For demonstration, we will assume you have a notifier in a single-file Python module,mynotifier.py
, and the notifier class inside isMyNotifier
.If you intend to share your updater or notifier, e.g. on PyPI, GitHub, or otherwise, you may want to create a
README.md
in the same directory.Create a file
pyproject.toml
in the directory with contents similar to this:[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] # This becomes the package name on PyPI, if you choose to publish it name = "ruddr_notifier_mynotifier" version = "0.0.1" authors = [ { name="Your Name", email="your_email@example.com" }, ] description = "My Ruddr Notifier" # Uncomment the next line if you created a README #readme = "README.md" requires-python = ">=3.7" classifiers = [ "Programming Language :: Python :: 3", ] [project.entry-points."ruddr.notifier"] my_notifier = "mynotifier:MyNotifier"
Be sure to set the name, version, authors, and description as appropriate, but that last section is the important part. In that example, an entry point named
my_notifier
is created in theruddr.notifier
group, and it points to theMyNotifier
class in themynotifier
module.You now have an installable Python package. Use
pip install -U .
to install it from the current directory. (If you wish to make it public, you can also publish it to PyPI and install it by name.)Once installed, the entry point name,
my_notifier
in the example above, can be used as the notifiertype
in your Ruddr config. For example:[notifier.main] type = my_notifier # ...
Using Ruddr as a Library¶
Ruddr’s primary use case is as a standalone service, but it can be integrated into other Python programs as a library as well. The steps boil down to this:
First, create an instance of
Config
. It can be created directly, or you may useread_config()
orread_config_from_path()
.Use the
Config
to create aDDNSManager
.Call
start()
on theDDNSManager
you created. This will return once Ruddr finishes starting. Ruddr runs in background non-daemon threads (“non-daemon” meaning that your program will not end until they are stopped as described in the next step).When ready for Ruddr to stop, call
stop()
on your :class`DDNSManager` object. Ruddr will halt the background threads gracefully.
An immediate update (if possible) can be triggered on a started
DDNSManager
by calling its do_notify()
method.
This is not always possible if the configured notifiers do not support it,
though most do.
See the next section for the APIs involved.
Warning
The config file reader functions can throw ConfigError
. The
DDNSManager
constructor can raise ConfigError
and its
start()
function can raise NotifierSetupError
. Be
ready to handle those exceptions. Both of them can be caught under
RuddrSetupError
.
Manager and Config API¶
- ruddr.read_config(configfile)¶
Read configuration in from the given file-like object opened in text mode
- Parameters:
configfile (Iterable[str]) – Filelike object to read the config from
- Raises:
ConfigError – if the config file cannot be read or is invalid
- Returns:
A
Config
ready to be passed toDDNSManager
- Return type:
- ruddr.read_config_from_path(filename)¶
Read configuration from the named file or
Path
- Parameters:
filename (str | Path) – Filename or path to read from
- Raises:
ConfigError – if the config file cannot be read or is invalid
- Returns:
A
Config
ready to be passed toDDNSManager
- Return type:
- class ruddr.Config(main, notifiers, updaters)¶
Contains all Ruddr configuration required by
DDNSManager
. Normally, this would be created from a configuration file byread_config()
orread_config_from_path()
, but it can also be created directly when using Ruddr as a library.Note that all configuration values should be strings, as they would be from Python’s
configparser.ConfigParser
.The configuration must be finalized before use, but programs using Ruddr as a library need not concern themselves with that.
DDNSManager
will do that itself.- Parameters:
main (Dict[str, str]) – A dictionary of global configuration options, that is, the options that go under
[ruddr]
in the configuration file.notifiers (Dict[str, Dict[str, str]]) – A dictionary of notifier configurations. Keys are notifier names (i.e. the
XYZ
part of[notifier.XYZ]
if it were from a configuration file) and values are themselves dicts of config options for that notifier.updaters (Dict[str, Dict[str, str]]) – A dictionary of updater configurations. Keys are updater names (i.e. the
XYZ
part of[updater.XYZ]
if it were from a configuration file) and values are themselves dicts of config options for that updater.
- finalize(validate_notifier_type, validate_updater_type)¶
Used by
DDNSManager
to finalize the configuration. This consists of validating the updater and notifier types, filling default values, and doing some normalization.Programs using Ruddr as a library need not call this function themselves;
DDNSManager
will handle it.- Parameters:
validate_notifier_type (Callable[[str | None, str], bool]) – A callable to check if a notifier type is valid. First parameter is a module name, or
None
if it’s a built-in notifier type. Second parameter is a class name or built-in notifier type name.validate_updater_type (Callable[[str | None, str], bool]) – A callable to check if an updater type is valid. Parameters are the same as above.
- Raises:
ConfigError – if the configuration is invalid
- class ruddr.DDNSManager(config)¶
Manages the rest of the Ruddr system. Creates notifiers and updaters and manages the addrfile.
- Parameters:
- Raises:
ConfigError – if configuration is not valid
- start()¶
Start running all notifiers. Returns after they start. The notifiers will continue running in background threads.
- Raises:
NotifierSetupError – if a notifier fails to start
- do_notify()¶
Do an on-demand notify from all notifiers.
Not all notifiers will support this, but most will.
Does not raise any exceptions.
- stop()¶
Stop all running notifiers gracefully. This will allow Python to exit naturally.
Does not raise any exceptions, even if not yet started.
Ruddr Exceptions¶
Below is a summary of all the exceptions that can be raised by Ruddr or in custom notifiers and updaters. Note that the rest of the API documentation on this page describes more precisely when particular exceptions might be raised and when it’s appropriate for subclasses to raise them.
- exception ruddr.RuddrException¶
Bases:
Exception
Base class for all Ruddr exceptions
- exception ruddr.RuddrSetupError¶
Bases:
RuddrException
Base class for Ruddr exceptions that happen during startup
- exception ruddr.ConfigError¶
Bases:
RuddrSetupError
Raised when the configuration is malformed or has other errors
- exception ruddr.NotifierSetupError¶
Bases:
RuddrSetupError
Raised by a notifier when there is a fatal error during setup for persistent checks
- exception ruddr.NotStartedError¶
Bases:
RuddrException
Raised when requesting an on-demand notify before starting the notifier
- exception ruddr.NotifyError¶
Bases:
RuddrException
Notifiers should raise when an attempt to check the current IP address fails. Doing so triggers the retry mechanism.
- exception ruddr.PublishError¶
Bases:
RuddrException
Updaters should raise when an attempt to publish an update fails. Doing so triggers the retry mechanism.
- exception ruddr.FatalPublishError¶
Bases:
PublishError
Updaters should raise when an attempt to publish an update fails in a non-recoverable way. Doing so causes the updater to halt until the next time Ruddr is started.
Development on Ruddr Itself¶
Everything discussed so far on this page has been about development that ties into Ruddr. This section is for development on Ruddr itself, for example fixing bugs or adding new features.
Installation for Development¶
The latest sources for Ruddr are available on GitHub. Once you have cloned the
repo, the easiest way to work on development is to optionally set up a virtual
environment, then install directly out of the repo with the dev
extra.
Assuming you have a shell open in the repo:
# Optionally, set up a virtual environment
python3 -m venv venv
. venv/bin/activate
# Install in develop mode with the "dev" extra
pip install -U -e .[dev]
The dev
extra includes everything required to check style, check types,
run unit tests, and regenerate the documentation.
Running Tests¶
Ruddr’s full set of checks and tests can be run with tox. It includes style
checks and linting, type checking, and unit tests with coverage. If you
installed with the dev
extra above, you have everything you need.
(Alternatively, the test
extra includes just the testing tools from the
dev
extra.)
To run the full test suite, make sure your virtual environment is active (if
you are using one) and run the tox
command:
# Skip this line if not using a virtual environment
. venv/bin/activate
tox
This will first run flake8
and pytype
. Then it will run pytest
with
coverage on each supported version of Python. Lastly, it will generate the
coverage report in the terminal and write it to htmlcov/index.html.
These tools can also be run individually:
flake8 src/ test/
pytype
pytest --cov
Generating Docs¶
The documentation is available online at https://ruddr.dcpx.org/, but if you
would like to generate a local copy (for reference or to preview changes),
install the docs
extra (the dev
extra includes the docs
extra) and
build the docs in docs/
as usual for Sphinx:
# Assuming you are in the git repo:
pip install .[docs]
cd docs
make html
Open docs/_build/html/index.html
to read them.
You can also generate other formats with make <format>
, provided the
necessary tools are available (e.g. make latexpdf
requires a LaTeX
distribution to be installed). The output will be in docs/_build/<format>/
.
Contributions¶
If you have code you would like to contribute, please feel free to submit a pull request on GitHub. (Note that Issues and Bugs and Feature Requests are also helpful and very much appreciated!)
There are a few guidelines that make it more likely a PR can be accepted:
Generally speaking, development happens on the
dev
branch. Themaster
branch is reserved for released code only. (If you submit a pull request tomaster
, we will change it todev
.)The automated test suite should run when pull requests are submitted. If there are any problems, you should do your best to fix them (or explain why the test is flagging when it shouldn’t). Code that passes has a much higher chance of being accepted than code that fails. See Running Tests above.
Pay attention to code style. Flake8 runs as part of the test suite.
#noqa
is allowed, but with good reason.If you add new functionality, it has a higher chance of being accepted if you add additional documentation and tests to go with it. The automated test suite generates a code coverage report, both locally and online at TODO.
Pull requests need not be related to an existing issue, but if you submit one that is, you should reference the issue number somewhere in the pull request.
None of these are automatic deal breakers if you do not follow them, but following them does increase the chances of your pull request being accepted.
All merged code contributions will be mentioned in the CHANGELOG with attribution to the contributor.
Contributing Updaters and Notifiers¶
If you have written a new updater or notifier and wish to share it with the community, you have two options:
Contribute it for inclusion in Ruddr itself. To do this:
Add a new .py file with your updater/notifier under the appropriate package in the Ruddr sources (
src/ruddr/updaters
orsrc/ruddr/notifiers
)Add a new entry to the
updaters
ornotifiers
dict in the__init__.py
file in the same directory. The key will become the built-in type name of the updater or notifier, used for thetype=
config option. The value must be the class for the new updater/notifier.Add documentation for the new updater/notifier. Add a new section to
docs/updaters.rst
ordocs/notifiers.rst
listing the name, a brief description of the updater/notifier, a sample config snippet, and a detailed list of the configuration options it accepts.Open a pull request.
If you would prefer to independently maintain your updater or notifier, you can publish it to PyPI with a
ruddr.updater
orruddr.notifier
entry point. Anyone who installs your updater/notifier from PyPI will then be able to use that entry point name as atype=
option in their config.For example, if you declare this entry point in your
pyproject.toml
:[project.entry-points."ruddr.updater"] my_updater = "myupdater:MyUpdater"
then someone can use your
myupdater.MyUpdater
class as an updater with this Ruddr config snippet:[updater.foo] type = my_updater # ... other config for the updater
For more information on publishing an updater like this, see the second method under Using your Custom Updater or Notifier.
Some conventions when developing updaters or notifiers for inclusion in Ruddr:
Make liberal use of the logger, especially when there is a problem. In particular, Ruddr uses exceptions mainly to control contingency behavior when there is a problem. The exception message is, for the most part, ignored. That’s not to say exceptions shouldn’t carry an appropriate message, but the primary way errors are communicated to the user is through logging. If there is a problem, an error should always be logged (critical if the problem is fatal to the updater/notifier), and a warning should be logged for potential problems.
Do as much useful work as possible, even if errors require skipping some parts. For example, if an updater can’t update one domain due to a typo in its name, it should still update the rest of the configured domains.
Don’t trust any input, whether from user config or API calls. For example, an improperly formatted IP address should be caught, logged, and an appropriate Ruddr exception raised, preventing a
ValueError
from crashing the whole program.