summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md7
-rw-r--r--doc/book.toml2
-rw-r--r--doc/default.nix47
-rw-r--r--doc/src/SUMMARY.md6
-rw-r--r--doc/src/how-to.md74
-rw-r--r--openwrt/default.nix43
-rw-r--r--openwrt/etc.nix38
-rw-r--r--openwrt/uci.nix5
8 files changed, 200 insertions, 22 deletions
diff --git a/README.md b/README.md
index bc4036d..fb7c9a7 100644
--- a/README.md
+++ b/README.md
@@ -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 = ''