#!/usr/bin/env bash # Deploy CM4 eMMC provisioning to a Proxmox host: host scripts, udev, systemd units, # LXC container (dashboard), usbboot (rpiboot), and PiShrink. Uses hostname "cm4-provisioning" # to find the container on redeploy; creates with next available ID if not found. # # With host internet: installs usbboot and PiShrink so USB flash/backup and dashboard # Shrink/Compress work. The only manual step left is to add a golden image for Deploy. # # Usage: ./deploy-to-proxmox.sh [proxmox_host] # Example: ./deploy-to-proxmox.sh root@10.20.30.152 # # Optional env: # DEPLOY_ROOTFS_STORAGE=name — LXC rootfs storage (default: auto-detect: local-lvm, local, local-zfs, or first active) # CM4_BACKUPS_HOST_PATH=/path — host dir for backups; bind-mounted into LXC # DEPLOY_LXC_ROOT_PASSWORD=secret — set root password in LXC and enable SSH # DEPLOY_LXC_SSH_KEY=/path/to/pub — copy this key to LXC root (default: ~/.ssh/id_ed25519.pub or id_rsa.pub) # DEPLOY_LOG=1 — also log to deploy-YYYYMMDD-HHMMSS.log # # Requires: ssh key access to root@. For full install (usbboot, PiShrink), host needs internet. set -e PROXMOX="${1:-root@10.130.60.224}" ROOTFS_STORAGE="${DEPLOY_ROOTFS_STORAGE:-local-lvm}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(dirname "$SCRIPT_DIR")" LOG_FILE="" if [[ -n "${DEPLOY_LOG:-}" ]]; then LOG_FILE="$SCRIPT_DIR/deploy-$(date +%Y%m%d-%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "[$(date -Iseconds)] Logging to $LOG_FILE" fi log() { echo "[$(date -Iseconds)] $*"; } # Optional: gather SSH key and LXC root password for setup inside deploy DEPLOY_SSH_KEY_B64="" DEPLOY_LXC_PWD_B64="" if [[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" ]]; then DEPLOY_LXC_PWD_B64=$(echo -n "$DEPLOY_LXC_ROOT_PASSWORD" | base64 -w 0 2>/dev/null || base64 2>/dev/null | tr -d '\n') fi KEY_FILE="${DEPLOY_LXC_SSH_KEY:-}" if [[ -z "$KEY_FILE" ]]; then for f in ~/.ssh/id_ed25519.pub ~/.ssh/id_rsa.pub; do [[ -f "$f" ]] && { KEY_FILE="$f"; break; } done fi if [[ -n "$KEY_FILE" && -f "$KEY_FILE" ]]; then DEPLOY_SSH_KEY_B64=$(base64 -w 0 < "$KEY_FILE" 2>/dev/null || base64 < "$KEY_FILE" 2>/dev/null | tr -d '\n') log "Will copy SSH key to LXC: $KEY_FILE" fi [[ -n "$DEPLOY_LXC_ROOT_PASSWORD" ]] && log "Will set LXC root password (DEPLOY_LXC_ROOT_PASSWORD)." log "Deploying to $PROXMOX ..." log "[1/5] Cleaning remote staging dir ..." ssh "$PROXMOX" "rm -rf /tmp/emmc-provisioning-deploy" log "[2/5] Rsync repo to $PROXMOX ..." rsync -a "$REPO_DIR/" "$PROXMOX:/tmp/emmc-provisioning-deploy/" --exclude='.git' --exclude='scripts/deploy-to-proxmox.sh' --exclude='scripts/deploy-*.log' log "[3/5] Running remote install (host + LXC) ..." # Pass optional LXC SSH vars (base64) so remote can set password and add key ssh "$PROXMOX" "ROOTFS_STORAGE='$ROOTFS_STORAGE' CM4_BACKUPS_HOST_PATH='${CM4_BACKUPS_HOST_PATH:-}' DEPLOY_SSH_KEY_B64='${DEPLOY_SSH_KEY_B64:-}' DEPLOY_LXC_PWD_B64='${DEPLOY_LXC_PWD_B64:-}'" bash -s << 'REMOTE' set -e DEPLOY=/tmp/emmc-provisioning-deploy BACKUPS_HOST_PATH="${CM4_BACKUPS_HOST_PATH:-}" LXC_HOSTNAME="cm4-provisioning" log() { echo "[$(date -Iseconds)] $*"; } # --- Auto-select LXC rootfs storage if not set or not available --- _storage_exists() { pvesm status 2>/dev/null | awk -v s="$1" 'NR>1 && $1==s && $3=="active" {exit(0)} END{exit(1)}'; } if [[ -n "${ROOTFS_STORAGE:-}" ]] && _storage_exists "$ROOTFS_STORAGE"; then : # use provided and existing storage else ROOTFS_STORAGE="" for cand in local-lvm local local-zfs; do if _storage_exists "$cand"; then ROOTFS_STORAGE="$cand" break fi done if [[ -z "$ROOTFS_STORAGE" ]]; then ROOTFS_STORAGE=$(pvesm status 2>/dev/null | awk 'NR>1 && $3=="active" {print $1; exit}') fi [[ -z "$ROOTFS_STORAGE" ]] && { log "Error: no Proxmox storage found. Run: pvesm status"; exit 1; } log "Using storage: $ROOTFS_STORAGE" fi # --- Find existing LXC by hostname or use next available ID --- CTID="" for id in $(pct list 2>/dev/null | awk 'NR>1 {print $1}'); do h=$(pct config "$id" 2>/dev/null | sed -n 's/^hostname: *//p') if [[ "$h" == "$LXC_HOSTNAME" ]]; then CTID="$id" break fi done if [[ -n "$CTID" ]]; then log "Found existing LXC $CTID (hostname: $LXC_HOSTNAME)." else MAX_ID=$(pct list 2>/dev/null | awk 'NR>1 {print $1}' | sort -n | tail -1) [[ -z "$MAX_ID" ]] && MAX_ID=0 CTID=$((MAX_ID + 1)) log "Creating LXC $CTID ($LXC_HOSTNAME) (rootfs on ${ROOTFS_STORAGE})..." VZTMPL_DIR=/var/lib/vz/template/cache DEBIAN12_TMPL=$(ls "$VZTMPL_DIR"/debian-12-standard_*.tar.zst 2>/dev/null | head -1) if [[ -z "$DEBIAN12_TMPL" ]]; then log "Downloading Debian 12 LXC template..." pveam download local debian-12-standard_12.12-1_amd64.tar.zst || pveam download local debian-12-standard_12.7-1_amd64.tar.zst DEBIAN12_TMPL=$(ls "$VZTMPL_DIR"/debian-12-standard_*.tar.zst 2>/dev/null | head -1) fi [[ -z "$DEBIAN12_TMPL" ]] && { log "Error: no Debian 12 template found"; exit 1; } TMPL_NAME=$(basename "$DEBIAN12_TMPL") pct create "$CTID" "local:vztmpl/${TMPL_NAME}" \ --hostname "$LXC_HOSTNAME" --memory 1024 --swap 0 --cores 1 \ --rootfs "${ROOTFS_STORAGE}:8" --net0 name=eth0,bridge=vmbr0,ip=dhcp \ --unprivileged 0 --features nesting=1 -tag cm4-provisioning mkdir -p /var/lib/cm4-provisioning pct set "$CTID" -mp0 /var/lib/cm4-provisioning,mp=/var/lib/cm4-provisioning log "LXC $CTID created and mount configured." fi # Optional: bind-mount host directory for backup images if [[ -n "$BACKUPS_HOST_PATH" ]]; then mkdir -p "$BACKUPS_HOST_PATH" pct stop "$CTID" 2>/dev/null || true pct set "$CTID" -mp1 "$BACKUPS_HOST_PATH",mp=/var/lib/cm4-provisioning/backups pct start "$CTID" 2>/dev/null || true log "Backups mount: host $BACKUPS_HOST_PATH -> LXC $CTID /var/lib/cm4-provisioning/backups" fi # --- Host: scripts, systemd, udev --- log "Host: installing scripts and systemd units ..." mkdir -p /opt/cm4-provisioning /etc/cm4-provisioning cp "$DEPLOY/host/flash-emmc-on-connect.sh" /opt/cm4-provisioning/ chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh cp "$DEPLOY/host/build-cloudinit-image.sh" /opt/cm4-provisioning/ chmod +x /opt/cm4-provisioning/build-cloudinit-image.sh cp "$DEPLOY/host/run-shrink-on-host.sh" /opt/cm4-provisioning/ chmod +x /opt/cm4-provisioning/run-shrink-on-host.sh cp "$DEPLOY/scripts/fix-gadget-bootcode-on-host.sh" /opt/cm4-provisioning/ 2>/dev/null && chmod +x /opt/cm4-provisioning/fix-gadget-bootcode-on-host.sh || true cp "$DEPLOY/host/cm4-flash-trigger.sh" /usr/local/bin/ chmod +x /usr/local/bin/cm4-flash-trigger.sh cp "$DEPLOY/host/cm4-flash.service" /etc/systemd/system/ cp "$DEPLOY/host/cm4-build-cloudinit.path" /etc/systemd/system/ cp "$DEPLOY/host/cm4-build-cloudinit.service" /etc/systemd/system/ cp "$DEPLOY/host/cm4-shrink.path" /etc/systemd/system/ cp "$DEPLOY/host/cm4-shrink.service" /etc/systemd/system/ systemctl daemon-reload systemctl enable --now cm4-build-cloudinit.path 2>/dev/null || true systemctl enable --now cm4-shrink.path 2>/dev/null || true cp "$DEPLOY/host/89-cm4-boot-mode-permissions.rules" /etc/udev/rules.d/ 2>/dev/null || true cp "$DEPLOY/host/90-cm4-boot-mode.rules" /etc/udev/rules.d/ udevadm control --reload-rules 2>/dev/null || true log "Host: env and dirs ..." cat > /opt/cm4-provisioning/env << 'ENV' GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img RPIBOOT_DIR=/opt/usbboot EMMC_SIZE_BYTES=8589934592 ENV [[ -n "$BACKUPS_HOST_PATH" ]] && echo "BACKUPS_DIR=$BACKUPS_HOST_PATH" >> /opt/cm4-provisioning/env touch /etc/cm4-provisioning/enabled mkdir -p /var/lib/cm4-provisioning/backups [[ -n "$BACKUPS_HOST_PATH" ]] && mkdir -p "$BACKUPS_HOST_PATH" # --- Host: install usbboot (rpiboot) so USB flash/backup works --- log "Host: installing usbboot (rpiboot)..." if bash "$DEPLOY/scripts/install-usbboot-on-host.sh" 2>&1; then log "Host: usbboot installed at /opt/usbboot/rpiboot" else log "Warning: usbboot install failed (e.g. no internet). USB flash/backup will not work until you run: bash /tmp/emmc-provisioning-deploy/scripts/install-usbboot-on-host.sh" fi # --- Host: install PiShrink so dashboard Shrink/Compress work --- log "Host: installing PiShrink..." if bash "$DEPLOY/scripts/install-pishrink-on-host.sh" 2>&1; then log "Host: PiShrink installed" grep -q "SHRINK_BACKUP" /opt/cm4-provisioning/env || echo "SHRINK_BACKUP=1" >> /opt/cm4-provisioning/env grep -q "PISHRINK_COMPRESS" /opt/cm4-provisioning/env || echo "PISHRINK_COMPRESS=xz" >> /opt/cm4-provisioning/env else log "Warning: PiShrink install failed (e.g. no internet). Dashboard Shrink/Compress will report 'PiShrink not installed' until you run: bash /tmp/emmc-provisioning-deploy/scripts/install-pishrink-on-host.sh" fi # --- Start LXC if stopped --- log "Starting LXC $CTID if stopped ..." pct start "$CTID" 2>/dev/null || true # --- LXC: flash scripts (for reference; actual flash runs on host) --- log "LXC: installing flash scripts ..." pct exec "$CTID" -- mkdir -p /opt/cm4-provisioning /etc/cm4-provisioning pct push "$CTID" "$DEPLOY/host/flash-emmc-on-connect.sh" /opt/cm4-provisioning/flash-emmc-on-connect.sh pct exec "$CTID" -- chmod +x /opt/cm4-provisioning/flash-emmc-on-connect.sh pct push "$CTID" "$DEPLOY/host/cm4-flash-trigger.sh" /usr/local/bin/cm4-flash-trigger.sh pct exec "$CTID" -- chmod +x /usr/local/bin/cm4-flash-trigger.sh pct exec "$CTID" -- bash -c 'echo -e "GOLDEN_IMAGE=/var/lib/cm4-provisioning/golden.img\nRPIBOOT_DIR=/opt/usbboot\nEMMC_SIZE_BYTES=8589934592" > /opt/cm4-provisioning/env' # --- LXC: dashboard (all files) --- log "LXC: installing dashboard ..." pct exec "$CTID" -- mkdir -p /opt/cm4-provisioning/dashboard/templates pct push "$CTID" "$DEPLOY/dashboard/app.py" /opt/cm4-provisioning/dashboard/app.py pct push "$CTID" "$DEPLOY/dashboard/templates/index.html" /opt/cm4-provisioning/dashboard/templates/index.html pct push "$CTID" "$DEPLOY/dashboard/cm4-dashboard.service" /opt/cm4-provisioning/dashboard/cm4-dashboard.service # --- LXC: Flask and systemd --- log "LXC: installing python3-flask and enabling cm4-dashboard ..." pct exec "$CTID" -- bash -c 'apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3-flask' pct exec "$CTID" -- cp /opt/cm4-provisioning/dashboard/cm4-dashboard.service /etc/systemd/system/ pct exec "$CTID" -- systemctl daemon-reload pct exec "$CTID" -- systemctl enable --now cm4-dashboard log "LXC: cm4-dashboard enabled and started." # --- LXC: optional SSH (root password + SSH key from deploy env) --- if [[ -n "${DEPLOY_SSH_KEY_B64:-}" ]] || [[ -n "${DEPLOY_LXC_PWD_B64:-}" ]]; then log "LXC: configuring SSH (root login, password, authorized_keys)..." pct exec "$CTID" -- bash -c 'apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq openssh-server 2>/dev/null; systemctl enable ssh 2>/dev/null; systemctl start ssh 2>/dev/null' || true pct exec "$CTID" -- bash -c 'sed -i "s/^#*PermitRootLogin.*/PermitRootLogin yes/" /etc/ssh/sshd_config 2>/dev/null; grep -q "^PermitRootLogin" /etc/ssh/sshd_config || echo "PermitRootLogin yes" >> /etc/ssh/sshd_config; systemctl restart ssh 2>/dev/null || systemctl restart sshd 2>/dev/null' || true if [[ -n "${DEPLOY_LXC_PWD_B64:-}" ]]; then PWD_RAW=$(echo "$DEPLOY_LXC_PWD_B64" | base64 -d 2>/dev/null) echo "root:$PWD_RAW" | pct exec "$CTID" -- chpasswd 2>/dev/null && log "LXC: root password set." || true fi if [[ -n "${DEPLOY_SSH_KEY_B64:-}" ]]; then echo "$DEPLOY_SSH_KEY_B64" | base64 -d 2>/dev/null | pct exec "$CTID" -- bash -c "mkdir -p /root/.ssh; chmod 700 /root/.ssh; cat >> /root/.ssh/authorized_keys; chmod 600 /root/.ssh/authorized_keys" 2>/dev/null && log "LXC: SSH key added to /root/.ssh/authorized_keys." || true fi LXC_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') [[ -n "$LXC_IP" ]] && echo "LXC SSH: ssh root@$LXC_IP" fi log "Deploy done (remote). LXC ID: $CTID" REMOTE log "[5/5] Deploy finished." echo "" echo "=== Deploy complete ===" echo "Host and LXC are fully set up: usbboot (rpiboot), PiShrink, dashboard, systemd, udev." echo "" echo "--- Only remaining step (manual) ---" echo " Add a golden image for Deploy (writing image to device):" echo " • Dashboard: open http://:5000 → Build cloud-init image → then Set as golden" echo " • Or copy your image: scp your-image.img $PROXMOX:/var/lib/cm4-provisioning/golden.img" echo " Backup (read from device) works without golden.img." echo "" echo "--- You have ---" echo " - Dashboard: http://:5000 (LXC IP: from Proxmox UI, or on host: pct exec -- hostname -I)" [[ -n "${DEPLOY_LXC_ROOT_PASSWORD:-}" || -n "${DEPLOY_SSH_KEY_B64:-}" ]] && echo " - LXC SSH: ssh root@ (password and/or key were set)" [[ -n "${CM4_BACKUPS_HOST_PATH:-}" ]] && echo " - Backups on host: $CM4_BACKUPS_HOST_PATH" if [[ -n "$LOG_FILE" ]]; then echo " - Log: $LOG_FILE" fi