deploy-sh/deploy.sh
2025-05-08 19:14:49 +02:00

331 lines
12 KiB
Bash

#!/usr/bin/env bash
set -e
log() {
local prefix=""
if [[ -n "$host" ]]; then
prefix="[$host] "
fi
echo -e "\e[1m${2:-\e[34m}$prefix$1\e[0m"
}
sshHost() { echo "${1%:*}"; }
sshPort() { [[ "$1" =~ :[0-9]+$ ]] && echo "-p${1##*:}" || echo -p22; }
nixSshHost() { echo "ssh://$(sshHost $1)"; }
nixSshPort() { echo "NIX_SSHOPTS=$(sshPort $1)"; }
nix="nix --extra-experimental-features nix-command --extra-experimental-features flakes"
extractKernelPaths() {
build=$($nix derivation show "$1^*" | jq -r 'values | .[].env.buildCommand')
for path in initrd kernel kernel-modules; do
grep -o "^ln -s .* \\\$out/$path\$" <<< "$build" | cut -d' ' -f3
done
}
deploy() {
host="$1"
trap "log 'Deployment of $host failed!' \"\e[31m\"" ERR
log "Starting deployment of $host" "\e[33m"
log "Evaluating configuration"
local config
config=$($nix build --no-link --print-out-paths ".#nixosConfigurations.\"$host\".config.deploy-sh._config")
source "$config"
log "Evaluation succeeded!" "\e[32m"
if [[ -n "$targetHostOverride" ]]; then
targetHost="$targetHostOverride"
fi
if [[ -n "$buildHostOverride" ]]; then
buildHost="$buildHostOverride"
elif [[ $buildLocal = 1 ]]; then
buildHost=""
elif [[ $buildRemote = 1 ]]; then
buildHost="$targetHost"
fi
if [[ -n "$buildCacheOverride" ]]; then
buildCache="$buildCacheOverride"
fi
if [[ "$buildCache" = "\0" ]]; then
buildCache=""
fi
if [[ $nvd = 1 ]] || [[ $diff = 1 ]]; then
log "Copying current derivation from target host $targetHost"
if ! currentDrv=$(ssh $(sshPort "$targetHost") $(sshHost "$targetHost") nix-store --query --deriver /run/current-system); then
log "Failed to lookup current system on $targetHost" "\e[31m"
return 1
fi
if [[ "$currentDrv" = "$systemDrv" ]]; then
log "Current and new configuration are the same" "\e[32m"
else
if ! [[ -e "$currentDrv" ]] && ! env $(nixSshPort "$targetHost") $nix copy --derivation --no-check-sigs --from $(nixSshHost "$targetHost") "$currentDrv^*"; then
if [[ -z "$buildHost" ]] || [[ "$targetHost" = "$buildHost" ]]; then
log "Failed to fetch current system from $targetHost" "\e[31m"
return 1
fi
log "Failed to fetch current system from $targetHost" "\e[33m"
if ! env $(nixSshPort "$buildHost") $nix copy --derivation --no-check-sigs --from $(nixSshHost "$buildHost") "$currentDrv^*"; then
log "Failed to fetch current system from $buildHost" "\e[31m"
return 1
fi
fi
log "Current and new configuration differ:" "\e[33m"
if [[ $diff = 1 ]]; then
nix-diff "$currentDrv" "$systemDrv"
fi
if [[ $nvd = 1 ]]; then
nvd diff "$currentDrv" "$systemDrv"
fi
if [[ "$(extractKernelPaths $currentDrv)" != "$(extractKernelPaths $systemDrv)" ]]; then
log "Reboot is recommended after deploying this configuration!" "\e[33m"
fi
fi
fi
if [[ "$action" = "eval" ]]; then
return
fi
if [[ -z "$buildHost" ]]; then
log "Building locally, then deploying to $targetHost" "\e[36m"
elif [[ "$buildHost" = "$targetHost" ]]; then
log "Building and deploying to $targetHost" "\e[36m"
else
log "Building on $buildHost, then deploying to $targetHost" "\e[36m"
fi
if [[ -n "$buildCache" ]]; then
if [[ -z "$buildHost" ]]; then
log "Cache the system at $buildCache" "\e[36m"
else
log "Cache the system on $buildHost at $buildCache" "\e[36m"
fi
fi
log "System path: $system" "\e[0m"
log "System derivation: $systemDrv" "\e[0m"
local nomPipe="--log-format internal-json -v |& $nom/bin/nom --json"
if [[ "$buildHost" != "$targetHost" ]] && [[ "$pushDerivations" = "1" ]]; then
log "Copying derivations to target host $targetHost"
env $(nixSshPort "$targetHost") $nix copy --derivation --to $(nixSshHost "$targetHost") "$systemDrv^*"
fi
if [[ -n "$buildHost" ]]; then
log "Copying derivations to build host $buildHost"
env $(nixSshPort "$buildHost") $nix copy --derivation --to $(nixSshHost "$buildHost") "$systemDrv^*" "$nomDrv^*"
if [[ $fetch = 1 ]] && [[ "$targetHost" != "$buildHost" ]]; then
if oldSystem=$(ssh $(sshPort "$targetHost") $(sshHost "$targetHost") readlink /run/current-system); then
log "Fetching old system from $targetHost"
ssh $(sshPort "$buildHost") $(sshHost "$buildHost") env $(nixSshPort "$targetHost") $nix copy --no-check-sigs --from $(nixSshHost "$targetHost") "$oldSystem"
else
log "Failed to lookup current system on $targetHost" "\e[31m"
fi
fi
log "Building system for $targetHost on $buildHost"
ssh $(sshPort "$buildHost") $(sshHost "$buildHost") $nix build --no-link "$nomDrv^*"
if [[ -n "$buildCache" ]]; then
ssh $(sshPort "$buildHost") $(sshHost "$buildHost") "mkdir -p \"$(basename "$buildCache")\" && $nix build -o \"$buildCache\" \"$systemDrv^*\" $nomPipe"
else
ssh $(sshPort "$buildHost") $(sshHost "$buildHost") "$nix build --no-link \"$systemDrv^*\" $nomPipe"
fi
if [[ "$targetHost" != "$buildHost" ]]; then
log "Copying system to $targetHost"
ssh $(sshPort "$buildHost") $(sshHost "$buildHost") env $(nixSshPort "$targetHost") $nix copy --to $(nixSshHost "$targetHost") "$system"
fi
else
if [[ $fetch = 1 ]]; then
if oldSystem=$(ssh $(sshPort "$targetHost") $(sshHost "$targetHost") readlink /run/current-system); then
log "Fetching old system from $targetHost"
env $(nixSshPort "$targetHost") $nix copy --no-check-sigs --from $(nixSshHost "$targetHost") "$oldSystem"
else
log "Failed to lookup current system on $targetHost" "\e[31m"
fi
fi
log "Building system for $targetHost locally"
$nix build --no-link "$nomDrv^*"
if [[ -n "$buildCache" ]]; then
mkdir -p $(basename "$buildCache")
bash -c "$nix build -o \"$buildCache\" \"$systemDrv^*\" $nomPipe"
else
bash -c "$nix build --no-link \"$systemDrv^*\" $nomPipe"
fi
log "Copying system to $targetHost"
env $(nixSshPort "$targetHost") $nix copy --to $(nixSshHost "$targetHost") "$system"
fi
if [[ "$action" = "build" ]]; then
return
fi
log "Activating system on $targetHost ($action)"
if [[ "$action" =~ ^(switch|boot|reboot)$ ]]; then
ssh $(sshPort "$targetHost") $(sshHost "$targetHost") nix-env -p /nix/var/nix/profiles/system --set "$system"
fi
if [[ "$action" = "reboot" ]]; then
ssh $(sshPort "$targetHost") $(sshHost "$targetHost") "$system/bin/switch-to-configuration" boot
log "Rebooting $targetHost"
ssh $(sshPort "$targetHost") $(sshHost "$targetHost") reboot
log "Waiting for $targetHost to reboot"
sleep 5
while ! ssh $(sshPort "$targetHost") $(sshHost "$targetHost") -o StrictHostKeyChecking=yes true; do
sleep 1
done
else
ssh $(sshPort "$targetHost") $(sshHost "$targetHost") "$system/bin/switch-to-configuration" "$action"
fi
if ! ssh $(sshPort "$targetHost") $(sshHost "$targetHost") 'cmp -s <(readlink /run/booted-system/{initrd,kernel,kernel-modules}) <(readlink /run/current-system/{initrd,kernel,kernel-modules})'; then
rebootHosts+=("$host")
log "It is recommended to reboot the target host now!" "\e[33m"
fi
log "Deployment of $host succeeded!" "\e[32m"
}
for arg in $@; do
if [[ "$arg" =~ ^(-h|--help)$ ]]; then
echo -e "Simple NixOS remote deployment tool (\e[36mhttps://git.defelo.de/Defelo/deploy-sh\e[0m)"
echo -e
echo -e "\e[1m\e[32mUsage: \e[36mdeploy [OPTIONS] [HOSTS]...\e[0m"
echo -e
echo -e "For each host, only the most recent options to its left are taken into account. For"
echo -e "example, \`\e[36mdeploy --local foo bar --remote baz\e[0m\` will build hosts foo and bar locally,"
echo -e "and only baz on a remote build host."
echo -e "All hosts are deployed if no host is specified explicitly."
echo -e
echo -e "\e[1m\e[32mActivation options:\e[0m"
echo -e "\e[1m\e[36m --switch \e[0m Build and activate the new configuration, and make it the boot default. (default)"
echo -e "\e[1m\e[36m --boot \e[0m Build the new configuration and make it the boot default, but do not activate it."
echo -e "\e[1m\e[36m --test \e[0m Build and activate the new configuration, but do not add it to the boot menu."
echo -e "\e[1m\e[36m --dry-activate \e[0m Build the new configuration, but only dry-activate it."
echo -e "\e[1m\e[36m --reboot \e[0m Build the new configuration, make it the boot default and reboot into the new system."
echo -e "\e[1m\e[36m --eval \e[0m Evaluate the new configuration, but neither build nor activate it."
echo -e "\e[1m\e[36m --build \e[0m Build the new configuration, but do not activate it."
echo -e
echo -e "\e[1m\e[32mDiff options:\e[0m"
echo -e "\e[1m\e[36m --diff \e[0m Display differences between the current and new configuration"
echo -e "\e[1m\e[36m --no-diff \e[0m Don't display differences between the current and new configuration."
echo -e "\e[1m\e[36m --nvd \e[0m Display package differences between the current and new configuration. (default)"
echo -e "\e[1m\e[36m --no-nvd \e[0m Don't display package differences between the current and new configuration."
echo -e
echo -e "\e[1m\e[32mHost options:\e[0m"
echo -e "\e[1m\e[36m --local \e[0m Build the configuration locally and copy the new system to the target host."
echo -e "\e[1m\e[36m --remote \e[0m Build the configuration on the remote build host."
echo -e "\e[1m\e[36m --build-host \e[0m Set the host to build the configuration on."
echo -e "\e[1m\e[36m --target-host \e[0m Set the host to deploy the system on."
echo -e
echo -e "\e[1m\e[32mBuild options:\e[0m"
echo -e "\e[1m\e[36m --cache \e[0m Set a path on the build host where to store a symlink to the new system to avoid garbage collection."
echo -e "\e[1m\e[36m --no-cache \e[0m Don't store a symlink to the new system on the build host."
echo -e "\e[1m\e[36m --fetch \e[0m Copy the current system of the target host to the build host before building."
echo -e "\e[1m\e[36m --no-fetch \e[0m Don't copy the current system of the target host to the build host before building. (default)"
echo -e
echo -e "\e[1m\e[32mOptions:\e[0m"
echo -e "\e[1m\e[36m -h --help \e[0m Print help"
exit
fi
done
action=switch
buildLocal=0
buildRemote=0
buildHostOverride=""
targetHostOverride=""
buildCacheOverride=""
fetch=0
deployed=0
diff=0
nvd=1
rebootHosts=()
while [[ $# -gt 0 ]]; do
i="$1"; shift 1
case "$i" in
--switch|--boot|--test|--dry-activate|--reboot|--eval|--build)
action=${i#"--"}
;;
--diff)
diff=1
;;
--no-diff)
diff=0
;;
--nvd)
nvd=1
;;
--no-nvd)
nvd=0
;;
--local)
buildLocal=1
buildRemote=0
buildHostOverride=""
;;
--remote)
buildLocal=0
buildRemote=1
buildHostOverride=""
;;
--buildHost|--build-host)
buildLocal=0
buildRemote=0
buildHostOverride="$1"; shift 1
;;
--targetHost|--target-host)
targetHostOverride="$1"; shift 1
;;
--cache)
buildCacheOverride="$1"; shift 1
;;
--no-cache)
buildCacheOverride="\0"
;;
--fetch)
fetch=1
;;
--no-fetch)
fetch=0
;;
-*)
echo -e "\e[1m\e[31mUnknown flag: $i\e[0m"
echo -e "For more information, try '\e[1m\e[36m--help\e[0m'."
exit 1
;;
*)
if [[ $deployed = 1 ]]; then echo; fi
deploy "$i"
deployed=1
;;
esac
done
if [[ $deployed = 0 ]]; then
for host in $($nix eval --raw .#deploy-sh.hosts --apply 'hosts: builtins.concatStringsSep " " (builtins.attrNames hosts)'); do
if [[ $deployed = 1 ]]; then echo; fi
deploy "$host"
deployed=1
done
fi
unset host
if [[ ${#rebootHosts[@]} -ne 0 ]]; then
log "\nIt is recommended to reboot the following target hosts now:" "\e[33m"
for h in ${rebootHosts[@]}; do
log " - $h" "\e[33m"
done
fi