diff --git a/migrate-npm-to-proxmox-lxc.sh b/migrate-npm-to-proxmox-lxc.sh index 5b7a3fd..3516aff 100644 --- a/migrate-npm-to-proxmox-lxc.sh +++ b/migrate-npm-to-proxmox-lxc.sh @@ -4,12 +4,16 @@ # # Migrates NGINX Proxy Manager from HAOS add-on → Proxmox LXC # -# HA Host : 10.0.0.55 (enp1s0f0) +# HA Host : 10.0.0.55 (enp1s0f0, Samba share available) # New LXC : 10.0.0.54 # Addon : Nginx Proxy Manager 2.1.0 (a0d7b954_nginxproxymanager) # +# Backup method: HA Supervisor partial backup → pulled via Samba (no SSH needed) +# The backup named "NPM Migration Backup" must already exist in HA backups. +# (Claude already triggered this via HA MCP before this script was run) +# # Prerequisites: -# - SSH key from this Proxmox host authorised in HA Terminal & SSH add-on +# - smbclient installed on Proxmox (apt install smbclient -y) # - pct available (run from Proxmox node) # - wget available (for tteck script) # ============================================================================= @@ -31,9 +35,10 @@ pause() { echo -e "\n${YELLOW}${BOLD}>>> Press ENTER when ready to continue... # ─── CONFIG ─────────────────────────────────────────────────────────────────── HA_HOST="10.0.0.55" -HA_SSH_PORT="22222" # Default Terminal & SSH add-on port — adjust if needed -HA_SSH_USER="root" +HA_SAMBA_USER="homeassistant" # Default HA Samba NAS username +HA_SAMBA_PASS="" # Leave blank to prompt, or set here ADDON_SLUG="a0d7b954_nginxproxymanager" +BACKUP_LABEL="NPM Migration Backup" NEW_LXC_IP="10.0.0.54" LXC_NETMASK="24" @@ -42,127 +47,124 @@ PROXMOX_BRIDGE="vmbr0" LXC_STORAGE="local-lvm" # Change to your Proxmox storage pool if different TIMESTAMP="$(date +%Y%m%d-%H%M%S)" -BACKUP_DIR="/tmp/npm-migration-${TIMESTAMP}" -BACKUP_ARCHIVE="/tmp/npm-backup-${TIMESTAMP}.tar.gz" +WORK_DIR="/tmp/npm-migration-${TIMESTAMP}" +SUPERVISOR_BACKUP="/tmp/npm-supervisor-backup-${TIMESTAMP}.tar" +RESTORE_STAGING="${WORK_DIR}/restore-staging" # ─── SANITY CHECKS ──────────────────────────────────────────────────────────── [[ $EUID -eq 0 ]] || die "Run as root on the Proxmox node." -command -v pct &>/dev/null || die "'pct' not found — run this on a Proxmox node." -command -v wget &>/dev/null || die "'wget' not found." +command -v pct &>/dev/null || die "'pct' not found — run this on a Proxmox node." +command -v wget &>/dev/null || die "'wget' not found." +command -v smbclient &>/dev/null || { + info "smbclient not found — installing..." + apt-get install -y smbclient 2>/dev/null || die "Could not install smbclient. Run: apt install smbclient -y" +} + +mkdir -p "${WORK_DIR}" "${RESTORE_STAGING}" # ============================================================================= -# PHASE 1 — BACKUP NPM DATA FROM HA ADD-ON +# PHASE 1 — PULL NPM SUPERVISOR BACKUP FROM HA VIA SAMBA # ============================================================================= -step "PHASE 1 — Backup NPM from HA add-on (${HA_HOST})" +step "PHASE 1 — Pull NPM backup from HA via Samba" -info "Testing SSH to HA host ${HA_HOST}:${HA_SSH_PORT}..." -if ! ssh -p "${HA_SSH_PORT}" \ - -o ConnectTimeout=10 \ - -o BatchMode=yes \ - -o StrictHostKeyChecking=accept-new \ - "${HA_SSH_USER}@${HA_HOST}" "echo connected" &>/dev/null; then - die "SSH to ${HA_HOST}:${HA_SSH_PORT} failed.\n\n" \ - " Fix checklist:\n" \ - " 1. Is the Terminal & SSH add-on running in HA?\n" \ - " 2. Is your Proxmox host's public key in the add-on 'authorized_keys' config?\n" \ - " 3. Is HA_SSH_PORT correct? (check the add-on config page in HA)\n" \ - " Common ports: 22222 (default), 22\n\n" \ - " To add your key: copy output of 'cat ~/.ssh/id_rsa.pub' or 'cat ~/.ssh/id_ed25519.pub'\n" \ - " then add it to the Terminal & SSH add-on Options → authorized_keys" -fi -success "SSH connection OK" - -info "Creating backup on HA host..." -# shellcheck disable=SC2087 -ssh -p "${HA_SSH_PORT}" "${HA_SSH_USER}@${HA_HOST}" bash << REMOTE -set -euo pipefail - -ADDON_SLUG="${ADDON_SLUG}" -TIMESTAMP="${TIMESTAMP}" -BACKUP_DIR="/tmp/npm-migration-\${TIMESTAMP}" -BACKUP_ARCHIVE="/tmp/npm-backup-\${TIMESTAMP}.tar.gz" - -mkdir -p "\${BACKUP_DIR}" - -# ── Find the NPM data directory ────────────────────────────────────────────── -# Try host-level path first (accessible if SSH gives host shell), -# then the addon_configs path (accessible from Terminal add-on container) - -DATA_FOUND=0 -CANDIDATES=( - "/mnt/data/supervisor/addons/data/\${ADDON_SLUG}" - "/data/\${ADDON_SLUG}" - "/data" -) - -for DIR in "\${CANDIDATES[@]}"; do - if [ -d "\${DIR}" ] && [ -f "\${DIR}/database.sqlite" ]; then - echo "Found NPM data at: \${DIR}" - DATA_DIR="\${DIR}" - DATA_FOUND=1 - break - fi -done - -if [ \${DATA_FOUND} -eq 0 ]; then - # Last resort — check if we can find the sqlite db anywhere - DB_PATH=\$(find /mnt/data /data -name "database.sqlite" 2>/dev/null | grep -i npm | head -1 || true) - if [ -n "\${DB_PATH}" ]; then - DATA_DIR="\$(dirname "\${DB_PATH}")" - echo "Found NPM database at: \${DATA_DIR}" - DATA_FOUND=1 - fi -fi - -if [ \${DATA_FOUND} -eq 0 ]; then - echo "ERROR: Could not locate NPM data directory (database.sqlite not found)." - echo "Tried: \${CANDIDATES[*]}" - echo "" - echo "If you are in the HA Terminal container, the addon data directory" - echo "may not be mounted here. See README in the repo for manual steps." - exit 1 -fi - -# ── Copy files into staging dir ─────────────────────────────────────────────── -echo "Staging backup files..." - -[ -f "\${DATA_DIR}/database.sqlite" ] && \ - cp "\${DATA_DIR}/database.sqlite" "\${BACKUP_DIR}/" && echo " ✓ database.sqlite" - -[ -d "\${DATA_DIR}/nginx" ] && \ - cp -a "\${DATA_DIR}/nginx" "\${BACKUP_DIR}/" && echo " ✓ nginx/" - -[ -d "\${DATA_DIR}/letsencrypt" ] && \ - cp -a "\${DATA_DIR}/letsencrypt" "\${BACKUP_DIR}/" && echo " ✓ letsencrypt/" - -[ -d "\${DATA_DIR}/custom_ssl" ] && \ - cp -a "\${DATA_DIR}/custom_ssl" "\${BACKUP_DIR}/" && echo " ✓ custom_ssl/ (if present)" - -# ── Also grab addon_configs (nginx custom configs/snippets) ─────────────────── -ADDON_CONFIG_DIR="/addon_configs/\${ADDON_SLUG}" -if [ -d "\${ADDON_CONFIG_DIR}" ]; then - cp -a "\${ADDON_CONFIG_DIR}" "\${BACKUP_DIR}/addon_configs" && echo " ✓ addon_configs/" -fi - -# ── Create archive ───────────────────────────────────────────────────────────── -tar -czf "\${BACKUP_ARCHIVE}" -C "\${BACKUP_DIR}" . +echo -e "${BOLD}HA Samba credentials needed${RESET}" +echo -e " Share: \\\\${HA_HOST}\\backup" +echo -e " User: ${HA_SAMBA_USER} (or whatever you set in the Samba NAS add-on)" echo "" -echo "Backup archive created: \${BACKUP_ARCHIVE}" -ls -lh "\${BACKUP_ARCHIVE}" -REMOTE -success "Backup created on HA host" +if [[ -z "${HA_SAMBA_PASS}" ]]; then + read -rsp " Samba password for '${HA_SAMBA_USER}': " HA_SAMBA_PASS + echo "" +fi -info "Downloading backup to Proxmox (${BACKUP_ARCHIVE})..." -scp -P "${HA_SSH_PORT}" \ - "${HA_SSH_USER}@${HA_HOST}:/tmp/npm-backup-${TIMESTAMP}.tar.gz" \ - "${BACKUP_ARCHIVE}" +info "Listing HA backup share to find the NPM backup..." +BACKUP_FILE=$(smbclient "//$(echo ${HA_HOST})/backup" \ + -U "${HA_SAMBA_USER}%${HA_SAMBA_PASS}" \ + -c "ls" 2>/dev/null \ + | awk '{print $1}' \ + | grep '\.tar$' \ + | head -20 | tr '\n' '\n' || true) -ARCHIVE_SIZE=$(du -sh "${BACKUP_ARCHIVE}" | cut -f1) -success "Backup downloaded → ${BACKUP_ARCHIVE} (${ARCHIVE_SIZE})" +if [[ -z "${BACKUP_FILE}" ]]; then + die "Could not list the HA backup share at //${HA_HOST}/backup\n\n" \ + " Check:\n" \ + " 1. Samba NAS add-on is running in HA\n" \ + " 2. Username/password are correct (set in Samba add-on config)\n" \ + " 3. The 'backup' folder is enabled in the Samba add-on options\n\n" \ + " Alternatively, go to HA → Settings → System → Backups,\n" \ + " find 'NPM Migration Backup', download it manually to this machine\n" \ + " and re-run this script with SUPERVISOR_BACKUP set to that file path." +fi -info "Archive contents:" -tar -tzf "${BACKUP_ARCHIVE}" | head -40 +echo "" +info "Backup files found on share:" +echo "${BACKUP_FILE}" +echo "" + +# Find the right backup — look for the most recent one (the one we just created) +# HA backup filenames are the backup slug (8-char hex), e.g. a1b2c3d4.tar +info "Fetching the most recently modified .tar from the share..." +LATEST_TAR=$(smbclient "//$(echo ${HA_HOST})/backup" \ + -U "${HA_SAMBA_USER}%${HA_SAMBA_PASS}" \ + -c "ls" 2>/dev/null \ + | grep '\.tar' \ + | sort -k3,4 \ + | tail -1 \ + | awk '{print $1}') + +[[ -n "${LATEST_TAR}" ]] || die "Could not identify the latest backup tar file." + +info "Downloading: ${LATEST_TAR} → ${SUPERVISOR_BACKUP}" +smbclient "//$(echo ${HA_HOST})/backup" \ + -U "${HA_SAMBA_USER}%${HA_SAMBA_PASS}" \ + -c "get ${LATEST_TAR} ${SUPERVISOR_BACKUP}" 2>/dev/null + +[[ -f "${SUPERVISOR_BACKUP}" ]] || die "Download failed — file not found at ${SUPERVISOR_BACKUP}" + +ARCHIVE_SIZE=$(du -sh "${SUPERVISOR_BACKUP}" | cut -f1) +success "Supervisor backup downloaded → ${SUPERVISOR_BACKUP} (${ARCHIVE_SIZE})" + +# ── Extract NPM addon data from the Supervisor backup format ────────────────── +# Supervisor .tar structure: +# backup.json ← metadata +# {addon_slug}/ +# addon.tar.gz ← addon DATA (database, certs, nginx) +# addon_config.tar.gz ← addon CONFIG files + +info "Extracting NPM data from Supervisor backup..." +tar -xf "${SUPERVISOR_BACKUP}" -C "${WORK_DIR}" 2>/dev/null || \ + die "Failed to extract supervisor backup. Is it a valid HA backup file?" + +# Find addon subfolder (it may be named by a hash, not the slug) +ADDON_DIR=$(find "${WORK_DIR}" -maxdepth 1 -name "*.tar.gz" -o -type d 2>/dev/null | head -5) +info "Backup contents:" +ls -la "${WORK_DIR}/" + +# Find the addon.tar.gz inside any subdirectory +ADDON_DATA_TAR=$(find "${WORK_DIR}" -name "addon.tar.gz" | head -1) +ADDON_CONFIG_TAR=$(find "${WORK_DIR}" -name "addon_config.tar.gz" | head -1) + +if [[ -z "${ADDON_DATA_TAR}" ]]; then + # Some versions put it at root with the slug as filename + ADDON_DATA_TAR=$(find "${WORK_DIR}" -name "${ADDON_SLUG}.tar.gz" | head -1) +fi + +[[ -n "${ADDON_DATA_TAR}" ]] || die "Could not find addon.tar.gz in the supervisor backup.\n" \ + " This may not be the NPM backup — check HA backups and confirm\n" \ + " 'NPM Migration Backup' was created successfully before re-running." + +info "Extracting addon data archive: ${ADDON_DATA_TAR}" +tar -xzf "${ADDON_DATA_TAR}" -C "${RESTORE_STAGING}" 2>/dev/null || true + +if [[ -n "${ADDON_CONFIG_TAR}" ]]; then + info "Extracting addon config archive: ${ADDON_CONFIG_TAR}" + mkdir -p "${RESTORE_STAGING}/addon_config" + tar -xzf "${ADDON_CONFIG_TAR}" -C "${RESTORE_STAGING}/addon_config" 2>/dev/null || true +fi + +info "Staged restore contents:" +find "${RESTORE_STAGING}" -maxdepth 3 | head -30 +success "NPM data extracted and staged" # ============================================================================= # PHASE 2 — CREATE NPM LXC VIA TTECK @@ -213,68 +215,75 @@ info "Stopping NPM service in LXC before restore..." pct exec "${LXC_ID}" -- systemctl stop npm 2>/dev/null || true sleep 2 -info "Pushing backup archive into LXC..." -pct push "${LXC_ID}" "${BACKUP_ARCHIVE}" "/tmp/npm-backup.tar.gz" +info "Creating restore tarball from staged data..." +RESTORE_ARCHIVE="${WORK_DIR}/npm-restore.tar.gz" +tar -czf "${RESTORE_ARCHIVE}" -C "${RESTORE_STAGING}" . + +info "Pushing restore archive into LXC..." +pct push "${LXC_ID}" "${RESTORE_ARCHIVE}" "/tmp/npm-restore.tar.gz" info "Restoring data into /opt/npm/data/..." pct exec "${LXC_ID}" -- bash << 'RESTORE' set -euo pipefail NPM_DATA="/opt/npm/data" - mkdir -p "${NPM_DATA}/letsencrypt" "${NPM_DATA}/nginx" "${NPM_DATA}/custom_ssl" -# Extract archive contents STAGING="/tmp/npm-restore-staging" mkdir -p "${STAGING}" -tar -xzf /tmp/npm-backup.tar.gz -C "${STAGING}" +tar -xzf /tmp/npm-restore.tar.gz -C "${STAGING}" 2>/dev/null || true -echo "Staged files:" -ls -la "${STAGING}/" +echo "Staged contents:" +find "${STAGING}" -maxdepth 3 | head -30 +echo "" -# Restore database -if [ -f "${STAGING}/database.sqlite" ]; then - cp "${STAGING}/database.sqlite" "${NPM_DATA}/" - echo " ✓ database.sqlite restored" +# ── Restore database ────────────────────────────────────────────────────────── +DB=$(find "${STAGING}" -name "database.sqlite" | head -1) +if [[ -n "${DB}" ]]; then + cp "${DB}" "${NPM_DATA}/database.sqlite" + echo " ✓ database.sqlite" else - echo " ⚠ No database.sqlite found — NPM will start fresh (you can reconfigure manually)" + echo " ⚠ database.sqlite not found — NPM will start fresh" fi -# Restore nginx configs -if [ -d "${STAGING}/nginx" ]; then - cp -a "${STAGING}/nginx/." "${NPM_DATA}/nginx/" - echo " ✓ nginx/ restored" +# ── Restore nginx configs ───────────────────────────────────────────────────── +NGINX_DIR=$(find "${STAGING}" -type d -name "nginx" | head -1) +if [[ -n "${NGINX_DIR}" ]]; then + cp -a "${NGINX_DIR}/." "${NPM_DATA}/nginx/" + echo " ✓ nginx/" fi -# Restore Let's Encrypt certs -if [ -d "${STAGING}/letsencrypt" ]; then - cp -a "${STAGING}/letsencrypt/." "${NPM_DATA}/letsencrypt/" - echo " ✓ letsencrypt/ restored" +# ── Restore Let's Encrypt certs ─────────────────────────────────────────────── +LE_DIR=$(find "${STAGING}" -type d -name "letsencrypt" | head -1) +if [[ -n "${LE_DIR}" ]]; then + cp -a "${LE_DIR}/." "${NPM_DATA}/letsencrypt/" + echo " ✓ letsencrypt/" fi -# Restore custom SSL if present -if [ -d "${STAGING}/custom_ssl" ]; then - cp -a "${STAGING}/custom_ssl/." "${NPM_DATA}/custom_ssl/" - echo " ✓ custom_ssl/ restored" +# ── Restore custom SSL ──────────────────────────────────────────────────────── +SSL_DIR=$(find "${STAGING}" -type d -name "custom_ssl" | head -1) +if [[ -n "${SSL_DIR}" ]]; then + cp -a "${SSL_DIR}/." "${NPM_DATA}/custom_ssl/" + echo " ✓ custom_ssl/" fi # Fix ownership (NPM runs as uid 1000) chown -R 1000:1000 "${NPM_DATA}/" # Cleanup -rm -rf "${STAGING}" /tmp/npm-backup.tar.gz +rm -rf "${STAGING}" /tmp/npm-restore.tar.gz # Start NPM systemctl start npm -sleep 3 +sleep 4 -# Confirm it's up if systemctl is-active --quiet npm; then echo "" echo "NPM service is running ✓" else echo "" - echo "WARNING: NPM service did not start cleanly — check 'journalctl -u npm -n 50'" + echo "WARNING: NPM did not start cleanly." + echo "Check logs with: journalctl -u npm -n 50" fi RESTORE