diff --git a/opensuse-ct.sh b/opensuse-ct.sh new file mode 100755 index 0000000..eecf90b --- /dev/null +++ b/opensuse-ct.sh @@ -0,0 +1,234 @@ +#!/usr/bin/env bash +# +# docker-opensuse-ct.sh — interactive (whiptail) installer that provisions an +# openSUSE LXC container on Proxmox. +# +# Same UX pattern as opensuse-ct.sh: Default/Advanced menu, whiptail +# prompts, then a scripted pct create + pct exec install flow. +# +# Run this ON the Proxmox host as root: +# ./opensuse-ct.sh + +set -euo pipefail + +APP="opensuse" + +# ---------- 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." \ + 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="opensuse" + CT_STORAGE=$(pvesm status -content rootdir | awk 'NR==2{print $1}') + [[ -n "$CT_STORAGE" ]] || die "No rootdir-capable storage found." + DISK_GB=4 + CORES=2 + MEMORY=1024 + SWAP=512 + BRIDGE="vmbr0" + VLAN="" + NET_MODE="dhcp" + UNPRIVILEGED=1 + ROOT_PASSWORD=$(tr -dc 'A-Za-z0-9' &1 1>&2 2>&3) || exit 1 + CT_HOSTNAME=$(whiptail --inputbox "Hostname" 8 58 "opensuse" --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 70 "16" --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 "2048" --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.60/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?" 12 70; then + UNPRIVILEGED=1 + else + UNPRIVILEGED=0 + fi + + ROOT_PASSWORD=$(whiptail --passwordbox "Root password for the CT" 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" --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) +Features: nesting=1,keyctl=1" + +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,keyctl=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 docker 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)..." +run "zypper --non-interactive install curl" >/dev/null +msg_ok "Prerequisites installed." + +msg_info "updating os..." +run "zypper --non-interactive up" >/dev/null +msg_ok "OS upgraded." + +msg_info "Install nano and ssh utils..." +run "zypper in openssh-clients nano" >/dev/null +msg_ok "Tools installed." + + +# ---------- summary ---------- +CT_IP=$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}') + +whiptail --title "$APP LXC Installer — Done" --msgbox \ +"LXC is running. + +CT ID: $CTID +IP: ${CT_IP} +Root pass: ${ROOT_PASSWORD} + +Enter with: pct enter $CTID +(also printed below in the terminal)" 18 65 + +cat <