dewclaw: semi-declarative OpenWrt configuration
OpenWrt is an embedded Linux distribution optimized for small routers and access points with minimal amounts of storage to work with.
NixOS is a general-purpose Linux distribution built from the ground up with declarative configuration in mind, usually using a bunch of storage to do its thing.
dewclaw
is what happens if you try to mush the two together even though you know very well that you shouldn't.
Originally created by @pennae, you can find the annocement thread here.
This is a fork based on the origial code.
How to use
dewclaw can declaratively manage some (but by far not all) aspects of OpenWrt devices.
Packages can be installed (and subsequently removed) declaratively by listing them in the packages
option.
UCI configs can be set declaratively using the uci.settings
hierarchy, or be marked for imperative configuration by adding the appropriate package names to uci.retain
.
Files in /etc
can be create with the etc
hierarchy.
Mapping UCI options
Mapping existing UCI configurations to uci.settings
values is straight-forward starting with the output of uci show
. UCI outputs its configuration in a specific format:
package.namedSection=type1
package.namedSection.option='value'
package.namedSection.list='value1' 'value2' ...
package.@anonSection=type2
package.@anonSection.option='value'
In dewclaw package
is the top level of keys in uci.settings
, type
is the second level, and below the type
level we either have a third namedSection
level or a list of anonSection
s.
Each named or anonymous section is itself a set of option = value
assignments.
dewclaw cannot mix named and anonymous sections, any given type must be configured entirely with named sections or entirely with unnamed sections.
The example uci show
output above would thus map to the following dewclaw device configuration:
openwrt.router.uci.settings = {
package.type1 = {
namedSection = {
option = "value";
list = [ "value1" "value2" ];
};
};
package.type2 = [
{
option = "value";
}
];
}
Option values may be any UCI-compatible type: strings, paths and integers are passed through, booleans are converted to 0/1
.
Additionally there is support for secret values, with a sops secrets backend built into dewclaw directly.
Secrets are loaded from a backend during deployment time and will be interpolated into the generated UCI config.
To load an option value from a secret, set option._secret = "secretName"
in uci.settings
.
Building a configuration
Once a configuration for any number of devices is written it can be passed to dewclaw and built into a set of deployment scripts:
{ pkgs ? import <nixpkgs> {} }:
import <dewclaw> {
inherit pkgs;
configuration = ./config.nix;
}
All openwrt
device configurations listed in config.nix
will be built, each producing a stand-alone deployment script, and provided in a single nix output.
Deploying a configuration
Building the provided example produces an output with a single deployment script, deploy-example
, that can be run without arguments to deploy to the assigned target and reboot the device.
The deployment process on the device will take a snapshot of the current device configuration, apply changes as needed to satisfy the new configuration, and wait for confirmation that the new configuration is acceptable.
The deployment script provides this confirmation by reconnecting to the device after it has rebooted, if this reconnection succeeds the configuration is accepted.
After a reboot the device will wait for a set amount of time before automatically rolling back to the previous configuration.
Reload-only deployment
Deploy scripts also accept a --reload
argument to instruct the device to only reload UCI configuration instead of rebooting.
This is faster and less disruptive but may have unintended side-effects on services that are not properly configured by OpenWrt's reload_config
and should thus be used with care.
Despite not rebooting to apply the configuration this mode also takes a snapshot and performs a rollback if no confirmation is provided.
_module.args
Additional arguments passed to each module in addition to ones
like lib
, config
,
and pkgs
, modulesPath
.
This option is also available to all submodules. Submodules do not
inherit args from their parent module, nor do they provide args to
their parent module or sibling submodules. The sole exception to
this is the argument name
which is provided by
parent modules to a submodule and contains the attribute name
the submodule is bound to, or a unique generated name if it is
not bound to an attribute.
Some arguments are already passed by default, of which the following cannot be changed with this option:
-
lib
: The nixpkgs library. -
config
: The results of all options after merging the values from all modules together. -
options
: The options declared in all modules. -
specialArgs
: ThespecialArgs
argument passed toevalModules
. -
All attributes of
specialArgs
Whereas option values can generally depend on other option values thanks to laziness, this does not apply to
imports
, which must be computed statically before anything else.For this reason, callers of the module system can provide
specialArgs
which are available during import resolution.For NixOS,
specialArgs
includesmodulesPath
, which allows you to import extra modules from the nixpkgs package tree without having to somehow make the module aware of the location of thenixpkgs
or NixOS directories.{ modulesPath, ... }: { imports = [ (modulesPath + "/profiles/minimal.nix") ]; }
For NixOS, the default value for this option includes at least this argument:
pkgs
: The nixpkgs package set according to thenixpkgs.pkgs
option.
Type: lazy attribute set of raw value
Declared by:
openwrt
OpenWrt device configurations. Each attribute will produce an indepdent deployment script that applies the corresponding configuration to the target device.
Type: attribute set of (OpenWrt configuration)
Default:
{ }
Declared by:
openwrt.<name>.packages
Extra packages to install. These are merely names of packages available to opkg through the package source lists configured on the device, it is not currently possible to provide packages for installation without configuring an opkg source first.
Type: list of string
Default:
[ ]
Declared by:
openwrt.<name>.deploy.host
Host to deploy to. Defaults to the attribute name, but this may have unintended
side-effects when deploying to the DNS server of the current network. Prefer
IP addresses or names of ssh_config
host blocks for such cases.
Type: string
Default:
"‹name›"
Example:
"192.168.0.1"
Declared by:
openwrt.<name>.deploy.rebootAllowance
How long to wait (in seconds) for the device to come back up. The timer runs on the deploying host and starts when the device reboots.
Type: unsigned integer, meaning >=0
Default:
60
Declared by:
openwrt.<name>.deploy.reloadServiceWait
How long to wait (in seconds) during reload-only deployment to allow for more graceful service restarts. Small values make reloads faster, but since OpenWrt has no mechanism to figure out when all services are done starting this also introduces possible failure points.
Type: unsigned integer, meaning >=0
Default:
10
Declared by:
openwrt.<name>.deploy.rollbackTimeout
How long to wait (in seconds) before rolling back to the old configuration. The timer runs on the device and starts once the device has completed its boot cycle.
Warning: Values under 20
will very likely cause spurious rollbacks.
Note: During reload-only deployment this timeout includes the time needed to apply configuration, which may be substatial if network activity is necessary (eg when installing packages).
Type: unsigned integer, meaning >=0
Default:
60
Declared by:
openwrt.<name>.deploy.sshConfig
SSH options to apply to connections, see ssh_config(5)
.
Notably these are not command-line arguments, although they will
be passed as -o...
arguments.
Type: attribute set of (string or signed integer or boolean or path)
Default:
{ }
Declared by:
openwrt.<name>.etc
Extra files to create in the target /etc
. It is not currently possible to
delete files from the target.
This option should usually not be used if there’s a UCI way to achieve the same effect.
Type: attribute set of (`/etc` file description)
Default:
{ }
Declared by:
openwrt.<name>.etc.<name>.enable
Whether to enable this /etc
file.
Type: boolean
Default:
true
Example:
true
Declared by:
openwrt.<name>.etc.<name>.text
Contents of the file.
Type: strings concatenated with “\n”
Declared by:
openwrt.<name>.providers
Alternative method to replace critical packages with another variant, for
example dnsmasq
.
This option should only be used when absolutely necessary, as packages
installed this way cannot be automatically cleaned up like packages
.
Type: attribute set of string
Default:
{ }
Example:
''
{
dnsmasq = "dnsmasq-full";
}
''
Declared by:
openwrt.<name>.services.qemu-ga.enable
Whether to enable QEMU guest agent.
Type: boolean
Default:
false
Example:
true
Declared by:
openwrt.<name>.services.qemu-ga.package
QEMU guest agent package to use.
Type: string
Default:
"qemu-ga"
Example:
"qemu-ga"
Declared by:
openwrt.<name>.services.statistics.enable
Whether to enable statistics service.
Type: boolean
Default:
false
Example:
true
Declared by:
openwrt.<name>.services.statistics.backup.enable
Whether to enable statistics periodical backup service.
Type: boolean
Default:
true
Example:
true
Declared by:
openwrt.<name>.services.statistics.backup.period
Crontab formatted string for backup period. Protect against unintended poweroff event.
Default to backup every hour.
Type: string
Default:
"0 * * * *"
Example:
"0 * * * *"
Declared by:
openwrt.<name>.services.statistics.monitors.interfaces.enable
Whether to enable network interface monitors.
Type: boolean
Default:
true
Example:
true
Declared by:
openwrt.<name>.services.statistics.monitors.interfaces.targets
List of network interfaces that will be monitored.
Type: list of string
Default:
[ ]
Example:
[
"eth0"
]
Declared by:
openwrt.<name>.uci.retain
UCI package configuration to retain. Packages listed here will not have preexisting
configuration deleted during deployment, even if no matching settings
are defined.
Type: list of string
Default:
[ ]
Example:
[
"ucitrack"
]
Declared by:
openwrt.<name>.uci.secretsCommand
Command to retrieve secrets. Must be an executable command that
returns a JSON object on stdout
, with secret names as keys and string
values.
Type: path
Default:
<derivation no-secrets>
Declared by:
openwrt.<name>.uci.settings
UCI settings in hierarchical representation. The toplevel key of this set denotes a UCI package, the second level the type of section, and the third level may be either a list of anonymous setions or a set of named sections.
Packages defined here will replace existing settings on the system entirely, no merging with existing configuration is done.
Type: UCI config
Default:
{ }
Example:
{
network = {
globals = [
{
ula_prefix = "fdb8:155d:7ef5::/48";
}
];
interface = {
loopback = {
device = "lo";
ipaddr = "127.0.0.1";
netmask = "255.0.0.0";
proto = "static";
};
};
};
}
Declared by:
openwrt.<name>.uci.sopsSecrets
sops secrets file. This as a shorthand for setting secretsCommand
to a script that calls sops -d <path>
. Path semantics apply: if the given
path is a path literal it is copied into the store and the resulting absolute
path is used, otherwise the given path is used verbatim in the generated script.
Type: null or path
Default:
null
Declared by:
openwrt.<name>.users.root.hashedPassword
Hashed password of the user. This should be either a disabled password
(e.g. *
or !
) or use MD5, SHA256, or SHA512.
You can use openssl passwd -6 -stdin
to generate this hash.
Type: null or string matching the pattern [^ :]*
Default:
null
Declared by: