mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-10-31 04:01:59 +01:00
Merge pull request #419 from yellowcooln/feat/proxmox-debian13-installer
feat: modernize Proxmox LXC installer for Debian 13
This commit is contained in:
4 files changed
+590
-41
No files matched your search
@@ -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
|
||||
|
||||
Executable
+132
@@ -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
@@ -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 "═══════════════════════════════════════════════════════════════"
|
||||
@@ -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
|
||||
Reference in new issue
Block a user