diff --git a/home/hosts/andromache/default.nix b/home/hosts/andromache/default.nix index f18f192b..fb0ed003 100644 --- a/home/hosts/andromache/default.nix +++ b/home/hosts/andromache/default.nix @@ -45,6 +45,7 @@ taskwarrior.enable = true; audio.enable = true; ssh.enable = true; + deploy.enable = true; music.enable = true; terminal.enable = true; devenv.enable = true; diff --git a/home/hosts/astyanax/default.nix b/home/hosts/astyanax/default.nix index a2941014..f613a92d 100644 --- a/home/hosts/astyanax/default.nix +++ b/home/hosts/astyanax/default.nix @@ -50,6 +50,7 @@ bash.addBinToPath = true; }; ssh.enable = true; + deploy.enable = true; taskwarrior.enable = true; terminal.enable = true; reference-manager.enable = true; diff --git a/home/modules/deploy/colmena-wrapper.bash b/home/modules/deploy/colmena-wrapper.bash new file mode 100644 index 00000000..a38ae78d --- /dev/null +++ b/home/modules/deploy/colmena-wrapper.bash @@ -0,0 +1,68 @@ +# `colmena` wrapper script that opens a short-lived SSH master process for +# `colmena` to use so it works with hardware-backed key touch+PIN. assumes +# ControlPath ~/.ssh/socket-%r@%h:%p (see ./default.nix) + +selector="" +want_on=0 +on_given=0 +deploy_cmd=0 +nodes=() +on_nodes=() +master_processes=() + +# build node list from `--on` value, a comma-separated string (e.g. "host-01,host-02") +for arg in "$@"; do + if ((want_on)); then + selector="$arg" + want_on=0 + continue + fi + case "$arg" in + --on=*) + selector="${arg#--on=}" + on_given=1 + ;; + --on) want_on=1 on_given=1 ;; + apply | exec | upload-keys) deploy_cmd=1 ;; + esac +done + +# shellcheck disable=SC2154 +if ((deploy_cmd)) && ! ((on_given)); then + on_nodes=("${!_colmena_node_tags[@]}") +else + IFS=',' read -ra nodes <<< "$selector" + for node in "${nodes[@]}"; do + for node_tag in "${!_colmena_node_tags[@]}"; do + if [[ "$node" == @* ]]; then + for tag in ${_colmena_node_tags[$node_tag]}; do + # shellcheck disable=SC2053 + [[ "$tag" == ${node#@} ]] && on_nodes+=("$node_tag") + done + else + # shellcheck disable=SC2053 + [[ "$node_tag" == $node ]] && on_nodes+=("$node_tag") + fi + done + done +fi + +for node in "${on_nodes[@]}"; do + # check if master process is running (see `man ssh`) + ssh -O check "$node" 2>/dev/null && continue + + # place ssh client into "master" mode for connection sharing (see `man ssh`) + if ssh -fNM "$node"; then + master_processes+=("$node") + fi +done + +rc=0 +@colmena@ "$@" || rc=$? + +# request master processes to exit (see `man ssh`) +for process in "${master_processes[@]}"; do + ssh -O exit "$process" 2>/dev/null || true +done + +exit "$rc" diff --git a/home/modules/deploy/default.nix b/home/modules/deploy/default.nix new file mode 100644 index 00000000..1bf98309 --- /dev/null +++ b/home/modules/deploy/default.nix @@ -0,0 +1,70 @@ +{ + config, + lib, + pkgs, + inputs, + myUtils, + ... +}: + +let + cfg = config.deploy; + + hostDir = ../../../hosts; + hostNames = myUtils.dirNames hostDir; + hostsWithKeys = lib.filter ( + hostname: builtins.pathExists (hostDir + "/${hostname}/ssh_host.pub") + ) hostNames; + + colmena = inputs.colmena.packages.${pkgs.stdenv.hostPlatform.system}.colmena; + + hostTags = hostname: (myUtils.hostMeta (hostDir + "/${hostname}")).tags; + remoteHostsWithKeys = lib.filter ( + hostname: !(builtins.elem "local" (hostTags hostname)) + ) hostsWithKeys; + nodeTagsDecl = '' + declare -A _colmena_node_tags=( + ${lib.concatMapStringsSep "\n" ( + hostname: " [${hostname}]=${lib.escapeShellArg (lib.concatStringsSep " " (hostTags hostname))}" + ) remoteHostsWithKeys} + ) + ''; + + colmenaWrapped = pkgs.writeShellApplication { + name = "colmena"; + runtimeInputs = [ pkgs.openssh ]; + text = + nodeTagsDecl + + lib.replaceStrings [ "@colmena@" ] [ "${colmena}/bin/colmena" ] ( + builtins.readFile ./colmena-wrapper.bash + ); + }; +in +{ + options.deploy.enable = lib.mkEnableOption "deploy"; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = config.ssh.enable; + message = "'home/modules/deploy' requires 'home/modules/ssh'"; + } + ]; + + home.packages = [ colmenaWrapped ]; + programs.ssh.settings = lib.genAttrs hostsWithKeys ( + hostname: + let + meta = myUtils.hostMeta (hostDir + "/${hostname}"); + isLocal = builtins.elem "local" meta.tags; + in + { + User = meta.host.username; + } + // lib.optionalAttrs (!isLocal) { + HostName = hostname; + ControlPath = "~/.ssh/socket-%r@%h:%p"; + } + ); + }; +} diff --git a/home/modules/ssh/default.nix b/home/modules/ssh/default.nix index 48704f82..c5bea019 100644 --- a/home/modules/ssh/default.nix +++ b/home/modules/ssh/default.nix @@ -2,17 +2,11 @@ config, lib, pkgs, - myUtils, ... }: let cfg = config.ssh; - hostDir = ../../hosts; - hostNames = myUtils.dirNames hostDir; - hostsWithKeys = lib.filter ( - hostname: builtins.pathExists (hostDir + "/${hostname}/ssh_host.pub") - ) hostNames; in { options.ssh.enable = lib.mkEnableOption "ssh"; @@ -24,30 +18,16 @@ in enable = true; enableDefaultConfig = false; - settings = - lib.genAttrs hostsWithKeys ( - hostname: - let - meta = myUtils.hostMeta (hostDir + "/${hostname}"); - isLocal = builtins.elem "local" meta.tags; - in - { - User = meta.host.username; - } - // lib.optionalAttrs (!isLocal) { - HostName = hostname; - } - ) - // { - "*" = { - AddKeysToAgent = "yes"; - ForwardAgent = false; - identityFile = [ - "~/.ssh/id_ed25519_sk" - "~/.ssh/id_ed25519_sk_bak" - ]; - }; + settings = { + "*" = { + AddKeysToAgent = "yes"; + ForwardAgent = false; + identityFile = [ + "~/.ssh/id_ed25519_sk" + "~/.ssh/id_ed25519_sk_bak" + ]; }; + }; }; }; } diff --git a/hosts/andromache/default.nix b/hosts/andromache/default.nix index f8c77e8f..36275e42 100644 --- a/hosts/andromache/default.nix +++ b/hosts/andromache/default.nix @@ -99,10 +99,6 @@ in boot.binfmt.emulatedSystems = [ "aarch64-linux" ]; - environment.systemPackages = [ - inputs.colmena.packages.${pkgs.stdenv.hostPlatform.system}.colmena - ]; - services.locate = { enable = true; package = pkgs.plocate; diff --git a/hosts/astyanax/default.nix b/hosts/astyanax/default.nix index c4ed207b..259f2d74 100644 --- a/hosts/astyanax/default.nix +++ b/hosts/astyanax/default.nix @@ -82,7 +82,6 @@ in }; environment.systemPackages = [ - inputs.colmena.packages.${pkgs.stdenv.hostPlatform.system}.colmena (pkgs.writeShellApplication { name = "wol-andromache"; runtimeInputs = [ pkgs.wakeonlan ];