summaryrefslogtreecommitdiff
path: root/openwrt/default.nix
diff options
context:
space:
mode:
authorpennae <pennae.git@eno.space>2023-09-22 20:55:05 +0200
committerpennae <pennae.git@eno.space>2023-09-22 21:06:55 +0200
commit66c6d2c1dfd4b3ef222bb64d3ccef9be915e0895 (patch)
tree0dde64acbdf9aa61134cdf066723bd731101f767 /openwrt/default.nix
downloaddewclaw-66c6d2c1dfd4b3ef222bb64d3ccef9be915e0895.tar.gz
dewclaw-66c6d2c1dfd4b3ef222bb64d3ccef9be915e0895.tar.xz
dewclaw-66c6d2c1dfd4b3ef222bb64d3ccef9be915e0895.zip
initial commit
without warranty of any kind, express or impliend
Diffstat (limited to 'openwrt/default.nix')
-rw-r--r--openwrt/default.nix214
1 files changed, 214 insertions, 0 deletions
diff --git a/openwrt/default.nix b/openwrt/default.nix
new file mode 100644
index 0000000..4bd00fb
--- /dev/null
+++ b/openwrt/default.nix
@@ -0,0 +1,214 @@
+{ config, lib, pkgs, ... }:
+
+let
+ cfg = config.openwrt;
+
+ devType = lib.types.submoduleWith {
+ specialArgs.pkgs = pkgs;
+ modules = [({ name, config, ... }: {
+ options = {
+ deploy = {
+ host = lib.mkOption {
+ type = lib.types.str;
+ default = name;
+ };
+
+ user = lib.mkOption {
+ type = lib.types.str;
+ default = "root";
+ visible = false;
+ description = ''
+ User name for SSH connections. Doesn't currently to anything useful considering
+ that we don't have any kind of `useSudo` option.
+ '';
+ };
+
+ sshConfig = lib.mkOption {
+ type = with lib.types; attrsOf (oneOf [ str int bool path ]);
+ default = {};
+ description = ''
+ SSH options to apply to connections, see {manpage}`ssh_config(5)`.
+ Notably these are *not* command-line arguments, although they *will*
+ be passed as `-o...` arguments.
+ '';
+ };
+
+ rebootAllowance = lib.mkOption {
+ type = lib.types.ints.unsigned;
+ default = 60;
+ description = ''
+ 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.
+ '';
+ };
+
+ rollbackTimeout = lib.mkOption {
+ type = lib.types.ints.unsigned;
+ default = 60;
+ description = ''
+ 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.
+ :::
+ '';
+ };
+ };
+
+ build = lib.mkOption {
+ type = lib.types.attrsOf lib.types.unspecified;
+ internal = true;
+ };
+
+ 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; };
+ };
+ }));
+ internal = true;
+ default = {};
+ };
+ };
+
+ imports = [
+ ./etc.nix
+ ./packages.nix
+ ./uci.nix
+ ./users.nix
+ ];
+
+ config = {
+ build.deploy =
+ let
+ steps = lib.sort (a: b: a.priority < b.priority) (lib.attrValues config.deploySteps);
+ prepare = lib.concatMapStringsSep "\n\n" (s: "# prepare ${s.name}\n${s.prepare}") steps;
+ copy = lib.concatMapStringsSep "\n\n" (s: "# copy ${s.name}\n${s.copy}") steps;
+ config_generation = pkgs.runCommand "config_generation.sh" {
+ src = ./config_generation.sh;
+ deploy_steps = ''
+ ${lib.concatMapStrings
+ (s: ''
+ # apply ${s.name}
+ log "running ${s.name} ..."
+ ${s.apply}
+ '')
+ steps}
+
+ log 'rebooting device ...'
+ '';
+ rollback_timeout = config.deploy.rollbackTimeout;
+ } ''
+ substitute "$src" "$out" \
+ --subst-var deploy_steps \
+ --subst-var rollback_timeout
+ chmod +x "$out"
+ '';
+ timeout = config.deploy.rollbackTimeout + config.deploy.rebootAllowance;
+ sshOpts =
+ ''-o ControlPath="$TMP/cm" ''
+ + lib.escapeShellArgs
+ (lib.mapAttrsToList
+ (arg: val: "-o${arg}=${
+ if val == true then "yes"
+ else if val == false then "no"
+ else toString val
+ }")
+ ({
+ ControlMaster = "auto";
+ User = config.deploy.user;
+ Hostname = config.deploy.host;
+ } // config.deploy.sshConfig)
+ );
+ in
+ pkgs.writeShellScriptBin "deploy-${name}" ''
+ set -euo pipefail
+ shopt -s inherit_errexit
+
+ BOLD='\e[1m'
+ PURP='\e[35m'
+ CYAN='\e[36m'
+ RED='\e[31m'
+ NORMAL='\e[0m'
+
+ log() {
+ printf "$BOLD$PURP> %s$NORMAL\n" "$*"
+ }
+ log_err() {
+ printf "$BOLD$RED> %s$NORMAL\n" "$*"
+ }
+
+ # generate a (reasonably) unique logger tag. mustn't be too long,
+ # or it'll be truncated and matching will fail.
+ TAG="apply_config_$$_$RANDOM"
+
+ ssh() {
+ command ssh ${sshOpts} device "$@"
+ }
+
+ scp() {
+ command scp -Op ${sshOpts} "$@"
+ }
+
+ main() {
+ export TMP="$(umask 0077; mktemp -d)"
+
+ trap '
+ [ -e "$TMP/cm" ] && ssh -O exit 2>/dev/null || true
+ rm -rf "$TMP"
+ ' EXIT
+
+ log 'preparing files'
+ ${prepare}
+
+ log 'copying files'
+ scp ${config_generation} device:/etc/init.d/config_generation
+ ${copy}
+
+ # apply the new config and wait for the box to go down via ssh connection
+ # timeout.
+ log 'applying config'
+ ssh '
+ export LOG_FMT="'"$CYAN"'>> %s'"$NORMAL"'"
+ /etc/init.d/config_generation apply </dev/null 2>&1 \
+ | logger -t '"$TAG" &
+ ssh 'logread -l9999 -f' | awk -v FS="$TAG: " '$2 { print $2 }' || true
+
+ log 'waiting for device to return'
+ __DO_WAIT=1 timeout --foreground ${toString timeout}s "$0" || {
+ log_err 'configuration change failed, device will roll back and reboot'
+ exit 1
+ }
+
+ log 'new configuration applied'
+ }
+
+ _wait() {
+ while ! ssh -oConnectTimeout=5 '/etc/init.d/config_generation commit'; do
+ sleep 1
+ done
+ }
+
+ case "''${__DO_WAIT:-}" in
+ "") main ;;
+ *) _wait ;;
+ esac
+ '';
+ };
+ })];
+ };
+
+in
+
+{
+ options.openwrt = lib.mkOption {
+ type = lib.types.attrsOf devType;
+ default = {};
+ };
+}