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'"