Created
February 22, 2026 05:18
-
-
Save noralstat/93cef470d9710c453f79382b79c03b03 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # iVentoy LXC Installer for Proxmox VE | |
| # Run this from a Proxmox VE host shell as root. | |
| set -euo pipefail | |
| # Defaults | |
| CTID_DEFAULT=$(pvesh get /cluster/nextid 2>/dev/null || echo 200) | |
| HOSTNAME_DEFAULT="iventoy" | |
| DISK_SIZE_DEFAULT="8" | |
| RAM_DEFAULT="1024" | |
| CORES_DEFAULT="2" | |
| BRIDGE_DEFAULT="vmbr0" | |
| TEMPLATE_PREFIX_DEFAULT="debian-13-standard" | |
| # Colors | |
| GN="\033[1;32m" | |
| RD="\033[1;31m" | |
| YW="\033[1;33m" | |
| CY="\033[1;36m" | |
| CL="\033[0m" | |
| info() { echo -e " ${CY}[INFO]${CL} $1"; } | |
| ok() { echo -e " ${GN}[OK]${CL} $1"; } | |
| err() { echo -e " ${RD}[ERROR]${CL} $1"; exit 1; } | |
| warn() { echo -e " ${YW}[WARN]${CL} $1"; } | |
| require_cmd() { | |
| command -v "$1" >/dev/null 2>&1 || err "Missing required command: $1" | |
| } | |
| prompt_default() { | |
| local prompt="$1" | |
| local default_value="$2" | |
| local var_name="$3" | |
| local input | |
| read -r -p " ${prompt} [${default_value}]: " input | |
| printf -v "$var_name" "%s" "${input:-$default_value}" | |
| } | |
| validate_positive_int() { | |
| local value="$1" | |
| local label="$2" | |
| [[ "$value" =~ ^[0-9]+$ ]] || err "$label must be a positive integer." | |
| [[ "$value" -gt 0 ]] || err "$label must be greater than zero." | |
| } | |
| storage_supports_content() { | |
| local storage="$1" | |
| local content="$2" | |
| pvesm status -content "$content" 2>/dev/null | awk 'NR>1 && $3=="active" {print $1}' | grep -Fxq "$storage" | |
| } | |
| latest_template_from_prefix() { | |
| local prefix="$1" | |
| grep -oE "${prefix}[^[:space:]]*" | sort -V | tail -n1 || true | |
| } | |
| info "Validating Proxmox host prerequisites..." | |
| [[ "$(id -u)" -eq 0 ]] || err "Run as root." | |
| require_cmd pct | |
| require_cmd pvesh | |
| require_cmd pvesm | |
| require_cmd pveam | |
| require_cmd awk | |
| require_cmd grep | |
| ok "Host checks passed." | |
| echo "" | |
| echo -e "${GN}===========================================${CL}" | |
| echo -e "${GN} iVentoy LXC Installer for PVE ${CL}" | |
| echo -e "${GN}===========================================${CL}" | |
| echo "" | |
| prompt_default "Container ID" "$CTID_DEFAULT" CTID | |
| [[ "$CTID" =~ ^[0-9]+$ ]] || err "Container ID must be numeric." | |
| pct status "$CTID" >/dev/null 2>&1 && err "CT $CTID already exists. Pick a different ID." | |
| prompt_default "Hostname" "$HOSTNAME_DEFAULT" HOSTNAME | |
| prompt_default "Disk size in GB" "$DISK_SIZE_DEFAULT" DISK_SIZE | |
| prompt_default "RAM in MB" "$RAM_DEFAULT" RAM | |
| prompt_default "CPU cores" "$CORES_DEFAULT" CORES | |
| prompt_default "Network bridge" "$BRIDGE_DEFAULT" BRIDGE | |
| validate_positive_int "$DISK_SIZE" "Disk size" | |
| validate_positive_int "$RAM" "RAM" | |
| validate_positive_int "$CORES" "CPU cores" | |
| [[ -d "/sys/class/net/${BRIDGE}" ]] || err "Network bridge ${BRIDGE} does not exist on the host." | |
| info "Detecting storage..." | |
| ROOTFS_STORAGE_DEFAULT=$(pvesm status -content rootdir 2>/dev/null | awk 'NR>1 && $3=="active" {print $1; exit}') | |
| [[ -n "$ROOTFS_STORAGE_DEFAULT" ]] || err "No active storage with rootdir content found." | |
| prompt_default "Storage for rootfs" "$ROOTFS_STORAGE_DEFAULT" STORAGE | |
| storage_supports_content "$STORAGE" rootdir || err "Storage ${STORAGE} does not support rootdir or is inactive." | |
| TEMPLATE_STORAGE_DEFAULT=$(pvesm status -content vztmpl 2>/dev/null | awk 'NR>1 && $3=="active" {print $1; exit}') | |
| [[ -n "$TEMPLATE_STORAGE_DEFAULT" ]] || err "No active storage with vztmpl content found." | |
| prompt_default "Template storage" "$TEMPLATE_STORAGE_DEFAULT" TEMPLATE_STORAGE | |
| storage_supports_content "$TEMPLATE_STORAGE" vztmpl || err "Storage ${TEMPLATE_STORAGE} does not support vztmpl or is inactive." | |
| TEMPLATE=$(pveam list "$TEMPLATE_STORAGE" 2>/dev/null | latest_template_from_prefix "$TEMPLATE_PREFIX_DEFAULT") | |
| if [[ -z "$TEMPLATE" ]]; then | |
| info "Template ${TEMPLATE_PREFIX_DEFAULT} not found locally. Refreshing template list..." | |
| pveam update >/dev/null | |
| TEMPLATE=$(pveam available --section system 2>/dev/null | latest_template_from_prefix "$TEMPLATE_PREFIX_DEFAULT") | |
| fi | |
| if [[ -z "$TEMPLATE" ]]; then | |
| warn "Debian 13 template not found. Falling back to latest Debian standard template." | |
| TEMPLATE=$(pveam available --section system 2>/dev/null | grep -oE "debian-[0-9]+-standard[^[:space:]]*" | sort -V | tail -n1 || true) | |
| fi | |
| [[ -n "$TEMPLATE" ]] || err "Could not find a Debian standard LXC template." | |
| if ! pveam list "$TEMPLATE_STORAGE" 2>/dev/null | grep -Fq "$TEMPLATE"; then | |
| info "Downloading template ${TEMPLATE} to ${TEMPLATE_STORAGE}..." | |
| pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" >/dev/null | |
| fi | |
| ok "Template: $TEMPLATE" | |
| info "Creating CT $CTID ($HOSTNAME)..." | |
| pct create "$CTID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}" \ | |
| --hostname "$HOSTNAME" \ | |
| --ostype debian \ | |
| --memory "$RAM" \ | |
| --cores "$CORES" \ | |
| --rootfs "${STORAGE}:${DISK_SIZE}" \ | |
| --net0 "name=eth0,bridge=${BRIDGE},ip=dhcp" \ | |
| --unprivileged 0 \ | |
| --features "nesting=1,keyctl=1" \ | |
| --onboot 1 \ | |
| --start 0 | |
| ok "Container $CTID created." | |
| info "Starting CT $CTID..." | |
| pct start "$CTID" | |
| sleep 3 | |
| info "Installing iVentoy inside CT $CTID..." | |
| pct exec "$CTID" -- bash -s <<'IN_CONTAINER' | |
| set -euo pipefail | |
| export DEBIAN_FRONTEND=noninteractive | |
| for attempt in 1 2 3 4 5; do | |
| if apt-get update -qq >/dev/null 2>&1; then | |
| break | |
| fi | |
| if [ "$attempt" -eq 5 ]; then | |
| echo "ERROR: apt-get update failed after multiple attempts." | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| apt-get install -y -qq ca-certificates curl tar >/dev/null 2>&1 | |
| RELEASE_JSON=$(curl -fsSL https://api.github.com/repos/ventoy/PXE/releases/latest) | |
| RELEASE=$(printf '%s\n' "$RELEASE_JSON" | grep -oE '"tag_name"[[:space:]]*:[[:space:]]*"v?[^"]+"' | head -n1 | cut -d'"' -f4 | sed 's/^v//') | |
| ASSET=$(printf '%s\n' "$RELEASE_JSON" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]*linux-free\.tar\.gz"' | head -n1 | cut -d'"' -f4) | |
| if [ -z "$RELEASE" ]; then | |
| echo "ERROR: Could not determine latest iVentoy release." | |
| exit 1 | |
| fi | |
| if [ -z "$ASSET" ]; then | |
| ASSET="iventoy-${RELEASE}-linux-free.tar.gz" | |
| fi | |
| echo "Installing iVentoy v${RELEASE}..." | |
| TMP_ARCHIVE="/tmp/iventoy.tar.gz" | |
| if ! curl -fsSL "https://github.com/ventoy/PXE/releases/download/v${RELEASE}/${ASSET}" -o "$TMP_ARCHIVE"; then | |
| curl -fsSL "https://github.com/ventoy/PXE/releases/download/${RELEASE}/${ASSET}" -o "$TMP_ARCHIVE" | |
| fi | |
| TOP_DIR=$(tar -tf "$TMP_ARCHIVE" | head -n1 | cut -d/ -f1) | |
| [ -n "$TOP_DIR" ] || { echo "ERROR: Unexpected iVentoy archive layout."; exit 1; } | |
| mkdir -p /opt/iventoy /opt/iventoy/iso | |
| tar -xzf "$TMP_ARCHIVE" -C /tmp | |
| cp -a "/tmp/${TOP_DIR}/." /opt/iventoy/ | |
| rm -rf "$TMP_ARCHIVE" "/tmp/${TOP_DIR}" | |
| chmod +x /opt/iventoy/iventoy.sh | |
| cat > /etc/systemd/system/iventoy.service <<'SERVICE_EOF' | |
| [Unit] | |
| Description=iVentoy PXE Service | |
| Documentation=https://www.iventoy.com | |
| Wants=network-online.target | |
| After=network-online.target | |
| [Service] | |
| Type=forking | |
| WorkingDirectory=/opt/iventoy | |
| Environment=IVENTOY_API_ALL=1 | |
| Environment=IVENTOY_AUTO_RUN=1 | |
| Environment=LIBRARY_PATH=/opt/iventoy/lib/lin64 | |
| Environment=LD_LIBRARY_PATH=/opt/iventoy/lib/lin64 | |
| ExecStart=/opt/iventoy/iventoy.sh -R start | |
| ExecStop=/opt/iventoy/iventoy.sh -R stop | |
| Restart=on-failure | |
| RestartSec=3 | |
| [Install] | |
| WantedBy=multi-user.target | |
| SERVICE_EOF | |
| systemctl daemon-reload | |
| systemctl enable --now iventoy >/dev/null 2>&1 | |
| systemctl is-active --quiet iventoy || { echo "ERROR: iVentoy service is not active."; exit 1; } | |
| echo "iVentoy service started successfully." | |
| IN_CONTAINER | |
| ok "iVentoy installed." | |
| sleep 2 | |
| IP=$(pct exec "$CTID" -- bash -lc "hostname -I 2>/dev/null | awk '{print \$1}'" || true) | |
| echo "" | |
| echo -e "${GN}===========================================${CL}" | |
| echo -e "${GN} Installation Complete ${CL}" | |
| echo -e "${GN}===========================================${CL}" | |
| echo "" | |
| echo -e " ${CY}Container ID:${CL} $CTID" | |
| echo -e " ${CY}Hostname:${CL} $HOSTNAME" | |
| if [[ -n "${IP:-}" ]]; then | |
| echo -e " ${CY}Web UI:${CL} ${GN}http://${IP}:26000${CL}" | |
| else | |
| echo -e " ${CY}Web UI:${CL} ${YW}CT IP not detected yet. Check with: pct exec $CTID -- hostname -I${CL}" | |
| fi | |
| echo "" | |
| echo -e " ${YW}ISO path in CT:${CL} /opt/iventoy/iso/" | |
| echo -e " ${YW}Example push:${CL} pct push $CTID myfile.iso /opt/iventoy/iso/myfile.iso" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment