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 01/14] 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 From 4e8be755b8227c95f1cd6fc2aec480397d14fc94 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:34:57 -0400 Subject: [PATCH 02/14] fix: harden Proxmox installer inputs --- scripts/openhop-update | 4 ++++ scripts/proxmox-install.sh | 26 ++++++++++++++++++++++++-- tests/test_proxmox_installer.py | 22 ++++++++++++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/scripts/openhop-update b/scripts/openhop-update index c2e6460..326e318 100755 --- a/scripts/openhop-update +++ b/scripts/openhop-update @@ -50,6 +50,10 @@ echo "==> Running openHop Repeater upgrade" cd "$REPO_DIR" OPENHOP_UPGRADE_REF="$BRANCH" TERM="${TERM:-xterm}" bash manage.sh upgrade +# 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 diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 7826e65..20840dd 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -36,6 +36,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}" @@ -105,6 +115,10 @@ 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}" +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 @@ -192,10 +206,18 @@ fi 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" # ── Bootstrap: install git, clone repo ──────────────────────────────────── @@ -246,7 +268,7 @@ 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..." diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index 968ed9e..a5d29a0 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -20,13 +20,30 @@ def test_ch341_host_rule_is_opt_in() -> None: 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') + rule_section = script.split("# ── Host udev rule", 1)[1].split("# ── Start container", 1)[0] assert prompt in script - assert script.index(rule) > conditional + 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_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() @@ -62,6 +79,7 @@ def test_update_command_updates_apt_and_current_repeater_branch() -> None: 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 From 403151247b45a8f092f503f537d6b1e812c1183d Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:47:53 -0400 Subject: [PATCH 03/14] test: allow installer runs from fork branches --- scripts/proxmox-install.sh | 4 ++-- tests/test_proxmox_installer.py | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 20840dd..d963f18 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -11,8 +11,8 @@ set -euo pipefail # ── Defaults ─────────────────────────────────────────────────────────────── -REPO="https://github.com/openhop-dev/openhop_repeater.git" -BRANCH="dev" +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 diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index a5d29a0..1146018 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -15,6 +15,13 @@ def test_proxmox_installer_uses_debian_13() -> None: assert "Debian 12" not 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() From 4fadb3b767e8f33ac04fb913dedb3434e141b69b Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:51:35 -0400 Subject: [PATCH 04/14] fix: match LXC template to host architecture --- README.md | 2 +- scripts/proxmox-install.sh | 15 +++++++++++++-- tests/test_proxmox_installer.py | 9 +++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 68cad24..1017d2d 100644 --- a/README.md +++ b/README.md @@ -306,7 +306,7 @@ The installer will prompt for container settings (container ID, hostname, RAM, 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 13 LXC template. +1. Download a Debian 13 LXC template matching the Proxmox host architecture. 2. Create a privileged container with USB passthrough. 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. diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index d963f18..012b968 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -84,6 +84,13 @@ fi msg_ok "Running on Proxmox host as root" +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) @@ -154,10 +161,14 @@ read -p " Proceed? [Y/n]: " -r echo "" msg_info "Downloading Debian 13 template..." TEMPLATE_FILE=$(pveam available -section system 2>/dev/null \ - | awk -v template="$CT_TEMPLATE" '$2 ~ template {print $2}' \ + | 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 "Template not found. Run: pveam update"; exit 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" diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index 1146018..e0b0e8b 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -15,6 +15,15 @@ def test_proxmox_installer_uses_debian_13() -> None: 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() From 7fce3b0d74b7c0c82305b7b5822e02dc94b1fff6 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 15:55:11 -0400 Subject: [PATCH 05/14] feat: add VLAN option and valid default password --- README.md | 3 ++- scripts/proxmox-install.sh | 27 +++++++++++++++++++++++---- tests/test_proxmox_installer.py | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 1017d2d..6a71a09 100644 --- a/README.md +++ b/README.md @@ -328,8 +328,9 @@ whether to download the optional openHop Console WebUI. It will 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 | diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 012b968..b6a9b1c 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -20,8 +20,10 @@ 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" @@ -117,6 +119,17 @@ 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}" @@ -130,11 +143,12 @@ 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}" +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 @@ -149,7 +163,8 @@ fi 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 " 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 (required for USB passthrough)" @@ -176,13 +191,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 \ diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index e0b0e8b..4ab9dfa 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -52,6 +52,24 @@ def test_git_branch_is_validated_and_not_evaluated_by_a_shell() -> None: 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_fails_when_container_network_never_becomes_ready() -> None: script = INSTALLER.read_text() From 7664eb08d60b9f733509ee6740570ad3ad0839d1 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 16:06:10 -0400 Subject: [PATCH 06/14] feat: confirm updates and manage Console checkout --- README.md | 10 +++-- scripts/openhop-update | 68 +++++++++++++++++++++++++++++++++ scripts/proxmox-install.sh | 6 +++ tests/test_proxmox_installer.py | 23 +++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6a71a09..27392e2 100644 --- a/README.md +++ b/README.md @@ -314,8 +314,10 @@ whether to download the optional openHop Console WebUI. It will then: 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. + The public Console distribution repository is cloned to `/root/pymc_console` + so it can be inspected and upgraded in the same way as Repeater. 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 @@ -349,7 +351,9 @@ bash manage.sh ``` Run `update` inside the LXC to update Debian packages, fast-forward the branch -selected during installation, and run `manage.sh upgrade`: +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 diff --git a/scripts/openhop-update b/scripts/openhop-update index 326e318..0d06521 100755 --- a/scripts/openhop-update +++ b/scripts/openhop-update @@ -4,6 +4,9 @@ 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 @@ -26,6 +29,30 @@ if [[ -n "$(git -C "$REPO_DIR" status --porcelain)" ]]; then 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 @@ -37,6 +64,39 @@ if ! git -C "$REPO_DIR" merge-base --is-ancestor HEAD "origin/$BRANCH"; then 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 --branch main --single-branch "$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" @@ -50,6 +110,14 @@ 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 diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index b6a9b1c..46a57c6 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -27,6 +27,7 @@ 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 @@ -337,6 +338,11 @@ 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 --branch main --single-branch \ + "$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 diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index 4ab9dfa..0f606e2 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -87,6 +87,7 @@ def test_console_is_opt_in_installed_after_repeater_and_not_enabled() -> None: '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 @@ -117,6 +118,28 @@ def test_update_command_updates_apt_and_current_repeater_branch() -> None: 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() + + assert "git clone --branch main --single-branch" in installer + assert "/root/pymc_console" in installer + assert 'readonly CONSOLE_REPO_DIR="/root/pymc_console"' in updater + assert 'git clone --branch main --single-branch "$CONSOLE_REPO_URL"' 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() From 41205b486db4451b1f841b8e42677c142ad09567 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:15:43 -0400 Subject: [PATCH 07/14] feat: skip USB passthrough unless CH341 selected --- scripts/proxmox-install.sh | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 46a57c6..9efc349 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -168,7 +168,7 @@ echo " Cores: ${CT_CORES} Storage: ${CT_STORAGE} Bridge: ${CT_BRIDGE} VLAN: echo " Branch: ${BRANCH}" echo " CH341 host udev rule: ${CH341_UDEV_SUMMARY}" echo " openHop Console WebUI: ${CONSOLE_SUMMARY}" -echo " Mode: privileged (required for USB passthrough)" +echo " Mode: privileged" echo "" read -p " Proceed? [Y/n]: " -r [[ "${REPLY:-Y}" =~ ^[Nn]$ ]] && { msg_warn "Aborted"; exit 0; } @@ -212,14 +212,18 @@ pct create "$CTID" "${CT_TEMPLATE_STORAGE}:vztmpl/${TEMPLATE_FILE}" \ msg_ok "Container created" # ── USB passthrough ─────────────────────────────────────────────────────── -msg_info "Configuring USB passthrough..." -cat >> "/etc/pve/lxc/${CTID}.conf" <<'EOF' +if [[ "$INSTALL_CH341_UDEV" == "true" ]]; then + 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" + msg_ok "USB passthrough configured" +else + msg_info "Skipping USB passthrough (CH341 not selected)" +fi # ── Host udev rule ──────────────────────────────────────────────────────── if [[ "$INSTALL_CH341_UDEV" == "true" ]]; then From a92ec8cfdcf4902aa2ced650db5daca925ff4404 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:21:23 -0400 Subject: [PATCH 08/14] feat: always enable USB passthrough, keep udev rule opt-in --- scripts/proxmox-install.sh | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 9efc349..1e535d7 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -212,18 +212,14 @@ pct create "$CTID" "${CT_TEMPLATE_STORAGE}:vztmpl/${TEMPLATE_FILE}" \ msg_ok "Container created" # ── USB passthrough ─────────────────────────────────────────────────────── -if [[ "$INSTALL_CH341_UDEV" == "true" ]]; then - msg_info "Configuring USB passthrough..." - cat >> "/etc/pve/lxc/${CTID}.conf" <<'EOF' +msg_info "Configuring USB passthrough..." +cat >> "/etc/pve/lxc/${CTID}.conf" <<'EOF' -# CH341 USB passthrough for openHop Repeater +# 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" -else - msg_info "Skipping USB passthrough (CH341 not selected)" -fi +msg_ok "USB passthrough configured" # ── Host udev rule ──────────────────────────────────────────────────────── if [[ "$INSTALL_CH341_UDEV" == "true" ]]; then From ca4b77a452b1292dd144dbb2f378683e841e74eb Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:27:26 -0400 Subject: [PATCH 09/14] feat: add openHop Repeater LXC notes HTML to installer --- scripts/proxmox-install.sh | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 1e535d7..a5a9447 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -359,6 +359,103 @@ if [[ "$INSTALL_CONSOLE" == "true" ]]; then msg_info "Complete the Repeater setup wizard before selecting openHop Console in Web Settings" fi +# ── Container notes ─────────────────────────────────────────────────────── +PROXMOX_NOTES_DIR="/var/lib/pve-manager/notes" +if [[ -d "$PROXMOX_NOTES_DIR" ]]; then + cat > "${PROXMOX_NOTES_DIR}/${CTID}.html" <<'NOTES' +
+ + openHop Logo + + +

+ openHop Repeater LXC +

+ +

+ + Support openHop + +

+ +

+ + Open openHop Repeater documentation + +

+ + + + Website + + + + + GitHub + + + + + Discord + + + + + Discussions + + + + + Issues + +
+NOTES +fi + # ── Get container IP ────────────────────────────────────────────────────── sleep 2 CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') From f3570953c99fd1d5cfb4a2005cfcda114d081c02 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:45:09 -0400 Subject: [PATCH 10/14] fix: use pct set --notes for container notes (proper Proxmox API) --- scripts/proxmox-install.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index a5a9447..8fce5a6 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -360,9 +360,9 @@ if [[ "$INSTALL_CONSOLE" == "true" ]]; then fi # ── Container notes ─────────────────────────────────────────────────────── -PROXMOX_NOTES_DIR="/var/lib/pve-manager/notes" -if [[ -d "$PROXMOX_NOTES_DIR" ]]; then - cat > "${PROXMOX_NOTES_DIR}/${CTID}.html" <<'NOTES' +if command -v pct &>/dev/null; then + NOTES_TMP=$(mktemp) + cat > "$NOTES_TMP" <<'NOTES'
NOTES + NOTES_TEXT=$(cat "$NOTES_TMP") + rm -f "$NOTES_TMP" + pct set "$CTID" --notes "$NOTES_TEXT" + msg_ok "Container notes set" fi # ── Get container IP ────────────────────────────────────────────────────── From dc8a42a16acdffccdb83686fe77abd9d9efff800 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:58:35 -0400 Subject: [PATCH 11/14] fix: set Proxmox container description correctly --- scripts/proxmox-install.sh | 16 ++++++---------- tests/test_proxmox_installer.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index 8fce5a6..d6f4bba 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -214,8 +214,6 @@ msg_ok "Container created" # ── USB passthrough ─────────────────────────────────────────────────────── msg_info "Configuring USB passthrough..." cat >> "/etc/pve/lxc/${CTID}.conf" <<'EOF' - -# 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 @@ -360,9 +358,8 @@ if [[ "$INSTALL_CONSOLE" == "true" ]]; then fi # ── Container notes ─────────────────────────────────────────────────────── -if command -v pct &>/dev/null; then - NOTES_TMP=$(mktemp) - cat > "$NOTES_TMP" <<'NOTES' +NOTES_TMP=$(mktemp) +cat > "$NOTES_TMP" <<'NOTES'
/dev/null; then
NOTES - NOTES_TEXT=$(cat "$NOTES_TMP") - rm -f "$NOTES_TMP" - pct set "$CTID" --notes "$NOTES_TEXT" - msg_ok "Container notes set" -fi +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 diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index 0f606e2..0a4f161 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -92,6 +92,16 @@ def test_console_is_opt_in_installed_after_repeater_and_not_enabled() -> None: 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() From def72a2b19e53b9ea24a70f363f06a1ad55c0fd1 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:04:59 -0400 Subject: [PATCH 12/14] feat: fully update Debian before Repeater install --- README.md | 12 +++++++----- scripts/proxmox-install.sh | 10 +++++++++- tests/test_proxmox_installer.py | 9 +++++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 27392e2..ebb4c62 100644 --- a/README.md +++ b/README.md @@ -310,15 +310,17 @@ whether to download the optional openHop Console WebUI. It will then: 2. Create a privileged container with USB passthrough. 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. 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. +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 to `/root/pymc_console` so it can be inspected and upgraded in the same way as Repeater. Console is not selected as the default frontend, allowing the Repeater setup wizard to be completed first. -8. Display the dashboard URL. +9. Display the dashboard URL. ### Default Container Settings diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index d6f4bba..f3b1c3b 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -249,13 +249,21 @@ if [[ "$NETWORK_READY" != "true" ]]; then 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 diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index 0a4f161..f8429f3 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -70,6 +70,15 @@ def test_optional_vlan_is_validated_and_added_to_net0() -> None: 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() From a6ab8bc90634f12275db070939d970c0ad268b4f Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:19:09 -0400 Subject: [PATCH 13/14] feat: shallow clone Console distribution --- README.md | 5 +++-- scripts/openhop-update | 3 ++- scripts/proxmox-install.sh | 2 +- tests/test_proxmox_installer.py | 6 ++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ebb4c62..2f1763c 100644 --- a/README.md +++ b/README.md @@ -316,8 +316,9 @@ whether to download the optional openHop Console WebUI. It will then: 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 to `/root/pymc_console` - so it can be inspected and upgraded in the same way as Repeater. Console is + 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. diff --git a/scripts/openhop-update b/scripts/openhop-update index 0d06521..f292906 100755 --- a/scripts/openhop-update +++ b/scripts/openhop-update @@ -71,7 +71,8 @@ if [[ "$CONSOLE_INSTALLED" == "true" ]]; then exit 1 fi echo "==> Creating openHop Console checkout" - git clone --branch main --single-branch "$CONSOLE_REPO_URL" "$CONSOLE_REPO_DIR" + git clone --depth 1 --single-branch --branch main --no-tags \ + "$CONSOLE_REPO_URL" "$CONSOLE_REPO_DIR" CONSOLE_CHECKOUT=true fi diff --git a/scripts/proxmox-install.sh b/scripts/proxmox-install.sh index f3b1c3b..ed77fc1 100644 --- a/scripts/proxmox-install.sh +++ b/scripts/proxmox-install.sh @@ -345,7 +345,7 @@ 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 --branch main --single-branch \ + 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" diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index f8429f3..9310627 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -151,10 +151,12 @@ def test_console_checkout_is_created_and_updated_when_console_is_installed() -> installer = INSTALLER.read_text() updater = UPDATER.read_text() - assert "git clone --branch main --single-branch" in installer + 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 'git clone --branch main --single-branch "$CONSOLE_REPO_URL"' 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 From 9e79acc887ce2c542f2d44c41aea63a34a544361 Mon Sep 17 00:00:00 2001 From: yellowcooln <12516003+yellowcooln@users.noreply.github.com> Date: Sat, 29 Aug 2026 23:46:49 -0400 Subject: [PATCH 14/14] docs: update Proxmox LXC requirements --- README.md | 12 ++++++++---- tests/test_proxmox_installer.py | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2f1763c..63db97a 100644 --- a/README.md +++ b/README.md @@ -282,15 +282,19 @@ always-on deployments without dedicating a full Raspberry Pi. 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 -- openHop Modem over TCP or USB, 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 diff --git a/tests/test_proxmox_installer.py b/tests/test_proxmox_installer.py index 9310627..9f7f237 100644 --- a/tests/test_proxmox_installer.py +++ b/tests/test_proxmox_installer.py @@ -165,6 +165,10 @@ 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