#!/usr/bin/env bash # # grafana-opensuse-ct.sh — interactive (whiptail) installer that provisions an # openSUSE LXC container on Proxmox and installs/configures Grafana inside it. # # Mimics the community-scripts.github.io/ProxmoxVE UX: a Default/Advanced menu, # whiptail prompts, then a scripted pct create + pct exec install flow. # # Run this ON the Proxmox host as root: # ./grafana-opensuse-ct.sh set -euo pipefail APP="Grafana" # ---------- helpers ---------- msg_info() { echo -e "\e[1;34m[*]\e[0m $1"; } msg_ok() { echo -e "\e[1;32m[OK]\e[0m $1"; } msg_err() { echo -e "\e[1;31m[ERR]\e[0m $1" >&2; } die() { msg_err "$1"; exit 1; } command -v pct >/dev/null 2>&1 || die "pct not found — run this on a Proxmox VE host." command -v whiptail >/dev/null 2>&1 || { msg_info "whiptail not found, installing..."; apt-get update -qq && apt-get install -y whiptail; } # ---------- welcome + mode ---------- whiptail --title "$APP LXC Installer" --msgbox \ "This will create an openSUSE LXC container on this Proxmox host and install Grafana inside it." \ 10 65 MODE=$(whiptail --title "$APP LXC Installer" --menu "Choose setup mode" 12 65 2 \ "default" "Quick install — sane defaults, minimal prompts" \ "advanced" "Advanced — customize every setting" \ 3>&1 1>&2 2>&3) || exit 1 # ---------- gather settings ---------- NEXTID=$(pvesh get /cluster/nextid) if [[ "$MODE" == "default" ]]; then CTID="$NEXTID" CT_HOSTNAME="grafana" CT_STORAGE=$(pvesm status -content rootdir | awk 'NR==2{print $1}') [[ -n "$CT_STORAGE" ]] || die "No rootdir-capable storage found." DISK_GB=8 CORES=2 MEMORY=1024 SWAP=512 BRIDGE="vmbr0" VLAN="" NET_MODE="dhcp" UNPRIVILEGED=1 GRAFANA_PORT=3000 ROOT_PASSWORD=$(tr -dc 'A-Za-z0-9' &1 1>&2 2>&3) || exit 1 CT_HOSTNAME=$(whiptail --inputbox "Hostname" 8 58 "grafana" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 mapfile -t STORAGES < <(pvesm status -content rootdir | awk 'NR>1{print $1}') [[ ${#STORAGES[@]} -gt 0 ]] || die "No rootdir-capable storage found." RADIO_ARGS=(); first=1 for s in "${STORAGES[@]}"; do if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$s" "" ON); first=0; else RADIO_ARGS+=("$s" "" OFF); fi done CT_STORAGE=$(whiptail --radiolist "Storage for rootfs" 15 60 6 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 DISK_GB=$(whiptail --inputbox "Disk size (GB)" 8 58 "8" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 CORES=$(whiptail --inputbox "CPU cores" 8 58 "2" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 MEMORY=$(whiptail --inputbox "Memory (MB)" 8 58 "1024" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 SWAP=$(whiptail --inputbox "Swap (MB)" 8 58 "512" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 mapfile -t BRIDGES < <(ip -o link show type bridge | awk -F': ' '{print $2}') [[ ${#BRIDGES[@]} -gt 0 ]] || die "No bridges found on this host." RADIO_ARGS=(); first=1 for b in "${BRIDGES[@]}"; do if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$b" "" ON); first=0; else RADIO_ARGS+=("$b" "" OFF); fi done BRIDGE=$(whiptail --radiolist "Network bridge" 15 60 6 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 VLAN=$(whiptail --inputbox "VLAN tag (blank = untagged)" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 NET_MODE=$(whiptail --menu "Network mode" 12 58 2 \ "dhcp" "DHCP" \ "static" "Static IP" \ --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 if [[ "$NET_MODE" == "static" ]]; then STATIC_IP_CIDR=$(whiptail --inputbox "IP/CIDR, e.g. 192.168.10.50/24" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 STATIC_GW=$(whiptail --inputbox "Gateway" 8 58 "" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 fi if whiptail --title "$APP LXC" --yesno "Create as unprivileged container? (recommended)" 8 58; then UNPRIVILEGED=1 else UNPRIVILEGED=0 fi GRAFANA_PORT=$(whiptail --inputbox "Grafana HTTP port" 8 58 "3000" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 ROOT_PASSWORD=$(whiptail --passwordbox "Root password for the CT" 8 58 --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 GRAFANA_ADMIN_PASSWORD=$(whiptail --passwordbox "Grafana admin password" 8 58 --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 SSH_PUBKEY_FILE=$(whiptail --inputbox "SSH public key file to inject (blank = skip)" 8 58 "$HOME/.ssh/id_rsa.pub" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 fi pct status "$CTID" >/dev/null 2>&1 && die "CT $CTID already exists." # ---------- template ---------- mapfile -t TEMPLATES < <(pveam available -section system | awk '{print $2}' | grep -i opensuse || true) if [[ ${#TEMPLATES[@]} -eq 0 ]]; then msg_info "Refreshing template index..." pveam update mapfile -t TEMPLATES < <(pveam available -section system | awk '{print $2}' | grep -i opensuse || true) fi [[ ${#TEMPLATES[@]} -gt 0 ]] || die "No openSUSE templates found in the Proxmox appliance list." if [[ "$MODE" == "default" ]]; then TEMPLATE="${TEMPLATES[-1]}" # newest listed else RADIO_ARGS=(); first=1 for t in "${TEMPLATES[@]}"; do if [[ $first -eq 1 ]]; then RADIO_ARGS+=("$t" "" ON); first=0; else RADIO_ARGS+=("$t" "" OFF); fi done TEMPLATE=$(whiptail --radiolist "Choose openSUSE template" 20 72 10 "${RADIO_ARGS[@]}" --title "$APP LXC" 3>&1 1>&2 2>&3) || exit 1 fi if ! pveam list local | grep -q "$TEMPLATE"; then msg_info "Downloading template $TEMPLATE..." pveam download local "$TEMPLATE" msg_ok "Template downloaded." fi # ---------- confirm ---------- SUMMARY="CTID: $CTID Hostname: $CT_HOSTNAME Template: $TEMPLATE Storage: $CT_STORAGE (${DISK_GB}G) CPU/RAM: ${CORES} cores / ${MEMORY}MB (+${SWAP}MB swap) Bridge: $BRIDGE${VLAN:+ (vlan $VLAN)} Network: ${NET_MODE}${STATIC_IP_CIDR:+ $STATIC_IP_CIDR via $STATIC_GW} Priv: $([[ $UNPRIVILEGED -eq 1 ]] && echo unprivileged || echo privileged) Grafana: port $GRAFANA_PORT" whiptail --title "Confirm" --yesno "$SUMMARY Proceed?" 20 65 || exit 1 # ---------- build net string ---------- if [[ "$NET_MODE" == "static" ]]; then NET_STR="name=eth0,bridge=${BRIDGE},ip=${STATIC_IP_CIDR},gw=${STATIC_GW}" else NET_STR="name=eth0,bridge=${BRIDGE},ip=dhcp" fi [[ -n "$VLAN" ]] && NET_STR="${NET_STR},tag=${VLAN}" # ---------- create + start ---------- CREATE_ARGS=( "$CTID" "local:vztmpl/${TEMPLATE}" --hostname "$CT_HOSTNAME" --cores "$CORES" --memory "$MEMORY" --swap "$SWAP" --rootfs "${CT_STORAGE}:${DISK_GB}" --net0 "$NET_STR" --unprivileged "$UNPRIVILEGED" --onboot 1 --features "nesting=1" --password "$ROOT_PASSWORD" ) [[ -n "${SSH_PUBKEY_FILE:-}" && -f "${SSH_PUBKEY_FILE:-}" ]] && CREATE_ARGS+=(--ssh-public-keys "$SSH_PUBKEY_FILE") msg_info "Creating CT $CTID ($CT_HOSTNAME)..." pct create "${CREATE_ARGS[@]}" >/dev/null msg_ok "Container created." msg_info "Starting CT $CTID..." pct start "$CTID" msg_info "Waiting for network..." for i in {1..30}; do pct exec "$CTID" -- sh -c "true" >/dev/null 2>&1 && pct exec "$CTID" -- ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 && break sleep 2 [[ $i -eq 30 ]] && die "CT never came up with network access." done msg_ok "Network is up." # ---------- install grafana inside the CT ---------- run() { pct exec "$CTID" -- bash -c "$1"; } msg_info "Refreshing zypper repos..." run "zypper --non-interactive refresh" >/dev/null msg_ok "Repos refreshed." msg_info "Installing prerequisites (curl, gpg2, firewalld)..." run "zypper --non-interactive install curl gpg2 firewalld" >/dev/null msg_ok "Prerequisites installed." msg_info "Adding Grafana repo..." run "zypper -n ar --refresh https://rpm.grafana.com grafana || true" run "zypper --gpg-auto-import-keys --non-interactive refresh" >/dev/null msg_ok "Grafana repo added." msg_info "Installing Grafana..." run "zypper --non-interactive install grafana" >/dev/null msg_ok "Grafana installed." msg_info "Configuring Grafana (port ${GRAFANA_PORT}, admin password)..." run "sed -i '/^\[server\]/,/^\[/{s/^;\{0,1\}[[:space:]]*http_port[[:space:]]*=.*/http_port = ${GRAFANA_PORT}/}' /etc/grafana/grafana.ini" run "sed -i '/^\[security\]/,/^\[/{s/^;\{0,1\}[[:space:]]*admin_password[[:space:]]*=.*/admin_password = ${GRAFANA_ADMIN_PASSWORD}/}' /etc/grafana/grafana.ini" msg_ok "Grafana configured." msg_info "Enabling firewalld and opening ports 22/${GRAFANA_PORT}..." run "systemctl enable --now firewalld" >/dev/null run "firewall-cmd --permanent --add-port=22/tcp" >/dev/null run "firewall-cmd --permanent --add-port=${GRAFANA_PORT}/tcp" >/dev/null run "firewall-cmd --reload" >/dev/null msg_ok "Firewall configured." msg_info "Starting Grafana..." run "systemctl enable --now grafana-server" >/dev/null msg_info "Waiting for Grafana to answer..." for i in {1..15}; do run "curl -sf http://localhost:${GRAFANA_PORT}/api/health >/dev/null" && break sleep 3 [[ $i -eq 15 ]] && die "Grafana did not come up in time — check: pct exec $CTID -- systemctl status grafana-server" done msg_ok "Grafana is up." # ---------- summary ---------- CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') whiptail --title "$APP LXC Installer — Done" --msgbox \ "Grafana is running. CT ID: $CTID URL: http://${CT_IP}:${GRAFANA_PORT} User: admin Password: ${GRAFANA_ADMIN_PASSWORD} Root pass: ${ROOT_PASSWORD} (also printed below in the terminal)" 16 65 cat <