229 lines
8.8 KiB
Bash
229 lines
8.8 KiB
Bash
#!/usr/bin/env bash
|
|
# Runs on the Proxmox host as the claude-deploy user (restricted sudo: only
|
|
# pct create/set/start/stop/exec/status/list and pveam update/list/download
|
|
# for VMIDs 200-299). Creates the Graylog LXC container and triggers the
|
|
# in-container install. Idempotent: safe to re-run.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
# ---- defaults (override via flags or env) ----------------------------------
|
|
VMID="${VMID:-}"
|
|
CT_HOSTNAME="${CT_HOSTNAME:-graylog}"
|
|
CORES="${CORES:-4}"
|
|
MEMORY_MB="${MEMORY_MB:-8192}"
|
|
SWAP_MB="${SWAP_MB:-512}"
|
|
DISK_GB="${DISK_GB:-50}"
|
|
TEMPLATE_STORAGE="${TEMPLATE_STORAGE:-local-btrfs}"
|
|
ROOTFS_STORAGE="${ROOTFS_STORAGE:-EX-Ceph}"
|
|
BRIDGE="${BRIDGE:-vmbr0}"
|
|
VLAN_TAG="${VLAN_TAG:-}"
|
|
IP_CIDR="${IP_CIDR:-}"
|
|
GATEWAY="${GATEWAY:-}"
|
|
NAMESERVER="${NAMESERVER:-1.1.1.1}"
|
|
SEARCHDOMAIN="${SEARCHDOMAIN:-}"
|
|
TIMEZONE="${TIMEZONE:-Europe/Kyiv}"
|
|
DEBIAN_TEMPLATE_GLOB="${DEBIAN_TEMPLATE_GLOB:-debian-12-standard_*_amd64.tar.zst}"
|
|
GRAYLOG_EXTERNAL_URI="${GRAYLOG_EXTERNAL_URI:-}"
|
|
DISCORD_WEBHOOK_URL="${DISCORD_WEBHOOK_URL:-}"
|
|
|
|
# Colors only when stderr is an actual terminal (never garble log files/pipes).
|
|
if [ -t 2 ]; then
|
|
C_RESET=$'\033[0m'; C_CYAN=$'\033[36m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_RED=$'\033[1;31m'
|
|
else
|
|
C_RESET=''; C_CYAN=''; C_GREEN=''; C_YELLOW=''; C_RED=''
|
|
fi
|
|
|
|
log() { echo "${C_CYAN}[create-graylog-lxc]${C_RESET} $*" >&2; }
|
|
ok() { echo "${C_GREEN}[create-graylog-lxc] ✓${C_RESET} $*" >&2; }
|
|
skip() { echo "${C_YELLOW}[create-graylog-lxc] ⏭${C_RESET} $*" >&2; }
|
|
die() { echo "${C_RED}[create-graylog-lxc] ✗ ERROR:${C_RESET} $*" >&2; exit 1; }
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 --ip 10.254.254.202/24 --gw 10.254.254.235 --vlan 1254 [options]
|
|
|
|
Required:
|
|
--ip <cidr> Static IP for the container, e.g. 10.254.254.202/24
|
|
--gw <ip> Gateway IP
|
|
--external-uri <url> Public URL used for GRAYLOG_HTTP_EXTERNAL_URI, e.g. http://93.171.241.5:9000/
|
|
|
|
Optional (sane defaults shown):
|
|
--vmid <200-299> default: first free VMID in 200-299
|
|
--hostname <name> default: graylog
|
|
--cores <n> default: 4
|
|
--memory <MB> default: 8192
|
|
--swap <MB> default: 512
|
|
--disk <GB> default: 50
|
|
--bridge <name> default: vmbr0
|
|
--vlan <tag> default: none (untagged)
|
|
--nameserver <ip> default: 1.1.1.1
|
|
--searchdomain <domain> default: (none)
|
|
--timezone <tz> default: Europe/Kyiv
|
|
--template-storage <s> default: local-btrfs (must support content type vztmpl)
|
|
--rootfs-storage <s> default: EX-Ceph (container disk)
|
|
--discord-webhook <url> optional, passed through to install-graylog.sh
|
|
|
|
Everything can also be set via environment variables of the same name in
|
|
UPPER_SNAKE_CASE (see top of script).
|
|
EOF
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--vmid) VMID="$2"; shift 2 ;;
|
|
--hostname) CT_HOSTNAME="$2"; shift 2 ;;
|
|
--cores) CORES="$2"; shift 2 ;;
|
|
--memory) MEMORY_MB="$2"; shift 2 ;;
|
|
--swap) SWAP_MB="$2"; shift 2 ;;
|
|
--disk) DISK_GB="$2"; shift 2 ;;
|
|
--bridge) BRIDGE="$2"; shift 2 ;;
|
|
--vlan) VLAN_TAG="$2"; shift 2 ;;
|
|
--ip) IP_CIDR="$2"; shift 2 ;;
|
|
--gw) GATEWAY="$2"; shift 2 ;;
|
|
--nameserver) NAMESERVER="$2"; shift 2 ;;
|
|
--searchdomain) SEARCHDOMAIN="$2"; shift 2 ;;
|
|
--timezone) TIMEZONE="$2"; shift 2 ;;
|
|
--template-storage) TEMPLATE_STORAGE="$2"; shift 2 ;;
|
|
--rootfs-storage) ROOTFS_STORAGE="$2"; shift 2 ;;
|
|
--external-uri) GRAYLOG_EXTERNAL_URI="$2"; shift 2 ;;
|
|
--discord-webhook) DISCORD_WEBHOOK_URL="$2"; shift 2 ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) die "unknown argument: $1 (see --help)" ;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$IP_CIDR" ] || { usage; die "--ip is required"; }
|
|
[ -n "$GATEWAY" ] || { usage; die "--gw is required"; }
|
|
[ -n "$GRAYLOG_EXTERNAL_URI" ] || { usage; die "--external-uri is required"; }
|
|
|
|
# ---------------------------------------------------------------------------
|
|
pick_free_vmid() {
|
|
local used id
|
|
used="$(sudo /usr/sbin/pct list | awk 'NR>1{print $1}')"
|
|
for id in $(seq 200 299); do
|
|
if ! echo "$used" | grep -qx "$id"; then
|
|
echo "$id"
|
|
return
|
|
fi
|
|
done
|
|
die "no free VMID in range 200-299"
|
|
}
|
|
|
|
[ -n "$VMID" ] || VMID="$(pick_free_vmid)"
|
|
case "$VMID" in
|
|
2[0-9][0-9]) ;;
|
|
*) die "VMID must be in range 200-299, got: $VMID" ;;
|
|
esac
|
|
|
|
CONTAINER_EXISTS=0
|
|
if sudo /usr/sbin/pct list | awk 'NR>1{print $1}' | grep -qx "$VMID"; then
|
|
CONTAINER_EXISTS=1
|
|
skip "container VMID $VMID already exists, reusing it"
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
ensure_template() {
|
|
log "Checking for Debian 12 template on storage '$TEMPLATE_STORAGE'..."
|
|
local found
|
|
found="$(sudo /usr/bin/pveam list "$TEMPLATE_STORAGE" 2>/dev/null | awk -v s="$TEMPLATE_STORAGE" '$1 ~ s":vztmpl/debian-12-standard" {print $1}' | tail -1)"
|
|
if [ -n "$found" ]; then
|
|
TEMPLATE_VOLID="$found"
|
|
skip "template already cached: $TEMPLATE_VOLID"
|
|
return
|
|
fi
|
|
log "No cached template found, downloading (this can take a minute)..."
|
|
sudo /usr/bin/pveam update
|
|
local latest
|
|
latest="$(sudo /usr/bin/pveam available --section system | awk '/debian-12-standard/{print $2}' | sort -V | tail -1)"
|
|
[ -n "$latest" ] || die "could not find a debian-12-standard template in the pveam catalog"
|
|
sudo /usr/bin/pveam download "$TEMPLATE_STORAGE" "$latest"
|
|
TEMPLATE_VOLID="$(sudo /usr/bin/pveam list "$TEMPLATE_STORAGE" | awk -v s="$TEMPLATE_STORAGE" '$1 ~ s":vztmpl/debian-12-standard" {print $1}' | tail -1)"
|
|
[ -n "$TEMPLATE_VOLID" ] || die "template download reported success but volume not found"
|
|
ok "downloaded template: $TEMPLATE_VOLID"
|
|
}
|
|
|
|
create_container() {
|
|
local net0="name=eth0,bridge=$BRIDGE,ip=${IP_CIDR},gw=${GATEWAY}"
|
|
[ -n "$VLAN_TAG" ] && net0="${net0},tag=${VLAN_TAG}"
|
|
|
|
log "Creating container $VMID ($CT_HOSTNAME) from $TEMPLATE_VOLID ..."
|
|
sudo /usr/sbin/pct create "$VMID" "$TEMPLATE_VOLID" \
|
|
--hostname "$CT_HOSTNAME" \
|
|
--cores "$CORES" \
|
|
--memory "$MEMORY_MB" \
|
|
--swap "$SWAP_MB" \
|
|
--rootfs "${ROOTFS_STORAGE}:${DISK_GB}" \
|
|
--net0 "$net0" \
|
|
--unprivileged 1 \
|
|
--features nesting=1,keyctl=1 \
|
|
--onboot 1 \
|
|
--nameserver "$NAMESERVER" \
|
|
${SEARCHDOMAIN:+--searchdomain "$SEARCHDOMAIN"} \
|
|
--timezone "$TIMEZONE" \
|
|
--description "Graylog centralized log server"
|
|
ok "container $VMID created"
|
|
}
|
|
|
|
start_container_and_wait_net() {
|
|
log "Starting container $VMID..."
|
|
sudo /usr/sbin/pct start "$VMID"
|
|
log "Waiting for network..."
|
|
local i
|
|
for i in $(seq 1 20); do
|
|
if sudo /usr/sbin/pct exec "$VMID" -- ping -c1 -W2 "$(echo "$GATEWAY")" >/dev/null 2>&1; then
|
|
ok "network is up"
|
|
return
|
|
fi
|
|
sleep 2
|
|
done
|
|
die "container did not get network connectivity within ~40s"
|
|
}
|
|
|
|
check_apparmor_hint() {
|
|
# We cannot fix this ourselves (no permission to edit /etc/pve/lxc/*.conf),
|
|
# so just remind the operator up front; install-graylog.sh will hard-stop
|
|
# with the same instructions if it's actually needed.
|
|
log "NOTE: if Docker fails inside the container with a sysctl/AppArmor permission error,"
|
|
log " the host admin must run:"
|
|
log " echo 'lxc.apparmor.profile: unconfined' >> /etc/pve/lxc/${VMID}.conf && pct reboot ${VMID}"
|
|
}
|
|
|
|
copy_install_payload() {
|
|
log "Copying install payload into the container (/opt/graylog-deploy)..."
|
|
sudo /usr/sbin/pct exec "$VMID" -- mkdir -p /opt/graylog-deploy
|
|
tar czf - -C "$SCRIPT_DIR" rules streams pipelines alerts dashboards install-graylog.sh docker-compose.yml nftables.conf \
|
|
| sudo /usr/sbin/pct exec "$VMID" -- bash -c 'tar xzf - -C /opt/graylog-deploy'
|
|
sudo /usr/sbin/pct exec "$VMID" -- chmod +x /opt/graylog-deploy/install-graylog.sh
|
|
}
|
|
|
|
run_install() {
|
|
log "Running install-graylog.sh inside the container..."
|
|
# GRAYLOG_ADMIN_PASSWORD is only needed on a re-run after the one-time
|
|
# credentials file was deleted (as instructed in the README); forward it
|
|
# through if the operator set it, otherwise install-graylog.sh reads the
|
|
# one-time file itself on a fresh install.
|
|
sudo /usr/sbin/pct exec "$VMID" -- env \
|
|
"GRAYLOG_EXTERNAL_URI=$GRAYLOG_EXTERNAL_URI" \
|
|
"DISCORD_WEBHOOK_URL=$DISCORD_WEBHOOK_URL" \
|
|
${GRAYLOG_ADMIN_PASSWORD:+"GRAYLOG_ADMIN_PASSWORD=$GRAYLOG_ADMIN_PASSWORD"} \
|
|
bash /opt/graylog-deploy/install-graylog.sh
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
if [ "$CONTAINER_EXISTS" -eq 0 ]; then
|
|
ensure_template
|
|
create_container
|
|
start_container_and_wait_net
|
|
check_apparmor_hint
|
|
else
|
|
sudo /usr/sbin/pct status "$VMID" | grep -q running || sudo /usr/sbin/pct start "$VMID"
|
|
fi
|
|
|
|
copy_install_payload
|
|
run_install
|
|
|
|
echo >&2
|
|
echo "${C_GREEN}==================================================${C_RESET}" >&2
|
|
echo "${C_GREEN} DONE - VMID=$VMID, Web UI: $GRAYLOG_EXTERNAL_URI${C_RESET}" >&2
|
|
echo "${C_GREEN}==================================================${C_RESET}" >&2
|