diff --git a/README.md b/README.md index 4b767a3..63db97a 100644 --- a/README.md +++ b/README.md @@ -275,22 +275,26 @@ The upgrade script will: ## Proxmox LXC Installation openHop Repeater can run inside a Proxmox LXC container using a CH341 USB-to-SPI -adapter or a TCP modem. This is useful for headless, always-on deployments -without dedicating a full Raspberry Pi. +adapter or an openHop Modem over TCP or USB. This is useful for headless, +always-on deployments without dedicating a full Raspberry Pi. ### Requirements Software: -- Proxmox VE 7.x or 8.x host -- Internet access for the container +- Proxmox VE 8.x or 9.x host; 9.x is recommended for new deployments +- A matching Debian 13 standard LXC template available through `pveam` +- Internet access from the container during installation and updates Hardware, choose one: - CH341 USB-to-SPI adapter with VID `1a86` and PID `5512`, connected to the Proxmox host and wired to an SX1262-based LoRa module such as an Ebyte - E22-900M30S -- TCP modem, such as MeshSmith EtherMesh + E22-900M30S. Select the optional host-side CH341 udev rule for this setup. +- openHop Modem over TCP, such as MeshSmith EtherMesh-1W, reachable from the + container network. This does not need the CH341 udev rule or a USB device. +- openHop Modem over USB, connected to the Proxmox host. The installer enables + general USB passthrough by default; the CH341 udev rule is not needed. ### One-Line Install @@ -303,14 +307,25 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/openhop-dev/openhop_repe Replace `main` in the URL with another branch name if needed. The installer will prompt for container settings (container ID, hostname, RAM, -disk, bridge, etc.) and then: +disk, bridge, etc.), whether to install the host-side CH341 udev rule, and +whether to download the optional openHop Console WebUI. It will then: -1. Download a Debian 12 LXC template. +1. Download a Debian 13 LXC template matching the Proxmox host architecture. 2. Create a privileged container with USB passthrough. -3. Install a host-side udev rule for the CH341 device. -4. Clone the repository and pre-seed CH341 GPIO pin mappings. -5. Run `manage.sh install` inside the container. -6. Display the dashboard URL. +3. Install the host-side CH341 udev rule only when selected. This is not needed + for openHop Modem TCP or USB connections and defaults to No. +4. Start the container, wait for network access, then run a full Debian package + update and upgrade. +5. Clone the repository and pre-seed CH341 GPIO pin mappings. +6. Install an `update` command for Debian and openHop Repeater updates. +7. Run `manage.sh install` inside the container. +8. Optionally install openHop Console WebUI assets after Repeater installation. + The public Console distribution repository is cloned as a depth-one, + single-branch checkout without tags at `/root/pymc_console`, minimizing disk + usage while keeping it upgradeable in the same way as Repeater. Console is + not selected as the default frontend, allowing the Repeater setup wizard to + be completed first. +9. Display the dashboard URL. ### Default Container Settings @@ -322,8 +337,11 @@ disk, bridge, etc.) and then: | Disk | 4 GB | | CPU cores | 2 | | Bridge | `vmbr0` | +| VLAN ID | None | | Storage | `local-lvm` | -| Password | `pymc` | +| Password | `openHop1!` | +| Host-side CH341 udev rule | No | +| openHop Console WebUI | No | ### After Installation @@ -339,6 +357,18 @@ cd /opt/openhop_repeater bash manage.sh ``` +Run `update` inside the LXC to update Debian packages, fast-forward the branch +selected during installation, and run `manage.sh upgrade`. The command first +lists every action and requires a `y/N` confirmation. When Console is installed, +it also updates `/root/pymc_console` and refreshes the Console assets: + +```bash +update +``` + +If openHop Console WebUI was installed, complete the Repeater setup wizard +before selecting `openHop Console` from Web Settings. + Open the dashboard at: ```text diff --git a/scripts/openhop-update b/scripts/openhop-update new file mode 100755 index 0000000..f292906 --- /dev/null +++ b/scripts/openhop-update @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Update the Debian LXC and its openHop Repeater installation. + +set -Eeuo pipefail + +readonly REPO_DIR="/root/openhop-repeater" +readonly CONSOLE_REPO_DIR="/root/pymc_console" +readonly CONSOLE_REPO_URL="https://github.com/Treehouse-00/pymc_console-dist.git" +readonly CONSOLE_UI_DIR="/opt/pymc_console/web/html" + +if [[ "$EUID" -ne 0 ]]; then + echo "This command must be run as root." >&2 + exit 1 +fi + +if [[ ! -d "$REPO_DIR/.git" || ! -f "$REPO_DIR/manage.sh" ]]; then + echo "openHop Repeater source checkout not found at $REPO_DIR." >&2 + exit 1 +fi + +BRANCH=$(git -C "$REPO_DIR" branch --show-current) +if [[ -z "$BRANCH" ]]; then + echo "The openHop Repeater checkout is in detached HEAD state; update aborted." >&2 + exit 1 +fi + +if [[ -n "$(git -C "$REPO_DIR" status --porcelain)" ]]; then + echo "The openHop Repeater checkout has local changes; update aborted." >&2 + exit 1 +fi + +CONSOLE_INSTALLED=false +CONSOLE_CHECKOUT=false +[[ -f "$CONSOLE_UI_DIR/index.html" ]] && CONSOLE_INSTALLED=true +[[ -d "$CONSOLE_REPO_DIR/.git" && -f "$CONSOLE_REPO_DIR/manage.sh" ]] \ + && CONSOLE_CHECKOUT=true + +echo "This update will:" +echo " - update installed Debian packages" +echo " - fast-forward openHop Repeater branch: $BRANCH" +echo " - run openHop Repeater manage.sh upgrade" +if [[ "$CONSOLE_INSTALLED" == "true" && "$CONSOLE_CHECKOUT" == "true" ]]; then + echo " - fast-forward and upgrade openHop Console" +elif [[ "$CONSOLE_INSTALLED" == "true" ]]; then + echo " - create /root/pymc_console and upgrade openHop Console" +else + echo " - leave openHop Console unchanged (not installed)" +fi +echo "" +read -rp "Continue? [y/N]: " reply +if [[ ! "$reply" =~ ^[Yy]([Ee][Ss])?$ ]]; then + echo "Update cancelled." + exit 0 +fi + +echo "==> Checking openHop Repeater branch: $BRANCH" +git -C "$REPO_DIR" fetch origin --prune +if ! git -C "$REPO_DIR" show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then + echo "Remote branch origin/$BRANCH does not exist; update aborted." >&2 + exit 1 +fi +if ! git -C "$REPO_DIR" merge-base --is-ancestor HEAD "origin/$BRANCH"; then + echo "The local branch cannot be fast-forwarded to origin/$BRANCH; update aborted." >&2 + exit 1 +fi + +if [[ "$CONSOLE_INSTALLED" == "true" ]]; then + if [[ "$CONSOLE_CHECKOUT" != "true" ]]; then + if [[ -e "$CONSOLE_REPO_DIR" ]]; then + echo "$CONSOLE_REPO_DIR exists but is not a valid Console checkout; update aborted." >&2 + exit 1 + fi + echo "==> Creating openHop Console checkout" + git clone --depth 1 --single-branch --branch main --no-tags \ + "$CONSOLE_REPO_URL" "$CONSOLE_REPO_DIR" + CONSOLE_CHECKOUT=true + fi + + CONSOLE_BRANCH=$(git -C "$CONSOLE_REPO_DIR" branch --show-current) + if [[ -z "$CONSOLE_BRANCH" ]]; then + echo "The openHop Console checkout is in detached HEAD state; update aborted." >&2 + exit 1 + fi + if [[ -n "$(git -C "$CONSOLE_REPO_DIR" status --porcelain)" ]]; then + echo "The openHop Console checkout has local changes; update aborted." >&2 + exit 1 + fi + git -C "$CONSOLE_REPO_DIR" fetch origin --prune + if ! git -C "$CONSOLE_REPO_DIR" show-ref --verify --quiet \ + "refs/remotes/origin/$CONSOLE_BRANCH"; then + echo "Remote Console branch origin/$CONSOLE_BRANCH does not exist; update aborted." >&2 + exit 1 + fi + if ! git -C "$CONSOLE_REPO_DIR" merge-base --is-ancestor \ + HEAD "origin/$CONSOLE_BRANCH"; then + echo "The local Console branch cannot be fast-forwarded; update aborted." >&2 + exit 1 + fi +fi + +export DEBIAN_FRONTEND=noninteractive + +echo "==> Updating Debian packages" +apt-get update +apt-get upgrade -y + +echo "==> Updating openHop Repeater branch: $BRANCH" +git -C "$REPO_DIR" pull --ff-only origin "$BRANCH" + +echo "==> Running openHop Repeater upgrade" +cd "$REPO_DIR" +OPENHOP_UPGRADE_REF="$BRANCH" TERM="${TERM:-xterm}" bash manage.sh upgrade + +if [[ "$CONSOLE_INSTALLED" == "true" ]]; then + echo "==> Updating openHop Console branch: $CONSOLE_BRANCH" + git -C "$CONSOLE_REPO_DIR" pull --ff-only origin "$CONSOLE_BRANCH" + echo "==> Running openHop Console upgrade" + cd "$CONSOLE_REPO_DIR" + ASSUME_YES=1 NO_COLOR=1 bash manage.sh upgrade +fi + +# Keep the documented management copy in /opt aligned with the source used for +# this successful upgrade. +install -m 0755 "$REPO_DIR/manage.sh" /opt/openhop_repeater/manage.sh + +# Refresh the installed helper for the next run without replacing this script +# until all update steps have completed successfully. +install -m 0755 "$REPO_DIR/scripts/openhop-update" /usr/local/bin/openhop-update.new +mv /usr/local/bin/openhop-update.new /usr/local/bin/openhop-update +ln -sfn /usr/local/bin/openhop-update /usr/local/bin/update + +echo "==> Update complete" diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 8862ec3..ed77fc1 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -11,19 +11,25 @@ set -euo pipefail # ── Defaults ─────────────────────────────────────────────────────────────── -REPO="https://github.com/openhop-dev/openhop_repeater.git" -BRANCH="dev" -CT_TEMPLATE="debian-12-standard" +REPO="${OPENHOP_REPO:-https://github.com/openhop-dev/openhop_repeater.git}" +BRANCH="${OPENHOP_BRANCH:-dev}" +CT_TEMPLATE="debian-13-standard" CT_RAM=1024 CT_SWAP=512 CT_DISK=4 CT_CORES=2 CT_HOSTNAME="openhop-repeater" CT_BRIDGE="vmbr0" +CT_VLAN="" CT_STORAGE="local-lvm" CT_TEMPLATE_STORAGE="local" +CT_PASSWORD_DEFAULT="openHop1!" CH341_VID="1a86" CH341_PID="5512" +CONSOLE_RELEASE_URL="https://github.com/Treehouse-00/pymc_console-dist/releases/latest/download/pymc-ui-latest.tar.gz" +CONSOLE_REPO="https://github.com/Treehouse-00/pymc_console-dist.git" +INSTALL_CH341_UDEV=false +INSTALL_CONSOLE=false # ── Colors ───────────────────────────────────────────────────────────────── RD="\033[01;31m" GN="\033[1;92m" YW="\033[33m" BL="\033[36m" BLD="\033[1m" CL="\033[m" @@ -33,6 +39,16 @@ msg_ok() { echo -e " ${GN}✓${CL} ${1}"; } msg_warn() { echo -e " ${YW}⚠${CL} ${1}"; } msg_error() { echo -e " ${RD}✗${CL} ${1}"; } +is_safe_git_ref() { + local ref="${1:-}" + [[ "$ref" =~ ^[a-zA-Z0-9][a-zA-Z0-9._/-]{0,79}$ ]] \ + && [[ "$ref" != *..* ]] \ + && [[ "$ref" != *//* ]] \ + && [[ "$ref" != */ ]] \ + && [[ "$ref" != *. ]] \ + && [[ "$ref" != *.lock ]] +} + header() { clear echo -e "${BLD}" @@ -71,13 +87,12 @@ fi msg_ok "Running on Proxmox host as root" -# Check for CH341 -echo "" -if lsusb -d "${CH341_VID}:${CH341_PID}" &>/dev/null; then - msg_ok "CH341 USB device detected" -else - msg_warn "CH341 USB device not found — plug it in before starting the repeater" +CT_ARCH=$(dpkg --print-architecture) +if [[ ! "$CT_ARCH" =~ ^(amd64|arm64)$ ]]; then + msg_error "Unsupported Proxmox host architecture: ${CT_ARCH}" + exit 1 fi +msg_ok "Detected Proxmox host architecture: ${CT_ARCH}" # Default to the next available container ID, but allow the user to choose. DEFAULT_CTID=$(pvesh get /cluster/nextid) @@ -105,29 +120,71 @@ read -p " RAM in MB [${CT_RAM}]: " -r input; CT_RAM="${input:-$CT_RAM}" read -p " Disk in GB [${CT_DISK}]: " -r input; CT_DISK="${input:-$CT_DISK}" read -p " CPU cores [${CT_CORES}]: " -r input; CT_CORES="${input:-$CT_CORES}" read -p " Bridge [${CT_BRIDGE}]: " -r input; CT_BRIDGE="${input:-$CT_BRIDGE}" +while true; do + read -p " VLAN ID [none]: " -r input + CT_VLAN="${input:-}" + if [[ -z "$CT_VLAN" ]]; then + break + fi + if [[ "$CT_VLAN" =~ ^[0-9]+$ ]] && (( CT_VLAN >= 1 && CT_VLAN <= 4094 )); then + break + fi + msg_warn "VLAN ID must be blank or a number from 1 to 4094" +done AVAILABLE_STORAGES=$(pvesm status -content rootdir 2>/dev/null | awk 'NR>1 {print $1}' || echo "local-lvm") echo " Available storages: ${AVAILABLE_STORAGES}" read -p " Storage [${CT_STORAGE}]: " -r input; CT_STORAGE="${input:-$CT_STORAGE}" read -p " Git branch [${BRANCH}]: " -r input; BRANCH="${input:-$BRANCH}" -read -sp " Root password [pymc]: " CT_PASSWORD; echo -CT_PASSWORD="${CT_PASSWORD:-pymc}" +if ! is_safe_git_ref "$BRANCH"; then + msg_error "Invalid Git branch: ${BRANCH}" + exit 1 +fi +read -p " Install host-side CH341 udev rule? [y/N]: " -r input +[[ "${input:-n}" =~ ^[Yy]([Ee][Ss])?$ ]] && INSTALL_CH341_UDEV=true +read -p " Install optional openHop Console WebUI? [y/N]: " -r input +[[ "${input:-n}" =~ ^[Yy]([Ee][Ss])?$ ]] && INSTALL_CONSOLE=true +read -rsp " Root password [${CT_PASSWORD_DEFAULT}]: " CT_PASSWORD; echo +CT_PASSWORD="${CT_PASSWORD:-$CT_PASSWORD_DEFAULT}" + +CH341_UDEV_SUMMARY="No" +CONSOLE_SUMMARY="No" +VLAN_SUMMARY="${CT_VLAN:-none}" +if [[ "$INSTALL_CH341_UDEV" == "true" ]]; then + CH341_UDEV_SUMMARY="Yes" + if lsusb -d "${CH341_VID}:${CH341_PID}" &>/dev/null; then + msg_ok "CH341 USB device detected" + else + msg_warn "CH341 USB device not found — plug it in before starting the repeater" + fi +fi +[[ "$INSTALL_CONSOLE" == "true" ]] && CONSOLE_SUMMARY="Yes" # ── Confirmation ────────────────────────────────────────────────────────── echo "" echo -e "${BLD}Summary:${CL}" echo " CTID: ${CTID} Host: ${CT_HOSTNAME} RAM: ${CT_RAM}MB Disk: ${CT_DISK}GB" -echo " Cores: ${CT_CORES} Storage: ${CT_STORAGE} Bridge: ${CT_BRIDGE} Branch: ${BRANCH}" -echo " Mode: privileged (required for USB passthrough)" +echo " Cores: ${CT_CORES} Storage: ${CT_STORAGE} Bridge: ${CT_BRIDGE} VLAN: ${VLAN_SUMMARY}" +echo " Branch: ${BRANCH}" +echo " CH341 host udev rule: ${CH341_UDEV_SUMMARY}" +echo " openHop Console WebUI: ${CONSOLE_SUMMARY}" +echo " Mode: privileged" echo "" read -p " Proceed? [Y/n]: " -r [[ "${REPLY:-Y}" =~ ^[Nn]$ ]] && { msg_warn "Aborted"; exit 0; } # ── Download template ───────────────────────────────────────────────────── echo "" -msg_info "Downloading Debian 12 template..." -TEMPLATE_FILE=$(pveam available -section system 2>/dev/null | grep "${CT_TEMPLATE}" | sort -t- -k4 -V | tail -1 | awk '{print $2}') -[ -z "$TEMPLATE_FILE" ] && { msg_error "Template not found. Run: pveam update"; exit 1; } +msg_info "Downloading Debian 13 template..." +TEMPLATE_FILE=$(pveam available -section system 2>/dev/null \ + | awk -v template="$CT_TEMPLATE" -v arch="$CT_ARCH" \ + '$2 ~ ("^" template "_") && $2 ~ ("_" arch "\\.tar\\.") {print $2}' \ + | sort -V \ + | tail -1) +[ -z "$TEMPLATE_FILE" ] && { + msg_error "Debian 13 template for ${CT_ARCH} not found. Run: pveam update" + exit 1 +} pveam list "$CT_TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE_FILE" || \ pveam download "$CT_TEMPLATE_STORAGE" "$TEMPLATE_FILE" @@ -135,13 +192,17 @@ msg_ok "Template ready" # ── Create container ────────────────────────────────────────────────────── msg_info "Creating LXC container ${CTID}..." +CT_NET0="name=eth0,bridge=${CT_BRIDGE},ip=dhcp" +if [[ -n "$CT_VLAN" ]]; then + CT_NET0+=",tag=${CT_VLAN}" +fi pct create "$CTID" "${CT_TEMPLATE_STORAGE}:vztmpl/${TEMPLATE_FILE}" \ --hostname "$CT_HOSTNAME" \ --memory "$CT_RAM" \ --swap "$CT_SWAP" \ --cores "$CT_CORES" \ --rootfs "${CT_STORAGE}:${CT_DISK}" \ - --net0 "name=eth0,bridge=${CT_BRIDGE},ip=dhcp" \ + --net0 "$CT_NET0" \ --unprivileged 0 \ --features nesting=1 \ --onboot 1 \ @@ -153,38 +214,56 @@ msg_ok "Container created" # ── USB passthrough ─────────────────────────────────────────────────────── msg_info "Configuring USB passthrough..." cat >> "/etc/pve/lxc/${CTID}.conf" <<'EOF' - -# CH341 USB passthrough for openHop Repeater lxc.cgroup2.devices.allow: c 189:* rwm lxc.mount.entry: /dev/bus/usb dev/bus/usb none bind,optional,create=dir 0 0 EOF msg_ok "USB passthrough configured" # ── Host udev rule ──────────────────────────────────────────────────────── -msg_info "Installing CH341 udev rule on host..." -echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1a86", ATTR{idProduct}=="5512", MODE="0666"' \ - > /etc/udev/rules.d/99-ch341.rules -udevadm control --reload-rules -udevadm trigger --subsystem-match=usb --action=change -msg_ok "Host udev rule installed" +if [[ "$INSTALL_CH341_UDEV" == "true" ]]; then + msg_info "Installing CH341 udev rule on host..." + echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="1a86", ATTR{idProduct}=="5512", MODE="0666"' \ + > /etc/udev/rules.d/99-ch341.rules + udevadm control --reload-rules + udevadm trigger --subsystem-match=usb --action=change + msg_ok "Host udev rule installed" +else + msg_info "Skipping host-side CH341 udev rule" +fi # ── Start container & wait for network ──────────────────────────────────── msg_info "Starting container..." pct start "$CTID" sleep 3 +NETWORK_READY=false for _ in $(seq 1 30); do - pct exec "$CTID" -- ping -c1 -W1 8.8.8.8 &>/dev/null && break + if pct exec "$CTID" -- ping -c1 -W1 8.8.8.8 &>/dev/null; then + NETWORK_READY=true + break + fi sleep 1 done +if [[ "$NETWORK_READY" != "true" ]]; then + msg_error "Container network did not become ready after 30 seconds" + exit 1 +fi msg_ok "Container running with network" +# ── Update Debian before installing Repeater ─────────────────────────────── +msg_info "Updating Debian packages inside container..." +pct exec "$CTID" -- bash -c " + export DEBIAN_FRONTEND=noninteractive + apt-get update + apt-get full-upgrade -y +" +msg_ok "Debian packages fully updated" + # ── Bootstrap: install git, clone repo ──────────────────────────────────── msg_info "Installing git inside container..." pct exec "$CTID" -- bash -c " export DEBIAN_FRONTEND=noninteractive # Fix locale warnings - apt-get update -qq apt-get install -y locales >/dev/null 2>&1 sed -i 's/# en_US.UTF-8/en_US.UTF-8/' /etc/locale.gen locale-gen >/dev/null 2>&1 @@ -218,6 +297,7 @@ echo \" 💡 IP Address: \$IP\" echo \" 📡 Dashboard: http://\$IP:8000\" echo \"\" echo \" Management: cd /opt/openhop_repeater && bash manage.sh\" +echo \" Update: update\" echo \"\" MOTD chmod +x /etc/profile.d/pymc-motd.sh @@ -225,9 +305,16 @@ MOTD msg_ok "curl/git installed, locale fixed, console auto-login enabled" msg_info "Cloning openhop-repeater (branch: ${BRANCH})..." -pct exec "$CTID" -- bash -c "git clone --branch ${BRANCH} ${REPO} /root/openhop-repeater" +pct exec "$CTID" -- git clone --branch "$BRANCH" "$REPO" /root/openhop-repeater msg_ok "Repository cloned" +msg_info "Installing LXC update command..." +pct exec "$CTID" -- bash -c " + install -m 0755 /root/openhop-repeater/scripts/openhop-update /usr/local/bin/openhop-update + ln -sfn /usr/local/bin/openhop-update /usr/local/bin/update +" +msg_ok "Update command installed" + # Pre-seed config with CH341 radio type and correct GPIO pins pct exec "$CTID" -- bash -c " mkdir -p /etc/openhop_repeater @@ -255,6 +342,128 @@ lxc-attach -n "$CTID" -- bash -c "cd /root/openhop-repeater && TERM=xterm bash m echo "" msg_ok "manage.sh install completed" +# ── Optional openHop Console WebUI ───────────────────────────────────────── +if [[ "$INSTALL_CONSOLE" == "true" ]]; then + msg_info "Cloning openHop Console distribution repository..." + pct exec "$CTID" -- git clone --depth 1 --single-branch --branch main --no-tags \ + "$CONSOLE_REPO" /root/pymc_console + msg_ok "Console repository cloned to /root/pymc_console" + + msg_info "Installing optional openHop Console WebUI..." + pct exec "$CTID" -- bash -c " + set -e + console_archive=\$(mktemp) + trap 'rm -f \"\$console_archive\"' EXIT + curl -fL '${CONSOLE_RELEASE_URL}' -o \"\$console_archive\" + rm -rf /opt/pymc_console/web/html + install -d -m 0755 -o repeater -g repeater /opt/pymc_console/web/html + tar -xzf \"\$console_archive\" -C /opt/pymc_console/web/html + test -f /opt/pymc_console/web/html/index.html + chown -R repeater:repeater /opt/pymc_console + " + msg_ok "Console is installed but not enabled" + msg_info "Complete the Repeater setup wizard before selecting openHop Console in Web Settings" +fi + +# ── Container notes ─────────────────────────────────────────────────────── +NOTES_TMP=$(mktemp) +cat > "$NOTES_TMP" <<'NOTES' +
+ + openHop Logo + + +

+ openHop Repeater LXC +

+ +

+ + Support openHop + +

+ +

+ + Open openHop Repeater documentation + +

+ + + + Website + + + + + GitHub + + + + + Discord + + + + + Discussions + + + + + Issues + +
+NOTES +NOTES_TEXT=$(cat "$NOTES_TMP") +rm -f "$NOTES_TMP" +pct set "$CTID" --description "$NOTES_TEXT" +msg_ok "Container notes set" + # ── Get container IP ────────────────────────────────────────────────────── sleep 2 CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') @@ -272,5 +481,9 @@ echo -e " Dashboard: ${GN}http://${CT_IP:-}:8000${CL}" echo "" echo " Next: open the dashboard and complete the setup wizard" echo " Management: pct enter ${CTID}, then: cd /opt/openhop_repeater && bash manage.sh" +echo " Update: pct enter ${CTID}, then run: update" +if [[ "$INSTALL_CONSOLE" == "true" ]]; then + echo " Console: installed but not enabled; select it in Web Settings after setup" +fi echo "" echo "═══════════════════════════════════════════════════════════════" diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py new file mode 100644 index 0000000..9f7f237 --- /dev/null +++ b/tests/test_proxmox_installer.py @@ -0,0 +1,174 @@ +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +INSTALLER = ROOT / "scripts" / "proxmox-install.sh" +UPDATER = ROOT / "scripts" / "openhop-update" +README = ROOT / "README.md" + + +def test_proxmox_installer_uses_debian_13() -> None: + script = INSTALLER.read_text() + + assert 'CT_TEMPLATE="debian-13-standard"' in script + assert "Downloading Debian 13 template" in script + assert "sort -V" in script + assert "Debian 12" not in script + + +def test_template_selection_matches_the_proxmox_host_architecture() -> None: + script = INSTALLER.read_text() + + assert "CT_ARCH=$(dpkg --print-architecture)" in script + assert '[[ ! "$CT_ARCH" =~ ^(amd64|arm64)$ ]]' in script + assert '-v arch="$CT_ARCH"' in script + assert '$2 ~ ("_" arch "\\\\.tar\\\\.")' in script + + +def test_installer_allows_explicit_repository_and_branch_for_fork_testing() -> None: + script = INSTALLER.read_text() + + assert 'REPO="${OPENHOP_REPO:-https://github.com/openhop-dev/openhop_repeater.git}"' in script + assert 'BRANCH="${OPENHOP_BRANCH:-dev}"' in script + + +def test_ch341_host_rule_is_opt_in() -> None: + script = INSTALLER.read_text() + + prompt = "Install host-side CH341 udev rule? [y/N]" + rule = "/etc/udev/rules.d/99-ch341.rules" + rule_section = script.split("# ── Host udev rule", 1)[1].split("# ── Start container", 1)[0] + + assert prompt in script + assert 'if [[ "$INSTALL_CH341_UDEV" == "true" ]]; then' in rule_section + assert rule in rule_section + assert "CH341 host udev rule: ${CH341_UDEV_SUMMARY}" in script + + +def test_git_branch_is_validated_and_not_evaluated_by_a_shell() -> None: + script = INSTALLER.read_text() + + assert 'is_safe_git_ref "$BRANCH"' in script + assert 'pct exec "$CTID" -- git clone --branch "$BRANCH" "$REPO"' in script + assert 'bash -c "git clone --branch ${BRANCH}' not in script + + +def test_password_default_meets_proxmox_minimum_length() -> None: + script = INSTALLER.read_text() + + assert 'CT_PASSWORD_DEFAULT="openHop1!"' in script + assert 'CT_PASSWORD="${CT_PASSWORD:-$CT_PASSWORD_DEFAULT}"' in script + assert "Root password [pymc]" not in script + + +def test_optional_vlan_is_validated_and_added_to_net0() -> None: + script = INSTALLER.read_text() + + assert "VLAN ID [none]" in script + assert "CT_VLAN >= 1 && CT_VLAN <= 4094" in script + assert 'CT_NET0+=",tag=${CT_VLAN}"' in script + assert '--net0 "$CT_NET0"' in script + assert "VLAN: ${VLAN_SUMMARY}" in script + + +def test_installer_fully_updates_debian_before_cloning_repeater() -> None: + script = INSTALLER.read_text() + + assert "apt-get update" in script + assert "apt-get full-upgrade -y" in script + assert script.index("apt-get update") < script.index("Cloning openhop-repeater") + assert script.index("apt-get full-upgrade -y") < script.index("Cloning openhop-repeater") + + +def test_installer_fails_when_container_network_never_becomes_ready() -> None: + script = INSTALLER.read_text() + + assert "NETWORK_READY=false" in script + assert "NETWORK_READY=true" in script + assert "Container network did not become ready after 30 seconds" in script + + +def test_console_is_opt_in_installed_after_repeater_and_not_enabled() -> None: + script = INSTALLER.read_text() + + assert "Install optional openHop Console WebUI? [y/N]" in script + assert "openHop Console WebUI: ${CONSOLE_SUMMARY}" in script + assert script.index("Installing optional openHop Console WebUI") > script.index( + 'msg_ok "manage.sh install completed"' + ) + assert "pymc-ui-latest.tar.gz" in script + assert '"$CONSOLE_REPO" /root/pymc_console' in script + assert "Console is installed but not enabled" in script + assert "web.web_path" not in script + + +def test_installer_sets_proxmox_html_description() -> None: + script = INSTALLER.read_text() + + assert 'pct set "$CTID" --description "$NOTES_TEXT"' in script + assert 'pct set "$CTID" --notes' not in script + assert "# USB passthrough for openHop Repeater" not in script + assert "openHop Repeater LXC" in script + assert "https://docs.openhop.dev/projects/openhop-repeater/" in script + + +def test_installer_adds_update_command() -> None: + script = INSTALLER.read_text() + + assert ( + "install -m 0755 /root/openhop-repeater/scripts/openhop-update " + "/usr/local/bin/openhop-update" + ) in script + assert "ln -sfn /usr/local/bin/openhop-update /usr/local/bin/update" in script + assert "Update: update" in script + + +def test_update_command_updates_apt_and_current_repeater_branch() -> None: + updater = UPDATER.read_text() + + assert "apt-get update" in updater + assert "apt-get upgrade -y" in updater + assert 'BRANCH=$(git -C "$REPO_DIR" branch --show-current)' in updater + assert 'git -C "$REPO_DIR" status --porcelain' in updater + assert 'merge-base --is-ancestor HEAD "origin/$BRANCH"' in updater + assert 'git -C "$REPO_DIR" pull --ff-only origin "$BRANCH"' in updater + assert 'OPENHOP_UPGRADE_REF="$BRANCH"' in updater + assert "bash manage.sh upgrade" in updater + assert 'install -m 0755 "$REPO_DIR/manage.sh" /opt/openhop_repeater/manage.sh' in updater + assert 'install -m 0755 "$REPO_DIR/scripts/openhop-update"' in updater + + +def test_update_command_describes_work_and_requires_confirmation() -> None: + updater = UPDATER.read_text() + + prompt = 'read -rp "Continue? [y/N]: " reply' + assert 'echo "This update will:"' in updater + assert prompt in updater + assert 'echo "Update cancelled."' in updater + assert updater.index(prompt) < updater.index("apt-get update") + + +def test_console_checkout_is_created_and_updated_when_console_is_installed() -> None: + installer = INSTALLER.read_text() + updater = UPDATER.read_text() + + shallow_clone = "git clone --depth 1 --single-branch --branch main --no-tags" + assert shallow_clone in installer + assert "/root/pymc_console" in installer + assert 'readonly CONSOLE_REPO_DIR="/root/pymc_console"' in updater + assert shallow_clone in updater + assert '"$CONSOLE_REPO_URL" "$CONSOLE_REPO_DIR"' in updater + assert 'git -C "$CONSOLE_REPO_DIR" pull --ff-only' in updater + assert "ASSUME_YES=1 NO_COLOR=1 bash manage.sh upgrade" in updater + + +def test_readme_documents_new_installer_behavior() -> None: + readme = README.read_text() + + assert "Download a Debian 13 LXC template" in readme + assert "Proxmox VE 8.x or 9.x host" in readme + assert "Proxmox VE 7.x" not in readme + assert "MeshSmith EtherMesh-1W" in readme + assert "general USB passthrough by default" in readme + assert "host-side CH341 udev rule" in readme + assert "openHop Console WebUI" in readme + assert "Run `update` inside the LXC" in readme