Merge pull request #419 from yellowcooln/feat/proxmox-debian13-installer

feat: modernize Proxmox LXC installer for Debian 13
This commit is contained in:
Yellowcooln authored and GitHub committed 2026-08-30 20:51:49 -04:00
commit a0d75b6739
4 files changed
+590 -41

No files matched your search

+43 -13
View File
@@ -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
+132
View File
@@ -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"
+241 -28
View File
@@ -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'
<div align="center">
<a href="https://openhop.dev/" target="_blank" rel="noopener noreferrer">
<img
src="https://avatars.githubusercontent.com/u/295154237?s=100&v=4"
width="100"
height="100"
alt="openHop Logo"
/>
</a>
<h2 style="font-size: 24px; margin: 20px 0;">
openHop Repeater LXC
</h2>
<p style="margin: 16px 0;">
<a
href="https://buymeacoffee.com/rightup"
target="_blank"
rel="noopener noreferrer"
>
<img
src="https://img.shields.io/badge/❤️-Support%20openHop-FF5E5B"
alt="Support openHop"
/>
</a>
</p>
<p style="margin: 12px 0;">
<a
href="https://docs.openhop.dev/projects/openhop-repeater/"
target="_blank"
rel="noopener noreferrer"
>
<img
src="https://img.shields.io/badge/📡-Open%20Repeater%20Docs-00617f"
alt="Open openHop Repeater documentation"
/>
</a>
</p>
<span style="margin: 0 10px;">
<i class="fa fa-globe fa-fw" style="color: #f5f5f5;"></i>
<a
href="https://openhop.dev/"
target="_blank"
rel="noopener noreferrer"
style="text-decoration: none; color: #00617f;"
>Website</a>
</span>
<span style="margin: 0 10px;">
<i class="fa fa-github fa-fw" style="color: #f5f5f5;"></i>
<a
href="https://github.com/openhop-dev/openhop_repeater"
target="_blank"
rel="noopener noreferrer"
style="text-decoration: none; color: #00617f;"
>GitHub</a>
</span>
<span style="margin: 0 10px;">
<i class="fa fa-comments fa-fw" style="color: #f5f5f5;"></i>
<a
href="https://discord.gg/3s8MMaSTzq"
target="_blank"
rel="noopener noreferrer"
style="text-decoration: none; color: #00617f;"
>Discord</a>
</span>
<span style="margin: 0 10px;">
<i class="fa fa-comment-o fa-fw" style="color: #f5f5f5;"></i>
<a
href="https://github.com/openhop-dev/openhop_repeater/discussions"
target="_blank"
rel="noopener noreferrer"
style="text-decoration: none; color: #00617f;"
>Discussions</a>
</span>
<span style="margin: 0 10px;">
<i class="fa fa-exclamation-circle fa-fw" style="color: #f5f5f5;"></i>
<a
href="https://github.com/openhop-dev/openhop_repeater/issues"
target="_blank"
rel="noopener noreferrer"
style="text-decoration: none; color: #00617f;"
>Issues</a>
</span>
</div>
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:-<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 "═══════════════════════════════════════════════════════════════"
+174
View File
@@ -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