From 0431dfe883ed99670fc4ff24a3ebc0bc52b3e16b Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:31:33 -0400 Subject: [PATCH] feat: modernize Proxmox LXC installer --- README.md | 34 ++++++++++--- scripts/openhop-update | 59 ++++++++++++++++++++++ scripts/proxmox-install.sh | 86 ++++++++++++++++++++++++++------- tests/test_proxmox_installer.py | 74 ++++++++++++++++++++++++++++ 4 files changed, 227 insertions(+), 26 deletions(-) create mode 100755 scripts/openhop-update create mode 100644 tests/test_proxmox_installer.py diff --git a/README.md b/README.md index 4b767a3..68cad24 100644 --- a/README.md +++ b/README.md @@ -275,8 +275,8 @@ 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 @@ -290,7 +290,7 @@ 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 +- openHop Modem over TCP or USB, such as MeshSmith EtherMesh ### One-Line Install @@ -303,14 +303,20 @@ 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. 2. Create a privileged container with USB passthrough. -3. Install a host-side udev rule for the CH341 device. +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. Clone the repository and pre-seed CH341 GPIO pin mappings. -5. Run `manage.sh install` inside the container. -6. Display the dashboard URL. +5. Install an `update` command for Debian and openHop Repeater updates. +6. Run `manage.sh install` inside the container. +7. Optionally install openHop Console WebUI assets after Repeater installation. + Console is not selected as the default frontend, allowing the Repeater setup + wizard to be completed first. +8. Display the dashboard URL. ### Default Container Settings @@ -324,6 +330,8 @@ disk, bridge, etc.) and then: | Bridge | `vmbr0` | | Storage | `local-lvm` | | Password | `pymc` | +| Host-side CH341 udev rule | No | +| openHop Console WebUI | No | ### After Installation @@ -339,6 +347,16 @@ 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`: + +```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..c2e6460 --- /dev/null +++ b/scripts/openhop-update @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Update the Debian LXC and its openHop Repeater installation. + +set -Eeuo pipefail + +readonly REPO_DIR="/root/openhop-repeater" + +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 + +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 + +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 + +# 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..7826e65 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -13,7 +13,7 @@ set -euo pipefail # ── Defaults ─────────────────────────────────────────────────────────────── REPO="https://github.com/openhop-dev/openhop_repeater.git" BRANCH="dev" -CT_TEMPLATE="debian-12-standard" +CT_TEMPLATE="debian-13-standard" CT_RAM=1024 CT_SWAP=512 CT_DISK=4 @@ -24,6 +24,9 @@ CT_STORAGE="local-lvm" CT_TEMPLATE_STORAGE="local" 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" +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" @@ -71,14 +74,6 @@ 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" -fi - # Default to the next available container ID, but allow the user to choose. DEFAULT_CTID=$(pvesh get /cluster/nextid) @@ -110,14 +105,32 @@ AVAILABLE_STORAGES=$(pvesm status -content rootdir 2>/dev/null | awk 'NR>1 {prin 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 +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 [pymc]: " CT_PASSWORD; echo CT_PASSWORD="${CT_PASSWORD:-pymc}" +CH341_UDEV_SUMMARY="No" +CONSOLE_SUMMARY="No" +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 " CH341 host udev rule: ${CH341_UDEV_SUMMARY}" +echo " openHop Console WebUI: ${CONSOLE_SUMMARY}" echo " Mode: privileged (required for USB passthrough)" echo "" read -p " Proceed? [Y/n]: " -r @@ -125,8 +138,11 @@ read -p " Proceed? [Y/n]: " -r # ── 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}') +msg_info "Downloading Debian 13 template..." +TEMPLATE_FILE=$(pveam available -section system 2>/dev/null \ + | awk -v template="$CT_TEMPLATE" '$2 ~ template {print $2}' \ + | sort -V \ + | tail -1) [ -z "$TEMPLATE_FILE" ] && { msg_error "Template not found. Run: pveam update"; exit 1; } pveam list "$CT_TEMPLATE_STORAGE" 2>/dev/null | grep -q "$TEMPLATE_FILE" || \ @@ -161,12 +177,16 @@ 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..." @@ -218,6 +238,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 @@ -228,6 +249,13 @@ msg_info "Cloning openhop-repeater (branch: ${BRANCH})..." pct exec "$CTID" -- bash -c "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 +283,24 @@ 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 "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 + # ── Get container IP ────────────────────────────────────────────────────── sleep 2 CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') @@ -272,5 +318,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..968ed9e --- /dev/null +++ b/tests/test_proxmox_installer.py @@ -0,0 +1,74 @@ +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_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" + conditional = script.index('if [[ "$INSTALL_CH341_UDEV" == "true" ]]; then') + + assert prompt in script + assert script.index(rule) > conditional + assert "CH341 host udev rule: ${CH341_UDEV_SUMMARY}" 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 is installed but not enabled" in script + assert "web.web_path" not 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/scripts/openhop-update"' in updater + + +def test_readme_documents_new_installer_behavior() -> None: + readme = README.read_text() + + assert "Download a Debian 13 LXC template" in readme + assert "host-side CH341 udev rule" in readme + assert "openHop Console WebUI" in readme + assert "Run `update` inside the LXC" in readme