feat: modernize Proxmox LXC installer

This commit is contained in:
yellowcooln committed 2026-08-29 15:31:33 -04:00
1 parent e28be27a77
commit 0431dfe883
4 files changed
+227 -26

No files matched your search

+26 -8
View File
@@ -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
+59
View File
@@ -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"
+68 -18
View File
@@ -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:-<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 "═══════════════════════════════════════════════════════════════"
+74
View File
@@ -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