summaryrefslogtreecommitdiff
path: root/openwrt/uci.nix
diff options
context:
space:
mode:
Diffstat (limited to 'openwrt/uci.nix')
-rw-r--r--openwrt/uci.nix216
1 files changed, 216 insertions, 0 deletions
diff --git a/openwrt/uci.nix b/openwrt/uci.nix
new file mode 100644
index 0000000..1557797
--- /dev/null
+++ b/openwrt/uci.nix
@@ -0,0 +1,216 @@
+{ pkgs, lib, config, ... }:
+
+let
+ cfg = config.uci;
+
+ formatConfig = nix:
+ lib.concatStringsSep
+ "\n"
+ (lib.flatten
+ (lib.mapAttrsToList
+ (config: sections: [
+ "package ${config}"
+ (lib.mapAttrsToList formatSections sections)
+ ])
+ nix));
+
+ formatSections = type: sections:
+ if lib.isAttrs sections
+ then
+ lib.mapAttrsToList
+ (name: vals: [
+ "config ${type} ${formatScalar name}"
+ (formatSection vals)
+ ])
+ sections
+ else
+ map
+ (vals: [
+ "config ${type}"
+ (formatSection vals)
+ ])
+ sections;
+
+ formatSection =
+ lib.mapAttrsToList
+ (option: value:
+ if lib.isList value
+ then map (value: " list ${option} ${formatScalar value}") value
+ else " option ${option} ${formatScalar value}");
+
+ formatScalar = val:
+ if lib.isBool val then (if val then "'1'" else "'0'")
+ else if lib.isInt val then "'${toString val}'"
+ else if lib.isAttrs val then "'${secretName val._secret}'"
+ else "'${lib.replaceStrings [ "'" ] [ "'\\''" ] val}'";
+
+ secretName = sec: "@secret_${sec}_${builtins.hashString "sha256" sec}@";
+
+ collectSecrets = nix:
+ lib.pipe nix [
+ lib.attrValues
+ (lib.concatMap lib.attrValues)
+ (lib.concatMap (s: if lib.isAttrs s then lib.attrValues s else s))
+ (lib.concatMap lib.attrValues)
+ (lib.concatMap lib.toList)
+ (lib.concatMap (v: if v ? _secret then [{ name = v._secret; value = {}; }] else []))
+ lib.listToAttrs
+ lib.attrNames
+ ];
+in
+
+{
+ options.uci = {
+ secretsCommand = lib.mkOption {
+ type = lib.types.path;
+ default = pkgs.writeScript "no-secrets" "echo '{}'";
+ description = ''
+ Command to retrieve secrets. Must be an executable command that
+ returns a JSON object on `stdout`, with secret names as keys and string
+ values.
+ '';
+ };
+
+ sopsSecrets = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ description = ''
+ sops secrets file. This as a shorthand for setting {option}`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.
+ '';
+ };
+
+ settings = lib.mkOption {
+ type = with lib.types;
+ let
+ scalar = oneOf [
+ str
+ int
+ bool
+ (submodule {
+ options._secret = lib.mkOption {
+ type = str;
+ description = ''
+ Name of the secret to insert into the config from data exported
+ by {option}`secretsCommand`. Secrets are always interpolated as
+ strings, which uci allows for scalars. Lists cannot currently
+ be made entirely secret, only individual values of lists can.
+ '';
+ };
+ })
+ ];
+ options = attrsOf (either scalar (listOf scalar));
+ in
+ submodule {
+ freeformType =
+ # <config>.<name>=type -> config.type.name ...
+ # <config>.@<anonymous>=type -> config.type = [{ ... }]
+ attrsOf # config
+ (attrsOf # type
+ (either
+ (attrsOf options) # name ...
+ (listOf options) # [{ ... }]
+ ));
+ };
+ default = {};
+ description = ''
+ 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.
+ '';
+ example = {
+ network = {
+ interface.loopback = {
+ device = "lo";
+ proto = "static";
+ ipaddr = "127.0.0.1";
+ netmask = "255.0.0.0";
+ };
+
+ globals = [{ ula_prefix = "fdb8:155d:7ef5::/48"; }];
+ };
+ };
+ };
+
+ retain = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [];
+ example = [ "ucitrack" ];
+ description = ''
+ UCI package configuration to retain. Packages listed here will not have preexisting
+ configuration deleted during deployment, even if no matching {option}`settings`
+ are defined.
+ '';
+ };
+ };
+
+ config = {
+ uci.secretsCommand = lib.mkIf (cfg.sopsSecrets != null)
+ (pkgs.writeShellScript "sops" ''
+ ${pkgs.sops}/bin/sops --output-type json -d ${lib.escapeShellArg "${cfg.sopsSecrets}"}
+ '');
+
+ build.configFile = pkgs.writeText "config" (formatConfig cfg.settings);
+
+ deploySteps.uciConfig =
+ let
+ cfgName = baseNameOf config.build.configFile;
+ jq = "${pkgs.jq}/bin/jq";
+ configured = lib.attrNames config.uci.settings ++ config.uci.retain;
+ in
+ {
+ priority = 4999;
+ prepare = ''
+ cp --no-preserve=all ${config.build.configFile} "$TMP"
+ (
+ umask 0077
+ C="$TMP"/${cfgName}
+ S="$TMP"/secrets
+ ${cfg.secretsCommand} > "$S"
+ [ "$(${jq} -r type <"$S")" == "object" ] || {
+ log_err "secrets command did not produce an object"
+ exit 1
+ }
+ ${lib.concatMapStrings
+ (secret: let arg = lib.escapeShellArg secret; in ''
+ has="$(${jq} -r --arg s ${arg} 'has($s)' <"$S")"
+ $has || {
+ log_err secret ${arg} not defined
+ exit 1
+ }
+ ${pkgs.replace-secret}/bin/replace-secret \
+ ${lib.escapeShellArg (secretName secret)} \
+ <(${jq} -r --arg s ${arg} '.[$s]'" | tostring | sub(\"'\"; \"'\\\\'''\")" <"$S") \
+ "$C"
+ '')
+ (collectSecrets cfg.settings)}
+ )
+ '';
+ copy = ''
+ scp "$TMP"/${cfgName} device:/tmp/
+ '';
+ apply = ''
+ uci import < /tmp/${cfgName}
+ uci commit
+
+ (
+ cd /etc/config
+ for cfg in *; do
+ case "$cfg" in
+ ${lib.optionalString (configured != []) ''
+ ${lib.concatMapStringsSep "|" lib.escapeShellArg configured}) : ;;
+ ''}
+ *) rm "$cfg" ;;
+ esac
+ done
+ )
+ '';
+ };
+ };
+}