vm-bench/install.sh

402 lines
15 KiB
Bash
Executable file

#!/usr/bin/env bash
# ==============================================================================
# VM Bench Stack Provisioning & Lifecycle Script
# Target: Proxmox VE node (any version)
# Usage:
# ./install.sh --deploy [Interactive Setup & Deployment]
# ./install.sh --update [Pull Updates & Restart Services]
# ./install.sh --remove [Teardown Everything]
# ==============================================================================
set -euo pipefail
# Uncomment next line for debugging:
# set -x
# ------------------------------------------------------------------------------
# Formatting & Constants
# ------------------------------------------------------------------------------
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
NC='\033[0m'
REPO_URL="https://git.lohmar.co.uk/cclohmar/vm-bench.git"
SYSTEMD_DIR="/etc/systemd/system"
SERVICE_NAME="vm-bench-backend"
CT_CORES=1
CT_MEMORY=512
CT_SWAP=512
CT_DISK_SIZE=8
# ------------------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------------------
has_cmd() {
command -v "$1" >/dev/null 2>&1
}
get_default_bridge() {
ip -o link show type bridge | awk -F': ' '{print $2; exit}' 2>/dev/null || echo "vmbr0"
}
guess_gateway_from_bridge() {
local bridge="$1"
local ip_cidr
ip_cidr=$(ip -o -4 addr show dev "$bridge" 2>/dev/null | awk '{print $4; exit}')
if [[ -n "$ip_cidr" ]]; then
local ip="${ip_cidr%/*}"
local prefix="${ip%.*}"
echo "${prefix}.1"
else
echo ""
fi
}
# ------------------------------------------------------------------------------
# Environment Discovery & User Parameter Gathering
# ------------------------------------------------------------------------------
gather_environment_info() {
echo -e "${CYAN}==> [1/3] Environment Inspection & Parameter Collection${NC}"
# ---- Storage Selection ----
echo ""
echo "Inspecting available storage pools from /etc/pve/storage.cfg..."
declare -a storage_ids=()
declare -a storage_contents=()
declare -a storage_has_rootdir=()
# Parse storage.cfg for container-capable storage types
if [ -f /etc/pve/storage.cfg ]; then
# Extract storage IDs for types: lvmthin, zfspool, rbd, dir
while IFS= read -r id; do
storage_ids+=("$id")
# Get content for this pool (ignore errors)
local content
content=$(pvesm config "$id" 2>/dev/null | awk '/^content/ {$1=""; print $0}' | xargs | tr ' ' ',' || true)
storage_contents+=("${content:-none}")
if [[ "$content" == *"rootdir"* ]]; then
storage_has_rootdir+=("yes")
else
storage_has_rootdir+=("no")
fi
done < <(grep -E "^(lvmthin|zfspool|rbd|dir):" /etc/pve/storage.cfg | awk '{print $2}')
fi
# Fallback: if no storage found in config, use active pools from pvesm
if [ ${#storage_ids[@]} -eq 0 ]; then
echo -e "${YELLOW}No storage definitions found in /etc/pve/storage.cfg. Falling back to active pools from 'pvesm status'.${NC}"
while IFS= read -r pool; do
storage_ids+=("$pool")
local content
content=$(pvesm config "$pool" 2>/dev/null | awk '/^content/ {$1=""; print $0}' | xargs | tr ' ' ',' || true)
storage_contents+=("${content:-none}")
if [[ "$content" == *"rootdir"* ]]; then
storage_has_rootdir+=("yes")
else
storage_has_rootdir+=("no")
fi
done < <(pvesm status 2>/dev/null | awk 'NR>1 && $3=="active" {print $1}' || true)
fi
# If still empty, abort
if [ ${#storage_ids[@]} -eq 0 ]; then
echo -e "${RED}No usable storage pools found. Please create a storage pool (e.g., 'local' directory) and try again.${NC}"
exit 1
fi
# Show menu
echo "Available Storage Options Detected:"
for i in "${!storage_ids[@]}"; do
id="${storage_ids[$i]}"
content="${storage_contents[$i]}"
marker=""
[ "${storage_has_rootdir[$i]}" = "yes" ] && marker=" ✓ (has rootdir)"
printf " [%d] %-18s (content: %s)%s\n" "$((i+1))" "$id" "$content" "$marker"
done
# Pick default: first with rootdir, else first
default_idx=0
for i in "${!storage_ids[@]}"; do
if [ "${storage_has_rootdir[$i]}" = "yes" ]; then
default_idx=$i
break
fi
done
default_num=$((default_idx+1))
read -p "Select storage pool [1-${#storage_ids[@]}] (default: ${default_num}): " POOL_CHOICE
POOL_CHOICE="${POOL_CHOICE:-$default_num}"
if [[ "$POOL_CHOICE" =~ ^[0-9]+$ ]] && [ "$POOL_CHOICE" -ge 1 ] && [ "$POOL_CHOICE" -le "${#storage_ids[@]}" ]; then
TARGET_STORAGE="${storage_ids[$((POOL_CHOICE-1))]}"
TARGET_HAS_ROOTDIR="${storage_has_rootdir[$((POOL_CHOICE-1))]}"
else
echo -e "${YELLOW}Invalid choice. Defaulting to '${storage_ids[$default_idx]}'.${NC}"
TARGET_STORAGE="${storage_ids[$default_idx]}"
TARGET_HAS_ROOTDIR="${storage_has_rootdir[$default_idx]}"
fi
# Ensure rootdir capability
if [ "$TARGET_HAS_ROOTDIR" != "yes" ]; then
echo "Enabling 'rootdir' capability on storage pool '${TARGET_STORAGE}'..."
current_content="${storage_contents[$((POOL_CHOICE-1))]:-}"
if [ -n "$current_content" ]; then
new_content="${current_content},rootdir"
else
new_content="rootdir"
fi
pvesm set "$TARGET_STORAGE" --content "$new_content" || {
echo -e "${RED}Failed to add rootdir. Please enable it manually.${NC}"
exit 1
}
fi
# ---- Path & Container Configuration ----
echo ""
read -p "Base Storage Path [/mnt/pve/host-shared/converter]: " PATH_INPUT
STORAGE_PATH="${PATH_INPUT:-/mnt/pve/host-shared/converter}"
STORAGE_PATH="${STORAGE_PATH%/}"
read -p "Container ID [20020]: " CTID_INPUT
CTID="${CTID_INPUT:-20020}"
DEFAULT_BRIDGE=$(get_default_bridge)
read -p "Network Bridge [default: ${DEFAULT_BRIDGE}]: " BRIDGE_INPUT
CT_BRIDGE="${BRIDGE_INPUT:-$DEFAULT_BRIDGE}"
SUGGESTED_GW=$(guess_gateway_from_bridge "$CT_BRIDGE")
[ -z "$SUGGESTED_GW" ] && SUGGESTED_GW="10.2.0.1"
read -p "Container IP/CIDR (e.g., 10.2.0.20/16): " CT_IP
while [[ -z "$CT_IP" ]]; do
echo -e "${RED}Error: Static IP with CIDR mask is required (e.g., 10.2.0.20/16).${NC}"
read -p "Container IP/CIDR: " CT_IP
done
read -p "Gateway IP [${SUGGESTED_GW}]: " GW_INPUT
CT_GW="${GW_INPUT:-$SUGGESTED_GW}"
read -p "MAC Address (Press Enter to auto-generate): " MAC_INPUT
CT_MAC="${MAC_INPUT:-}"
echo ""
echo -e "${GREEN}✓ Environment configuration gathered successfully.${NC}"
}
# ------------------------------------------------------------------------------
# Action Functions (Deploy, Update, Remove)
# ------------------------------------------------------------------------------
do_deploy() {
echo -e "${GREEN}==> Beginning Deployment on Host $(hostname)...${NC}"
if ! command -v pct &>/dev/null; then
echo -e "${RED}Error: 'pct' CLI not found. Script must be executed on a Proxmox host.${NC}"
exit 1
fi
gather_environment_info
REAL_PATH=$(readlink -f "$STORAGE_PATH" 2>/dev/null || echo "$STORAGE_PATH")
# Clean previous state
if [ -d "$REAL_PATH" ] || pct status "$CTID" &>/dev/null; then
echo -e "${YELLOW}! Cleaning pre-existing installation state at ${REAL_PATH} / CTID ${CTID}...${NC}"
pct stop "$CTID" --skiplock 2>/dev/null || true
pct destroy "$CTID" --purge 2>/dev/null || true
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
rm -rf "$REAL_PATH"
fi
# Fix APT sources for Proxmox (no subscription)
rm -f /etc/apt/sources.list.d/pve-enterprise.sources \
/etc/apt/sources.list.d/ceph.sources \
/etc/apt/sources.list.d/pve-enterprise.list \
/etc/apt/sources.list.d/pve-no-subscription.list 2>/dev/null || true
cat <<'EOF' > /etc/apt/sources.list.d/proxmox-no-sub.sources
Types: deb
URIs: http://download.proxmox.com/debian/pve
Suites: trixie
Components: pve-no-subscription
Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg
EOF
apt-get update -qq
echo -e "${CYAN}==> [2/3] Provisioning Directory Structure & Backend Service${NC}"
mkdir -p "$REAL_PATH"
echo "Cloning source code from ${REPO_URL}..."
git clone "$REPO_URL" "$REAL_PATH"
CONVERTER_ROOT="$REAL_PATH"
SCRIPT_DIR="${CONVERTER_ROOT}/backend"
SERVICE_FILE="${SCRIPT_DIR}/${SERVICE_NAME}.service"
mkdir -p "${CONVERTER_ROOT}/logs" "${CONVERTER_ROOT}/tmp"
chmod 777 "${CONVERTER_ROOT}/tmp"
apt-get install -y -qq --no-install-recommends \
python3 python3-pip python3-venv libguestfs-tools \
p7zip-full unzip unrar-free tar gzip bzip2 xz-utils git
VENV_DIR="${CONVERTER_ROOT}/venv"
echo "Setting up Python virtual environment..."
python3 -m venv "$VENV_DIR"
"$VENV_DIR/bin/pip" install --upgrade pip -q
if [ -f "${SCRIPT_DIR}/requirements.txt" ]; then
"$VENV_DIR/bin/pip" install -r "${SCRIPT_DIR}/requirements.txt" -q
fi
cat <<EOF > "$SERVICE_FILE"
[Unit]
Description=VM Bench Converter Backend Service
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=${SCRIPT_DIR}
ExecStart=${VENV_DIR}/bin/python ${SCRIPT_DIR}/app.py
Restart=always
RestartSec=5
StandardOutput=append:${CONVERTER_ROOT}/logs/vm-bench-backend.log
StandardError=append:${CONVERTER_ROOT}/logs/vm-bench-backend.log
[Install]
WantedBy=multi-user.target
EOF
cp "$SERVICE_FILE" "${SYSTEMD_DIR}/${SERVICE_NAME}.service"
systemctl daemon-reload
systemctl enable --now "$SERVICE_NAME"
echo -e "${GREEN}✓ Host Backend systemd service initialized and running.${NC}"
echo -e "${CYAN}==> [3/3] Creating and Starting Frontend LXC Container (${CTID})${NC}"
# Get LXC template
TEMPLATE=$(pveam available 2>/dev/null | grep -m1 "debian-12.*amd64" | awk '{print $2}' || true)
if [ -z "$TEMPLATE" ]; then
pveam update 2>/dev/null || true
pveam download local debian-12-standard_12.7-1_amd64.tar.zst 2>/dev/null || true
TEMPLATE="local:vztmpl/debian-12-standard_12.7-1_amd64.tar.zst"
else
TEMPLATE_NAME=$(basename "$TEMPLATE")
[ ! -f "/var/lib/vz/template/cache/${TEMPLATE_NAME}" ] && pveam download local "$TEMPLATE_NAME" 2>/dev/null || true
TEMPLATE="local:vztmpl/${TEMPLATE_NAME}"
fi
NET_CFG="name=eth0,bridge=${CT_BRIDGE},firewall=1,gw=${CT_GW},ip=${CT_IP},type=veth"
[ -n "$CT_MAC" ] && NET_CFG="${NET_CFG},hwaddr=${CT_MAC}"
pct create "$CTID" "$TEMPLATE" \
--arch amd64 \
--cores "$CT_CORES" \
--hostname vm-bench \
--memory "$CT_MEMORY" \
--net0 "$NET_CFG" \
--ostype debian \
--storage "$TARGET_STORAGE" \
--rootfs "${TARGET_STORAGE}:${CT_DISK_SIZE}" \
--swap "$CT_SWAP" \
--unprivileged 0 \
--features nesting=1 \
--mp0 "${REAL_PATH},mp=/mnt/converter" \
--onboot 1 \
--start 0
# Extra LXC config
CONF_FILE="/etc/pve/lxc/${CTID}.conf"
echo "lxc.cgroup2.devices.allow: a" >> "$CONF_FILE"
echo "lxc.mount.auto: proc:rw sys:rw cgroup:rw" >> "$CONF_FILE"
echo "lxc.apparmor.profile: unconfined" >> "$CONF_FILE"
pct start "$CTID" 2>/dev/null || true
echo ""
echo -e "${GREEN}======================================================================${NC}"
echo -e "${GREEN} Deployment Successful on $(hostname)!${NC}"
echo " - Host Backend API : http://127.0.0.1:9000"
echo " - LXC Container IP : ${CT_IP%/*}"
echo " - Container Storage : ${TARGET_STORAGE}"
echo " - Mount Directory : ${REAL_PATH} -> /mnt/converter"
echo -e "${GREEN}======================================================================${NC}"
}
do_update() {
echo -e "${CYAN}==> Updating VM Bench Stack...${NC}"
read -p "Base Storage Path [/mnt/pve/host-shared/converter]: " PATH_INPUT
STORAGE_PATH="${PATH_INPUT:-/mnt/pve/host-shared/converter}"
REAL_PATH=$(readlink -f "$STORAGE_PATH" 2>/dev/null || echo "$STORAGE_PATH")
read -p "Container ID [20020]: " CTID_INPUT
CTID="${CTID_INPUT:-20020}"
if [ -d "$REAL_PATH/.git" ]; then
echo "Pulling latest code from Git..."
git -C "$REAL_PATH" pull --ff-only
fi
if [ -d "${REAL_PATH}/venv" ]; then
echo "Updating Python environment..."
"${REAL_PATH}/venv/bin/pip" install --upgrade pip -q
[ -f "${REAL_PATH}/backend/requirements.txt" ] && "${REAL_PATH}/venv/bin/pip" install -r "${REAL_PATH}/backend/requirements.txt" -q
fi
echo "Restarting Services..."
systemctl restart "$SERVICE_NAME"
pct restart "$CTID" 2>/dev/null || true
echo -e "${GREEN}✓ Stack update complete.${NC}"
}
do_remove() {
echo -e "${YELLOW}==> Tearing Down VM Bench Stack...${NC}"
read -p "Base Storage Path [/mnt/pve/host-shared/converter]: " PATH_INPUT
STORAGE_PATH="${PATH_INPUT:-/mnt/pve/host-shared/converter}"
REAL_PATH=$(readlink -f "$STORAGE_PATH" 2>/dev/null || echo "$STORAGE_PATH")
read -p "Container ID to Destroy [20020]: " CTID_INPUT
CTID="${CTID_INPUT:-20020}"
if pct status "$CTID" &>/dev/null; then
echo "Destroying LXC Container ${CTID}..."
pct stop "$CTID" --skiplock 2>/dev/null || true
pct destroy "$CTID" --purge 2>/dev/null || true
fi
if systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null || systemctl is-enabled "$SERVICE_NAME" 2>/dev/null; then
echo "Removing Host Backend Systemd Service..."
systemctl stop "$SERVICE_NAME" 2>/dev/null || true
systemctl disable "$SERVICE_NAME" 2>/dev/null || true
rm -f "${SYSTEMD_DIR}/${SERVICE_NAME}.service"
systemctl daemon-reload
fi
if [ -d "$REAL_PATH" ]; then
echo "Purging directory ${REAL_PATH}..."
rm -rf "$REAL_PATH"
fi
echo -e "${GREEN}✓ Removal complete.${NC}"
}
# ------------------------------------------------------------------------------
# CLI Dispatcher
# ------------------------------------------------------------------------------
case "${1:-}" in
--deploy)
do_deploy
;;
--update)
do_update
;;
--remove)
do_remove
;;
*)
echo -e "${CYAN}VM Bench Stack Lifecycle Manager (${HOSTNAME})${NC}"
echo "Usage: $0 {--deploy|--update|--remove}"
echo " --deploy : Interactive installation, storage selection, and deployment"
echo " --update : Pull latest repository changes and refresh stack services"
echo " --remove : Purge LXC container, host systemd backend, and storage directories"
exit 1
;;
esac