diff options
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | doc/book.toml | 2 | ||||
-rw-r--r-- | doc/default.nix | 47 | ||||
-rw-r--r-- | doc/src/SUMMARY.md | 6 | ||||
-rw-r--r-- | doc/src/how-to.md | 74 | ||||
-rw-r--r-- | openwrt/default.nix | 43 | ||||
-rw-r--r-- | openwrt/etc.nix | 38 | ||||
-rw-r--r-- | openwrt/uci.nix | 5 |
8 files changed, 200 insertions, 22 deletions
@@ -1,3 +1,8 @@ # Declarative Imperative OpenWRT configs -Or dewclaw, for short. It evals, ship it! +[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](./index.html) is what happens if you try to mush the two together even though you know very well that you shouldn't. + +[OpenWRT]: https://openwrt.org/ +[NixOS]: https://nixos.org/ diff --git a/doc/book.toml b/doc/book.toml new file mode 100644 index 0000000..bb3ccc2 --- /dev/null +++ b/doc/book.toml @@ -0,0 +1,2 @@ +[book] +title = "dewclaw documentation" diff --git a/doc/default.nix b/doc/default.nix new file mode 100644 index 0000000..95b9dea --- /dev/null +++ b/doc/default.nix @@ -0,0 +1,47 @@ +{ pkgs ? import <nixpkgs> { config = {}; overlays = []; } +}: + +let + evaluated = pkgs.lib.evalModules { + modules = [ + ../openwrt + ]; + specialArgs = { + inherit pkgs; + }; + }; + + optionsDoc = pkgs.nixosOptionsDoc { + inherit (evaluated) options; + transformOptions = opt: + let + cwd = toString ../.; + shorten = decl: + let + removed = pkgs.lib.removePrefix cwd decl; + in + if removed != decl + then { + url = + "https://git.eno.space/dewclaw.git/tree${removed}" + + (if pkgs.lib.hasSuffix ".nix" removed + then "" + else "/default.nix"); + name = "<dewclaw${removed}>"; + } + else removed; + in + opt // { declarations = map shorten opt.declarations; }; + }; +in + +pkgs.runCommand "dewclaw-book" { + src = ./src; + buildInputs = [ pkgs.mdbook ]; +} '' + cp -r --no-preserve=all $src ./src + ln -s ${optionsDoc.optionsCommonMark} ./src/options.md + ln -s ${../README.md} ./src/README.md + mdbook build + mv book $out +'' diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md new file mode 100644 index 0000000..2040fd3 --- /dev/null +++ b/doc/src/SUMMARY.md @@ -0,0 +1,6 @@ +# Summary + +[But why?](./README.md) + +- [How to use this](./how-to.md) +- [Options documentation](./options.md) diff --git a/doc/src/how-to.md b/doc/src/how-to.md new file mode 100644 index 0000000..a78f3c5 --- /dev/null +++ b/doc/src/how-to.md @@ -0,0 +1,74 @@ +# 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: +```nix +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: +```nix +{ 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. + +[sops]: https://github.com/getsops/sops diff --git a/openwrt/default.nix b/openwrt/default.nix index c9ae707..a39dcbd 100644 --- a/openwrt/default.nix +++ b/openwrt/default.nix @@ -5,12 +5,19 @@ let devType = lib.types.submoduleWith { specialArgs.pkgs = pkgs; + description = "OpenWRT configuration"; modules = [({ name, config, ... }: { options = { deploy = { host = lib.mkOption { type = lib.types.str; default = name; + example = "192.168.0.1"; + description = '' + 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. + ''; }; user = lib.mkOption { @@ -53,7 +60,7 @@ let Values under `20` will very likely cause spurious rollbacks. ::: - ::: {.notice} + ::: {.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). @@ -81,12 +88,30 @@ let deploySteps = lib.mkOption { type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { options = { - name = lib.mkOption { type = lib.types.str; default = name; }; - priority = lib.mkOption { type = lib.types.int; }; - - prepare = lib.mkOption { type = lib.types.lines; default = ""; }; - copy = lib.mkOption { type = lib.types.lines; default = ""; }; - apply = lib.mkOption { type = lib.types.lines; }; + name = lib.mkOption { + type = lib.types.str; + default = name; + internal = true; + }; + priority = lib.mkOption { + type = lib.types.int; + internal = true; + }; + + prepare = lib.mkOption { + type = lib.types.lines; + default = ""; + internal = true; + }; + copy = lib.mkOption { + type = lib.types.lines; + default = ""; + internal = true; + }; + apply = lib.mkOption { + type = lib.types.lines; + internal = true; + }; }; })); internal = true; @@ -249,5 +274,9 @@ in options.openwrt = lib.mkOption { type = lib.types.attrsOf devType; default = {}; + description = '' + OpenWRT device configurations. Each attribute will produce an indepdent deployment + script that applies the corresponding configuration to the target device. + ''; }; } diff --git a/openwrt/etc.nix b/openwrt/etc.nix index 8231125..b9f8324 100644 --- a/openwrt/etc.nix +++ b/openwrt/etc.nix @@ -6,21 +6,33 @@ in { options.etc = lib.mkOption { - type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: { - options = { - enable = lib.mkEnableOption "this `/etc` file" // { - default = true; - }; + type = lib.types.attrsOf (lib.types.submoduleWith { + description = "`/etc` file description"; + modules = [ + ({ name, ... }: { + options = { + enable = lib.mkEnableOption "this `/etc` file" // { + default = true; + }; - text = lib.mkOption { - type = lib.types.lines; - description = '' - Contents of the file. - ''; - }; - }; - })); + text = lib.mkOption { + type = lib.types.lines; + description = '' + Contents of the file. + ''; + }; + }; + }) + ]; + }); default = {}; + description = '' + 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. + ''; }; config = lib.mkIf (cfg != {}) { diff --git a/openwrt/uci.nix b/openwrt/uci.nix index ac9c9f6..d6d3288 100644 --- a/openwrt/uci.nix +++ b/openwrt/uci.nix @@ -124,7 +124,10 @@ in (either (uciAttrsOf "section" options) # name ... (listOf options) # [{ ... }] - )); + )) + // { + description = "UCI config"; + }; }; default = {}; description = '' |