616 lines
25 KiB
Nix
616 lines
25 KiB
Nix
# UltraCloud Bootstrap ISO
|
|
# Minimal ISO with DHCP + Phone Home to Deployer + Auto-Install
|
|
# For VM cluster deployment: boots, phones home, partitions disk, installs NixOS
|
|
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
modulesPath,
|
|
ultracloudBaremetalFormatMountPaths ? { },
|
|
ultracloudBaremetalSystemPaths ? { },
|
|
...
|
|
}:
|
|
|
|
{
|
|
imports = [
|
|
"${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix"
|
|
];
|
|
|
|
# ISO metadata
|
|
image.fileName = "ultracloud-bootstrap.iso";
|
|
isoImage = {
|
|
makeEfiBootable = true;
|
|
makeUsbBootable = true;
|
|
};
|
|
|
|
# Embed the repository into the ISO for offline flake install
|
|
isoImage.contents = [
|
|
{ source = ../../.; target = "/opt/ultracloud-src"; }
|
|
];
|
|
|
|
# Minimal network: DHCP on all interfaces
|
|
networking.useNetworkd = true;
|
|
networking.networkmanager.enable = lib.mkForce false;
|
|
systemd.network.networks."10-dhcp" = {
|
|
matchConfig.Name = "*";
|
|
DHCP = "yes";
|
|
};
|
|
|
|
# Phone Home service — fetches secrets from Deployer
|
|
systemd.services.ultracloud-bootstrap = {
|
|
description = "UltraCloud Bootstrap via Phone Home";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
};
|
|
|
|
script = ''
|
|
set -euo pipefail
|
|
|
|
cmdline_value() {
|
|
local key="$1"
|
|
local arg
|
|
for arg in $(cat /proc/cmdline); do
|
|
case "$arg" in
|
|
"$key"=*)
|
|
echo "''${arg#*=}"
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
dmi_value() {
|
|
local path="$1"
|
|
if [ -r "$path" ]; then
|
|
tr -d '\n' <"$path" 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
resolve_deployer_url() {
|
|
local explicit_url="''${DEPLOYER_URL:-}"
|
|
if [ -z "$explicit_url" ]; then
|
|
explicit_url="$(cmdline_value ultracloud.deployer_url || true)"
|
|
fi
|
|
if [ -n "$explicit_url" ]; then
|
|
echo "$explicit_url"
|
|
return 0
|
|
fi
|
|
if ${pkgs.curl}/bin/curl -fsS --connect-timeout 2 --max-time 5 \
|
|
http://10.0.2.2:8088/health >/dev/null 2>&1; then
|
|
echo "http://10.0.2.2:8088"
|
|
return 0
|
|
fi
|
|
echo "http://192.168.100.1:8080"
|
|
}
|
|
|
|
mkdir -p /etc/ultracloud
|
|
|
|
# Discover Deployer via environment, kernel cmdline, or fallback.
|
|
DEPLOYER_URL="$(resolve_deployer_url)"
|
|
|
|
# Get machine identity
|
|
MACHINE_ID=$(cat /etc/machine-id)
|
|
|
|
echo "UltraCloud Bootstrap starting..."
|
|
echo "Machine ID: $MACHINE_ID"
|
|
echo "Deployer URL: $DEPLOYER_URL"
|
|
|
|
# Optional bootstrap token (from file or environment)
|
|
TOKEN_FILE="/etc/ultracloud/bootstrap-token"
|
|
DEPLOYER_TOKEN=""
|
|
if [ -s "$TOKEN_FILE" ]; then
|
|
DEPLOYER_TOKEN=$(cat "$TOKEN_FILE")
|
|
elif [ -n "''${DEPLOYER_BOOTSTRAP_TOKEN:-}" ]; then
|
|
DEPLOYER_TOKEN="''${DEPLOYER_BOOTSTRAP_TOKEN}"
|
|
else
|
|
DEPLOYER_TOKEN="$(cmdline_value ultracloud.bootstrap_token || true)"
|
|
fi
|
|
|
|
DEPLOYER_CA_CERT_PATH="''${DEPLOYER_CA_CERT:-}"
|
|
if [ -z "$DEPLOYER_CA_CERT_PATH" ]; then
|
|
DEPLOYER_CA_CERT_URL="$(cmdline_value ultracloud.ca_cert_url || true)"
|
|
if [ -n "$DEPLOYER_CA_CERT_URL" ]; then
|
|
DEPLOYER_CA_CERT_PATH="/etc/ultracloud/bootstrap-ca.crt"
|
|
${pkgs.curl}/bin/curl -sfL --connect-timeout 5 --max-time 30 \
|
|
"$DEPLOYER_CA_CERT_URL" \
|
|
-o "$DEPLOYER_CA_CERT_PATH"
|
|
fi
|
|
fi
|
|
|
|
CURL_ARGS=(-sf --connect-timeout 5 --max-time 15)
|
|
if [ -n "$DEPLOYER_TOKEN" ]; then
|
|
CURL_ARGS+=(-H "X-Deployer-Token: $DEPLOYER_TOKEN")
|
|
fi
|
|
if [ -n "$DEPLOYER_CA_CERT_PATH" ] && [ -f "$DEPLOYER_CA_CERT_PATH" ]; then
|
|
CURL_ARGS+=(--cacert "$DEPLOYER_CA_CERT_PATH")
|
|
fi
|
|
|
|
NODE_IP=$(${pkgs.iproute2}/bin/ip -4 route get 1.1.1.1 2>/dev/null | ${pkgs.gawk}/bin/awk '{for(i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')
|
|
if [ -z "$NODE_IP" ]; then
|
|
NODE_IP=$(${pkgs.iproute2}/bin/ip -4 addr show scope global 2>/dev/null | ${pkgs.gawk}/bin/awk '/inet / {sub("/.*","",$2); print $2; exit}')
|
|
fi
|
|
if [ -z "$NODE_IP" ]; then
|
|
NODE_IP=$(hostname -I 2>/dev/null | ${pkgs.gawk}/bin/awk '{print $1}')
|
|
fi
|
|
REQUESTED_NODE_ID="''${ULTRACLOUD_NODE_ID:-}"
|
|
if [ -z "$REQUESTED_NODE_ID" ]; then
|
|
REQUESTED_NODE_ID="$(cmdline_value ultracloud.node_id || true)"
|
|
fi
|
|
if [ -z "$REQUESTED_NODE_ID" ]; then
|
|
REQUESTED_NODE_ID="$(dmi_value /sys/class/dmi/id/product_serial)"
|
|
fi
|
|
if [ -z "$REQUESTED_NODE_ID" ]; then
|
|
REQUESTED_NODE_ID="$(hostname)"
|
|
fi
|
|
REQUESTED_HOSTNAME="''${ULTRACLOUD_HOSTNAME:-}"
|
|
if [ -z "$REQUESTED_HOSTNAME" ]; then
|
|
REQUESTED_HOSTNAME="$(cmdline_value ultracloud.hostname || true)"
|
|
fi
|
|
if [ -z "$REQUESTED_HOSTNAME" ]; then
|
|
REQUESTED_HOSTNAME="$REQUESTED_NODE_ID"
|
|
fi
|
|
echo "ULTRACLOUD_MARKER pre-install.boot.$REQUESTED_NODE_ID"
|
|
CPU_MODEL=$(${pkgs.gawk}/bin/awk -F: '/model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}' /proc/cpuinfo 2>/dev/null || true)
|
|
CPU_CORES=$(${pkgs.gawk}/bin/awk '/^cpu cores/ {print $4; exit}' /proc/cpuinfo 2>/dev/null || true)
|
|
CPU_THREADS=$(${pkgs.coreutils}/bin/nproc --all 2>/dev/null || true)
|
|
MEMORY_KIB=$(${pkgs.gawk}/bin/awk '/MemTotal:/ {print $2; exit}' /proc/meminfo 2>/dev/null || true)
|
|
MEMORY_BYTES=""
|
|
if [ -n "$MEMORY_KIB" ]; then
|
|
MEMORY_BYTES=$((MEMORY_KIB * 1024))
|
|
fi
|
|
|
|
DISKS_JSON=$(${pkgs.util-linux}/bin/lsblk -J -b -o NAME,PATH,SIZE,MODEL,SERIAL,ROTA,TYPE 2>/dev/null | ${pkgs.jq}/bin/jq '
|
|
[.blockdevices[] | select(.type == "disk") | {
|
|
name: .name,
|
|
path: (.path // null),
|
|
size_bytes: (.size | tonumber?),
|
|
model: ((.model // "") | if . == "" then null else . end),
|
|
serial: ((.serial // "") | if . == "" then null else . end),
|
|
rotational: (if .rota == null then null else (.rota == 1) end)
|
|
}]
|
|
')
|
|
NICS_JSON=$(${pkgs.iproute2}/bin/ip -j link 2>/dev/null | ${pkgs.jq}/bin/jq '
|
|
[.[] | select(.ifname != "lo") | {
|
|
name: .ifname,
|
|
mac_address: ((.address // "") | if . == "" or . == "00:00:00:00:00:00" then null else . end),
|
|
oper_state: ((.operstate // "") | ascii_downcase | if . == "" then null else . end)
|
|
}]
|
|
')
|
|
DMI_VENDOR=$(tr -d '\n' </sys/class/dmi/id/sys_vendor 2>/dev/null || true)
|
|
DMI_PRODUCT=$(tr -d '\n' </sys/class/dmi/id/product_name 2>/dev/null || true)
|
|
DMI_SERIAL=$(tr -d '\n' </sys/class/dmi/id/product_serial 2>/dev/null || true)
|
|
HARDWARE_FACTS=$(${pkgs.jq}/bin/jq -n \
|
|
--arg architecture "$(${pkgs.coreutils}/bin/uname -m)" \
|
|
--arg cpu_model "$CPU_MODEL" \
|
|
--arg cpu_threads "$CPU_THREADS" \
|
|
--arg cpu_cores "$CPU_CORES" \
|
|
--arg memory_bytes "$MEMORY_BYTES" \
|
|
--arg dmi_vendor "$DMI_VENDOR" \
|
|
--arg dmi_product "$DMI_PRODUCT" \
|
|
--arg dmi_serial "$DMI_SERIAL" \
|
|
--argjson disks "$DISKS_JSON" \
|
|
--argjson nics "$NICS_JSON" '
|
|
{
|
|
architecture: (if $architecture == "" then null else $architecture end),
|
|
cpu_model: (if $cpu_model == "" then null else $cpu_model end),
|
|
cpu_threads: (if $cpu_threads == "" then null else ($cpu_threads | tonumber) end),
|
|
cpu_cores: (if $cpu_cores == "" then null else ($cpu_cores | tonumber) end),
|
|
memory_bytes: (if $memory_bytes == "" then null else ($memory_bytes | tonumber) end),
|
|
disks: $disks,
|
|
nics: $nics,
|
|
dmi: ({
|
|
vendor: (if $dmi_vendor == "" then null else $dmi_vendor end),
|
|
product_name: (if $dmi_product == "" then null else $dmi_product end),
|
|
serial_number: (if $dmi_serial == "" then null else $dmi_serial end)
|
|
} | with_entries(select(.value != null)))
|
|
}
|
|
| if (.dmi | length) == 0 then del(.dmi) else . end
|
|
')
|
|
REQUEST_JSON=$(${pkgs.jq}/bin/jq -n \
|
|
--arg machine_id "$MACHINE_ID" \
|
|
--arg node_id "$REQUESTED_NODE_ID" \
|
|
--arg hostname "$REQUESTED_HOSTNAME" \
|
|
--arg ip "$NODE_IP" \
|
|
--argjson hardware_facts "$HARDWARE_FACTS" '
|
|
{
|
|
machine_id: $machine_id,
|
|
node_id: $node_id,
|
|
hostname: $hostname,
|
|
ip: $ip,
|
|
hardware_facts: $hardware_facts
|
|
}
|
|
')
|
|
|
|
# Phone Home request with retry
|
|
for i in 1 2 3 4 5; do
|
|
echo "Attempt $i/5: Contacting Deployer..."
|
|
|
|
if RESPONSE=$(${pkgs.curl}/bin/curl "''${CURL_ARGS[@]}" -X POST \
|
|
-H "Content-Type: application/json" \
|
|
-d "$REQUEST_JSON" \
|
|
"$DEPLOYER_URL/api/v1/phone-home"); then
|
|
|
|
echo "✓ Phone Home successful"
|
|
|
|
# Create directories
|
|
mkdir -p /etc/ssh /etc/ultracloud /root/.ssh
|
|
|
|
# Validate success flag
|
|
SUCCESS=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r '.success // false' || echo "false")
|
|
if [ "$SUCCESS" != "true" ]; then
|
|
MESSAGE=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r '.message // empty' || true)
|
|
echo "✗ Phone Home rejected: $MESSAGE"
|
|
sleep $((2 ** i))
|
|
continue
|
|
fi
|
|
|
|
NODE_CONFIG=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -c '.node_config // empty' || true)
|
|
if [ -z "$NODE_CONFIG" ] || [ "$NODE_CONFIG" = "null" ]; then
|
|
echo "✗ Phone Home response missing node_config"
|
|
sleep $((2 ** i))
|
|
continue
|
|
fi
|
|
echo "$NODE_CONFIG" > /etc/ultracloud/node-config.json
|
|
echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r '
|
|
.node_config.bootstrap_secrets.ssh_authorized_keys[]?
|
|
' > /root/.ssh/authorized_keys
|
|
|
|
# Apply SSH host key if provided
|
|
SSH_HOST_KEY=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r '.node_config.bootstrap_secrets.ssh_host_key // empty')
|
|
if [ -n "$SSH_HOST_KEY" ]; then
|
|
umask 077
|
|
echo "$SSH_HOST_KEY" > /etc/ssh/ssh_host_ed25519_key
|
|
${pkgs.openssh}/bin/ssh-keygen -y -f /etc/ssh/ssh_host_ed25519_key > /etc/ssh/ssh_host_ed25519_key.pub
|
|
fi
|
|
|
|
# Apply TLS material if provided
|
|
TLS_CERT=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r '.node_config.bootstrap_secrets.tls_cert // empty')
|
|
TLS_KEY=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r '.node_config.bootstrap_secrets.tls_key // empty')
|
|
if [ -n "$TLS_CERT" ] && [ -n "$TLS_KEY" ]; then
|
|
umask 077
|
|
mkdir -p /etc/ultracloud/tls
|
|
echo "$TLS_CERT" > /etc/ultracloud/tls/node.crt
|
|
echo "$TLS_KEY" > /etc/ultracloud/tls/node.key
|
|
fi
|
|
|
|
# Generate host keys locally if missing
|
|
if [ ! -s /etc/ssh/ssh_host_ed25519_key ]; then
|
|
${pkgs.openssh}/bin/ssh-keygen -A
|
|
fi
|
|
|
|
# Set permissions
|
|
chmod 644 /etc/ultracloud/node-config.json 2>/dev/null || true
|
|
chmod 700 /root/.ssh 2>/dev/null || true
|
|
chmod 600 /root/.ssh/authorized_keys 2>/dev/null || true
|
|
chmod 600 /etc/ssh/ssh_host_ed25519_key 2>/dev/null || true
|
|
chmod 644 /etc/ssh/ssh_host_ed25519_key.pub 2>/dev/null || true
|
|
chmod 600 /etc/ultracloud/tls/node.key 2>/dev/null || true
|
|
chmod 644 /etc/ultracloud/tls/node.crt 2>/dev/null || true
|
|
|
|
# Signal success
|
|
NODE_ID=$(echo "$RESPONSE" | ${pkgs.jq}/bin/jq -r '.node_config.assignment.node_id // "unknown"')
|
|
echo "ULTRACLOUD_MARKER pre-install.phone-home.complete.$NODE_ID"
|
|
echo "✓ Bootstrap complete: $NODE_ID"
|
|
exit 0
|
|
else
|
|
echo "✗ Phone Home failed, attempt $i/5"
|
|
sleep $((2 ** i))
|
|
fi
|
|
done
|
|
|
|
echo "✗ Bootstrap failed after 5 attempts"
|
|
exit 1
|
|
'';
|
|
};
|
|
|
|
# Auto-install service - partitions disk and runs nixos-install
|
|
systemd.services.ultracloud-install = {
|
|
description = "UltraCloud Auto-Install to Disk";
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "ultracloud-bootstrap.service" ];
|
|
requires = [ "ultracloud-bootstrap.service" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
StandardOutput = "journal+console";
|
|
StandardError = "journal+console";
|
|
};
|
|
|
|
script = ''
|
|
set -euo pipefail
|
|
export PATH="${pkgs.nix}/bin:${config.system.build.nixos-install}/bin:$PATH"
|
|
|
|
cmdline_value() {
|
|
local key="$1"
|
|
local arg
|
|
for arg in $(cat /proc/cmdline); do
|
|
case "$arg" in
|
|
"$key"=*)
|
|
echo "''${arg#*=}"
|
|
return 0
|
|
;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
resolve_deployer_url() {
|
|
local explicit_url="''${DEPLOYER_URL:-}"
|
|
if [ -z "$explicit_url" ]; then
|
|
explicit_url="$(cmdline_value ultracloud.deployer_url || true)"
|
|
fi
|
|
if [ -n "$explicit_url" ]; then
|
|
echo "$explicit_url"
|
|
return 0
|
|
fi
|
|
if ${pkgs.curl}/bin/curl -fsS --connect-timeout 2 --max-time 5 \
|
|
http://10.0.2.2:8088/health >/dev/null 2>&1; then
|
|
echo "http://10.0.2.2:8088"
|
|
return 0
|
|
fi
|
|
echo "http://192.168.100.1:8080"
|
|
}
|
|
|
|
resolve_binary_cache_url() {
|
|
local explicit_url="''${ULTRACLOUD_BINARY_CACHE_URL:-}"
|
|
if [ -z "$explicit_url" ]; then
|
|
explicit_url="$(cmdline_value ultracloud.binary_cache_url || true)"
|
|
fi
|
|
if [ -n "$explicit_url" ]; then
|
|
echo "$explicit_url"
|
|
return 0
|
|
fi
|
|
if ${pkgs.curl}/bin/curl -fsS --connect-timeout 2 --max-time 5 \
|
|
http://10.0.2.2:8090/nix-cache-info >/dev/null 2>&1; then
|
|
echo "http://10.0.2.2:8090"
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
if [ ! -s /etc/ultracloud/node-config.json ]; then
|
|
echo "ERROR: node-config.json missing (bootstrap not complete?)"
|
|
exit 1
|
|
fi
|
|
|
|
NODE_ID=$(${pkgs.jq}/bin/jq -r '.assignment.hostname // .assignment.node_id // empty' /etc/ultracloud/node-config.json)
|
|
NODE_IP=$(${pkgs.jq}/bin/jq -r '.assignment.ip // empty' /etc/ultracloud/node-config.json)
|
|
NIXOS_CONFIGURATION=$(${pkgs.jq}/bin/jq -r '.bootstrap_plan.install_plan.nixos_configuration // .assignment.hostname // empty' /etc/ultracloud/node-config.json)
|
|
INSTALL_PLAN_DISKO_CONFIG_PATH=$(${pkgs.jq}/bin/jq -r '.bootstrap_plan.install_plan.disko_config_path // empty' /etc/ultracloud/node-config.json)
|
|
DISKO_SCRIPT_PATH=$(${pkgs.jq}/bin/jq -r '.bootstrap_plan.install_plan.disko_script_path // empty' /etc/ultracloud/node-config.json)
|
|
if [ -z "$DISKO_SCRIPT_PATH" ] && [ -r /etc/ultracloud/disko-script-paths.json ]; then
|
|
DISKO_SCRIPT_PATH=$(${pkgs.jq}/bin/jq -r --arg cfg "$NIXOS_CONFIGURATION" '.[$cfg] // empty' /etc/ultracloud/disko-script-paths.json)
|
|
if [ -n "$DISKO_SCRIPT_PATH" ]; then
|
|
echo "Resolved pre-built Disko script for install profile $NIXOS_CONFIGURATION from the ISO profile map"
|
|
fi
|
|
fi
|
|
TARGET_SYSTEM_PATH=$(${pkgs.jq}/bin/jq -r '.bootstrap_plan.install_plan.target_system_path // empty' /etc/ultracloud/node-config.json)
|
|
if [ -z "$TARGET_SYSTEM_PATH" ] && [ -r /etc/ultracloud/system-paths.json ]; then
|
|
TARGET_SYSTEM_PATH=$(${pkgs.jq}/bin/jq -r --arg cfg "$NIXOS_CONFIGURATION" '.[$cfg] // empty' /etc/ultracloud/system-paths.json)
|
|
if [ -n "$TARGET_SYSTEM_PATH" ]; then
|
|
echo "Resolved pre-built target system for install profile $NIXOS_CONFIGURATION from the ISO profile map"
|
|
fi
|
|
fi
|
|
TARGET_DISK=$(${pkgs.jq}/bin/jq -r '.bootstrap_plan.install_plan.target_disk // empty' /etc/ultracloud/node-config.json)
|
|
TARGET_DISK_BY_ID=$(${pkgs.jq}/bin/jq -r '.bootstrap_plan.install_plan.target_disk_by_id // empty' /etc/ultracloud/node-config.json)
|
|
DEPLOYER_URL="$(resolve_deployer_url)"
|
|
SRC_ROOT="/opt/ultracloud-src"
|
|
|
|
if [ -z "$NODE_ID" ] || [ -z "$NODE_IP" ]; then
|
|
echo "ERROR: node-config.json missing hostname/ip"
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$NIXOS_CONFIGURATION" ]; then
|
|
echo "ERROR: node-config.json missing install_plan.nixos_configuration"
|
|
exit 1
|
|
fi
|
|
|
|
TOKEN_FILE="/etc/ultracloud/bootstrap-token"
|
|
DEPLOYER_TOKEN=""
|
|
if [ -s "$TOKEN_FILE" ]; then
|
|
DEPLOYER_TOKEN=$(cat "$TOKEN_FILE")
|
|
elif [ -n "''${DEPLOYER_BOOTSTRAP_TOKEN:-}" ]; then
|
|
DEPLOYER_TOKEN="''${DEPLOYER_BOOTSTRAP_TOKEN}"
|
|
else
|
|
DEPLOYER_TOKEN="$(cmdline_value ultracloud.bootstrap_token || true)"
|
|
fi
|
|
|
|
DEPLOYER_CA_CERT_PATH="''${DEPLOYER_CA_CERT:-}"
|
|
if [ -z "$DEPLOYER_CA_CERT_PATH" ]; then
|
|
DEPLOYER_CA_CERT_URL="$(cmdline_value ultracloud.ca_cert_url || true)"
|
|
if [ -n "$DEPLOYER_CA_CERT_URL" ]; then
|
|
DEPLOYER_CA_CERT_PATH="/etc/ultracloud/bootstrap-ca.crt"
|
|
${pkgs.curl}/bin/curl -sfL --connect-timeout 5 --max-time 30 \
|
|
"$DEPLOYER_CA_CERT_URL" \
|
|
-o "$DEPLOYER_CA_CERT_PATH"
|
|
fi
|
|
fi
|
|
|
|
CURL_ARGS=(-sfL --connect-timeout 5 --max-time 120)
|
|
if [ -n "$DEPLOYER_TOKEN" ]; then
|
|
CURL_ARGS+=(-H "X-Deployer-Token: $DEPLOYER_TOKEN")
|
|
fi
|
|
if [ -n "$DEPLOYER_CA_CERT_PATH" ] && [ -f "$DEPLOYER_CA_CERT_PATH" ]; then
|
|
CURL_ARGS+=(--cacert "$DEPLOYER_CA_CERT_PATH")
|
|
fi
|
|
|
|
BUNDLE_PATH="/run/ultracloud/flake-bundle.tar.gz"
|
|
mkdir -p /run/ultracloud
|
|
if ${pkgs.curl}/bin/curl "''${CURL_ARGS[@]}" \
|
|
"$DEPLOYER_URL/api/v1/bootstrap/flake-bundle" \
|
|
-o "$BUNDLE_PATH"; then
|
|
echo "Downloaded bootstrap flake bundle from deployer"
|
|
echo "ULTRACLOUD_MARKER install.bundle-downloaded.$NODE_ID"
|
|
rm -rf "$SRC_ROOT"
|
|
mkdir -p "$SRC_ROOT"
|
|
${pkgs.gzip}/bin/gzip -dc "$BUNDLE_PATH" | ${pkgs.gnutar}/bin/tar -xf - -C "$SRC_ROOT"
|
|
else
|
|
echo "No deployer flake bundle available; using embedded source tree"
|
|
fi
|
|
|
|
echo "ULTRACLOUD_MARKER install.start.$NODE_ID"
|
|
DISPLAY_TARGET_DISK="$TARGET_DISK"
|
|
DISK_SELECTOR_SOURCE="auto-discovery"
|
|
if [ -n "$TARGET_DISK_BY_ID" ]; then
|
|
DISPLAY_TARGET_DISK="$TARGET_DISK_BY_ID"
|
|
DISK_SELECTOR_SOURCE="install_plan.target_disk_by_id"
|
|
elif [ -n "$TARGET_DISK" ]; then
|
|
DISK_SELECTOR_SOURCE="install_plan.target_disk"
|
|
fi
|
|
echo "UltraCloud install starting for $NODE_ID (ip=$NODE_IP, nixos_configuration=$NIXOS_CONFIGURATION, target_disk=$DISPLAY_TARGET_DISK)"
|
|
if [ -n "$INSTALL_PLAN_DISKO_CONFIG_PATH" ]; then
|
|
echo "Install contract: disko_config_path=$INSTALL_PLAN_DISKO_CONFIG_PATH, disk_selector_source=$DISK_SELECTOR_SOURCE"
|
|
else
|
|
echo "Install contract: disko_config_path=(embedded profile map for $NIXOS_CONFIGURATION), disk_selector_source=$DISK_SELECTOR_SOURCE"
|
|
fi
|
|
|
|
# Resolve installation target disk.
|
|
if [ -n "$TARGET_DISK_BY_ID" ]; then
|
|
if [ ! -b "$TARGET_DISK_BY_ID" ]; then
|
|
echo "ERROR: target_disk_by_id does not exist: $TARGET_DISK_BY_ID"
|
|
exit 1
|
|
fi
|
|
DISK="$TARGET_DISK_BY_ID"
|
|
elif [ -n "$TARGET_DISK" ]; then
|
|
if [ ! -b "$TARGET_DISK" ]; then
|
|
echo "ERROR: target_disk does not exist: $TARGET_DISK"
|
|
exit 1
|
|
fi
|
|
DISK="$TARGET_DISK"
|
|
else
|
|
DISK=$(${pkgs.util-linux}/bin/lsblk -dpno NAME,TYPE | ${pkgs.gawk}/bin/awk '$2=="disk"{print $1; exit}')
|
|
fi
|
|
if [ -z "$DISK" ]; then
|
|
echo "ERROR: No disk found"
|
|
exit 1
|
|
fi
|
|
|
|
ROOT_PART=$(${pkgs.util-linux}/bin/lsblk -lnpo NAME,TYPE "$DISK" 2>/dev/null | ${pkgs.gawk}/bin/awk '$2=="part"{print $1}' | sed -n '2p')
|
|
mkdir -p /mnt
|
|
|
|
# Skip if already installed
|
|
if [ -n "$ROOT_PART" ] && ${pkgs.util-linux}/bin/lsblk -no FSTYPE "$ROOT_PART" 2>/dev/null | ${pkgs.gnugrep}/bin/grep -q '^ext4$'; then
|
|
mount "$ROOT_PART" /mnt 2>/dev/null || true
|
|
if [ -e /mnt/etc/NIXOS ]; then
|
|
echo "✓ Existing NixOS detected; skipping install"
|
|
umount /mnt || true
|
|
exit 0
|
|
fi
|
|
umount /mnt || true
|
|
fi
|
|
|
|
NIX_CONFIG_LINES=(
|
|
"experimental-features = nix-command flakes"
|
|
)
|
|
BINARY_CACHE_URL="$(resolve_binary_cache_url || true)"
|
|
NIXPKGS_PATH="$SRC_ROOT/.bundle-inputs/nixpkgs"
|
|
if [ ! -e "$NIXPKGS_PATH" ]; then
|
|
NIXPKGS_PATH="${pkgs.path}"
|
|
fi
|
|
if [ -n "$BINARY_CACHE_URL" ]; then
|
|
echo "Using bootstrap binary cache: $BINARY_CACHE_URL"
|
|
NIX_CONFIG_LINES+=("substituters = $BINARY_CACHE_URL")
|
|
NIX_CONFIG_LINES+=("trusted-substituters = $BINARY_CACHE_URL")
|
|
NIX_CONFIG_LINES+=("require-sigs = false")
|
|
fi
|
|
export NIX_CONFIG="$(printf '%s\n' "''${NIX_CONFIG_LINES[@]}")"
|
|
export NIX_PATH="nixpkgs=$NIXPKGS_PATH"
|
|
|
|
echo "Preparing NixOS installation for configuration $NIXOS_CONFIGURATION"
|
|
|
|
echo "Running disko to partition $DISK..."
|
|
if [ -n "$DISKO_SCRIPT_PATH" ]; then
|
|
echo "Realising pre-built Disko script: $DISKO_SCRIPT_PATH"
|
|
${pkgs.nix}/bin/nix-store --realise "$DISKO_SCRIPT_PATH" >/dev/null
|
|
"$DISKO_SCRIPT_PATH/bin/disko-format-mount"
|
|
else
|
|
${pkgs.disko}/bin/disko \
|
|
--mode destroy,format,mount \
|
|
--yes-wipe-all-disks \
|
|
--root-mountpoint /mnt \
|
|
--flake "$SRC_ROOT#$NIXOS_CONFIGURATION"
|
|
fi
|
|
echo "ULTRACLOUD_MARKER install.disko.complete.$NODE_ID"
|
|
|
|
echo "Running nixos-install..."
|
|
if [ -n "$TARGET_SYSTEM_PATH" ]; then
|
|
echo "Realising pre-built target system: $TARGET_SYSTEM_PATH"
|
|
${pkgs.nix}/bin/nix-store --realise "$TARGET_SYSTEM_PATH" >/dev/null
|
|
${config.system.build.nixos-install}/bin/nixos-install \
|
|
--system "$TARGET_SYSTEM_PATH" \
|
|
--no-root-passwd \
|
|
--no-channel-copy
|
|
else
|
|
${config.system.build.nixos-install}/bin/nixos-install \
|
|
--flake "$SRC_ROOT#$NIXOS_CONFIGURATION" \
|
|
--no-root-passwd \
|
|
--no-channel-copy
|
|
fi
|
|
echo "ULTRACLOUD_MARKER install.nixos-install.complete.$NODE_ID"
|
|
|
|
mkdir -p /mnt/etc/ssh /mnt/etc/ultracloud /mnt/root/.ssh /mnt/var/lib
|
|
cp -f /etc/ultracloud/node-config.json /mnt/etc/ultracloud/node-config.json
|
|
cp -f /root/.ssh/authorized_keys /mnt/root/.ssh/authorized_keys
|
|
shopt -s nullglob
|
|
for host_key in /etc/ssh/ssh_host_*; do
|
|
cp -f "$host_key" /mnt/etc/ssh/"$(basename "$host_key")"
|
|
done
|
|
shopt -u nullglob
|
|
chmod 700 /mnt/root/.ssh
|
|
chmod 600 /mnt/root/.ssh/authorized_keys
|
|
chmod 600 /mnt/etc/ssh/ssh_host_*_key 2>/dev/null || true
|
|
chmod 644 /mnt/etc/ssh/ssh_host_*_key.pub 2>/dev/null || true
|
|
|
|
rm -rf /mnt/var/lib/photon-src
|
|
cp -a "$SRC_ROOT" /mnt/var/lib/photon-src
|
|
|
|
sync
|
|
echo "ULTRACLOUD_MARKER reboot.$NODE_ID"
|
|
echo "Allowing the harness to observe the reboot marker before shutting down..."
|
|
sleep 15
|
|
echo "✓ Install complete; rebooting..."
|
|
${pkgs.systemd}/bin/systemctl reboot
|
|
'';
|
|
};
|
|
|
|
# Packages for bootstrap + install
|
|
environment.etc."ultracloud/disko-script-paths.json".text =
|
|
builtins.toJSON ultracloudBaremetalFormatMountPaths;
|
|
environment.etc."ultracloud/system-paths.json".text =
|
|
builtins.toJSON ultracloudBaremetalSystemPaths;
|
|
|
|
environment.systemPackages = with pkgs; [
|
|
curl
|
|
jq
|
|
vim
|
|
htop
|
|
nix
|
|
gawk
|
|
gnugrep
|
|
util-linux
|
|
parted
|
|
dosfstools
|
|
e2fsprogs
|
|
disko
|
|
gnutar
|
|
gzip
|
|
];
|
|
|
|
# SSH with key-based auth for non-interactive access
|
|
services.openssh = {
|
|
enable = true;
|
|
settings.PermitRootLogin = "prohibit-password";
|
|
};
|
|
|
|
# SSH access keys are provisioned dynamically via phone-home
|
|
users.users.root.openssh.authorizedKeys.keys = [ ];
|
|
}
|