main
1#!/usr/bin/env bash
2# Remotely install NixOS on a target machine from your workstation.
3#
4# This script automates the full remote installation process:
5# 1. Verifies SSH connectivity to the target
6# 2. Copies your SSH public key for passwordless access (optional)
7# 3. Builds the disko partitioning script locally
8# 4. Builds the full NixOS system closure locally
9# 5. Copies the disko script to the target and runs it (format + mount)
10# 6. Resizes the live USB's tmpfs to fit the incoming closure
11# 7. Copies the system closure to the target
12# 8. Runs nixos-install on the target
13#
14# This avoids building anything on the target, which is useful when the
15# live USB doesn't have enough space for a full build.
16#
17# Prerequisites:
18# - Target machine booted from NixOS live USB/CD
19# - Password set for nixos user: passwd nixos
20# - Network connectivity between workstation and target
21# - Host has a disko config in systems/<hostname>/disks.nix
22#
23# The disk device is defined in systems/<hostname>/disks.nix.
24# LUKS-encrypted hosts will prompt for a passphrase during partitioning.
25#
26# Examples:
27# ./remote-install.sh okinawa 192.168.1.108
28# ./remote-install.sh okinawa 192.168.1.108 --no-copy-keys
29# ./remote-install.sh okinawa 192.168.1.108 --tmpfs-size 24G
30#
31# Options:
32# --no-copy-keys Skip copying SSH public key to target
33# --tmpfs-size SIZE Size for /nix/.rw-store tmpfs (default: 28G)
34# --skip-format Skip disko format, only mount (for re-running)
35# -h, --help Show this help message
36
37set -euo pipefail
38
39# --- Colors and logging ---------------------------------------------------
40
41bold='\033[1m'
42blue='\033[1;34m'
43yellow='\033[1;33m'
44red='\033[1;31m'
45green='\033[1;32m'
46reset='\033[0m'
47
48info() { echo -e "${blue}==>${reset} ${bold}$*${reset}"; }
49warn() { echo -e "${yellow}==>${reset} ${bold}$*${reset}"; }
50error() { echo -e "${red}==>${reset} ${bold}$*${reset}" >&2; }
51success() { echo -e "${green}==>${reset} ${bold}$*${reset}"; }
52
53# --- Usage -----------------------------------------------------------------
54
55usage() {
56 sed -n '/^# Remotely install/,/^[^#]/{ /^#/s/^# \{0,1\}//p }' "$0"
57 echo ""
58 echo "Usage: $0 <hostname> <target-ip> [options...]"
59 echo ""
60 echo "Arguments:"
61 echo " hostname NixOS host configuration to install (from flake)"
62 echo " target-ip IP address of the target machine (NixOS live USB)"
63}
64
65# --- Argument parsing ------------------------------------------------------
66
67COPY_KEYS=true
68TMPFS_SIZE="28G"
69SKIP_FORMAT=false
70POSITIONAL=()
71
72while [[ $# -gt 0 ]]; do
73 case "$1" in
74 -h|--help)
75 usage
76 exit 0
77 ;;
78 --no-copy-keys)
79 COPY_KEYS=false
80 shift
81 ;;
82 --tmpfs-size)
83 TMPFS_SIZE="$2"
84 shift 2
85 ;;
86 --skip-format)
87 SKIP_FORMAT=true
88 shift
89 ;;
90 *)
91 POSITIONAL+=("$1")
92 shift
93 ;;
94 esac
95done
96
97if [[ ${#POSITIONAL[@]} -lt 2 ]]; then
98 usage
99 exit 1
100fi
101
102HOSTNAME="${POSITIONAL[0]}"
103TARGET_IP="${POSITIONAL[1]}"
104
105SSH_USER="nixos"
106TARGET="${SSH_USER}@${TARGET_IP}"
107SSH_OPTS=(-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null)
108
109# SSH commands: use -t for interactive (LUKS passphrase), plain for scripted
110# shellcheck disable=SC2029 # $@ is intentionally expanded client-side
111ssh_cmd() { ssh "${SSH_OPTS[@]}" "${TARGET}" "$@"; }
112ssh_tty() { ssh "${SSH_OPTS[@]}" -t "${TARGET}" "$@"; }
113
114# --- Preflight checks ------------------------------------------------------
115
116info "Remote NixOS installation: ${HOSTNAME} → ${TARGET_IP}"
117echo ""
118
119# Check the host config exists
120if ! nix eval ".#nixosConfigurations.${HOSTNAME}" --apply 'x: true' &>/dev/null; then
121 error "No NixOS configuration found for '${HOSTNAME}'"
122 error "Available hosts:"
123 nix eval '.#nixosConfigurations' --apply 'x: builtins.attrNames x' 2>/dev/null
124 exit 1
125fi
126
127# Verify SSH connectivity
128info "Verifying SSH connectivity to ${TARGET}..."
129if ! ssh_cmd "echo 'ok'" &>/dev/null; then
130 error "Cannot connect to ${TARGET}"
131 error "Make sure:"
132 error " 1. Target is booted from NixOS live USB"
133 error " 2. You've run 'passwd nixos' on the target"
134 error " 3. IP address ${TARGET_IP} is correct"
135 exit 1
136fi
137success "SSH connection verified"
138
139# --- Step 1: Copy SSH keys -------------------------------------------------
140
141if [[ "${COPY_KEYS}" == true ]]; then
142 info "Copying SSH public key to target..."
143 ssh-copy-id "${SSH_OPTS[@]}" "${TARGET}" 2>/dev/null || true
144fi
145
146# --- Step 2: Build disko script locally ------------------------------------
147
148DISKO_LINK="/tmp/disko-${HOSTNAME}"
149
150if [[ "${SKIP_FORMAT}" == false ]]; then
151 info "Building disko partitioning script..."
152 nix build ".#nixosConfigurations.${HOSTNAME}.config.system.build.diskoScript" \
153 -o "${DISKO_LINK}"
154 DISKO_SCRIPT=$(readlink -f "${DISKO_LINK}")
155 success "Disko script: ${DISKO_SCRIPT}"
156fi
157
158# --- Step 3: Build system closure locally -----------------------------------
159
160info "Building system closure (this may take a while)..."
161nix build ".#nixosConfigurations.${HOSTNAME}.config.system.build.toplevel"
162SYSTEM_PATH=$(readlink -f result)
163CLOSURE_SIZE=$(nix path-info -Sh "${SYSTEM_PATH}" 2>/dev/null | awk '{print $2}')
164success "System closure: ${SYSTEM_PATH} (${CLOSURE_SIZE})"
165
166# --- Step 4: Format and mount the target disk -------------------------------
167
168if [[ "${SKIP_FORMAT}" == false ]]; then
169 echo ""
170 warn "This will ERASE the target disk on ${TARGET_IP}!"
171 warn "LUKS-encrypted hosts will prompt for a passphrase."
172 echo ""
173 read -p "Continue with disk formatting? [y/N] " -n 1 -r
174 echo ""
175 if [[ ! $REPLY =~ ^[Yy]$ ]]; then
176 error "Aborted."
177 exit 1
178 fi
179
180 info "Copying disko script to target..."
181 NIX_SSHOPTS="${SSH_OPTS[*]}" nix copy --to "ssh://${TARGET}" "${DISKO_SCRIPT}"
182
183 info "Running disko on target..."
184 ssh_tty "sudo ${DISKO_SCRIPT}"
185 success "Disk formatted and mounted"
186fi
187
188# --- Step 5: Resize tmpfs ---------------------------------------------------
189
190info "Resizing tmpfs on target to ${TMPFS_SIZE}..."
191ssh_cmd "sudo mount -o remount,size=${TMPFS_SIZE} /nix/.rw-store"
192success "tmpfs resized to ${TMPFS_SIZE}"
193
194# --- Step 6: Copy system closure to target ----------------------------------
195
196info "Copying system closure to target (${CLOSURE_SIZE})..."
197NIX_SSHOPTS="${SSH_OPTS[*]}" nix copy -v --to "ssh://${TARGET}" "${SYSTEM_PATH}"
198success "System closure copied"
199
200# --- Step 7: Install --------------------------------------------------------
201
202info "Running nixos-install on target..."
203ssh_tty "sudo nixos-install --root /mnt --system '${SYSTEM_PATH}' --no-channel-copy"
204
205info "Running system activation..."
206ssh_tty "sudo nixos-enter --root /mnt -- /run/current-system/activate"
207success "Installation complete!"
208
209echo ""
210info "You can now reboot the target:"
211echo " ssh ${SSH_OPTS[*]} ${TARGET} 'sudo reboot'"