cockpit-kata/installer.sh

729 lines
23 KiB
Bash
Executable File

#!/usr/bin/env bash
# ============================================================================
# cockpit-kata-containers — Automated Installer
#
# Detects the host OS, installs all required dependencies, builds the Cockpit
# module, deploys it, and starts/restarts required services.
#
# Usage:
# sudo ./installer.sh # Full install
# sudo ./installer.sh --dev # Standalone dev mode only (no Cockpit)
# sudo ./installer.sh --uninstall # Remove the module
# ./installer.sh --check # Check prerequisites without installing
#
# Supported OS families:
# - RHEL / Fedora / CentOS Stream / Rocky / Alma
# - Debian / Ubuntu
# - openSUSE / SLES
# - Arch Linux
# - Clear Linux
# ============================================================================
set -euo pipefail
# ─── Colors ─────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# ─── Globals ────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COCKPIT_MODULE_DIR="${SCRIPT_DIR}/cockpit-kata"
COCKPIT_INSTALL_DIR="/usr/share/cockpit/kata"
KATA_MONITOR_PORT="${KATA_MONITOR_PORT:-8090}"
KATA_MONITOR_ADDR="${KATA_MONITOR_ADDR:-127.0.0.1}"
MODE="full" # full | dev | uninstall | check
LOG_FILE="/tmp/cockpit-kata-install.log"
CRICTL_VERSION="1.28.0"
# ─── Helpers ────────────────────────────────────────────────────────────────
log() { echo -e "${GREEN}[cockpit-kata]${NC} $*"; }
warn() { echo -e "${YELLOW}[cockpit-kata]${NC} WARNING: $*"; }
error() { echo -e "${RED}[cockpit-kata]${NC} ERROR: $*" >&2; }
info() { echo -e "${CYAN}[cockpit-kata]${NC} $*"; }
separator(){ echo -e "${BLUE}──────────────────────────────────────────────────────${NC}"; }
run_cmd() {
echo -e "${CYAN}+ $*${NC}" >> "$LOG_FILE"
if ! "$@" >> "$LOG_FILE" 2>&1; then
error "Command failed: $*"
error "Check log: $LOG_FILE"
return 1
fi
}
run_cmd_verbose() {
echo -e "${CYAN}+ $*${NC}"
"$@"
}
# ─── Parse Arguments ────────────────────────────────────────────────────────
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--dev) MODE="dev"; shift ;;
--uninstall) MODE="uninstall"; shift ;;
--check) MODE="check"; shift ;;
--help|-h)
echo "Usage: sudo ./installer.sh [--dev|--uninstall|--check|--help]"
echo ""
echo " (default) Full Cockpit module install"
echo " --dev Standalone dev mode only (no Cockpit required)"
echo " --uninstall Remove the Cockpit module"
echo " --check Check prerequisites without installing"
echo " --help Show this help"
exit 0
;;
*) error "Unknown argument: $1"; exit 1 ;;
esac
done
}
# ─── OS Detection ───────────────────────────────────────────────────────────
detect_os() {
if [[ -f /etc/os-release ]]; then
# shellcheck disable=SC1091
source /etc/os-release
OS_ID="${ID:-unknown}"
OS_VERSION="${VERSION_ID:-unknown}"
OS_NAME="${PRETTY_NAME:-$OS_ID $OS_VERSION}"
elif [[ -f /etc/redhat-release ]]; then
OS_ID="rhel"
OS_VERSION=$(rpm -q --qf '%{VERSION}' redhat-release 2>/dev/null || echo "unknown")
OS_NAME="Red Hat Enterprise Linux $OS_VERSION"
else
OS_ID="unknown"
OS_VERSION="unknown"
OS_NAME="Unknown Linux"
fi
# Determine family
case "$OS_ID" in
rhel|fedora|centos|rocky|almalinux|ol|anolis)
OS_FAMILY="rhel"
PKG_MANAGER="dnf"
;;
debian|ubuntu|pop|elementary|linuxmint|kali)
OS_FAMILY="debian"
PKG_MANAGER="apt"
;;
opensuse*|sles|suse)
OS_FAMILY="suse"
PKG_MANAGER="zypper"
;;
arch|manjaro|endeavouros|garuda)
OS_FAMILY="arch"
PKG_MANAGER="pacman"
;;
clear-linux-os)
OS_FAMILY="clear"
PKG_MANAGER="swupd"
;;
*)
OS_FAMILY="unknown"
PKG_MANAGER="unknown"
;;
esac
log "Detected OS: ${OS_NAME}"
log "OS Family: ${OS_FAMILY}"
log "Pkg Manager: ${PKG_MANAGER}"
}
# ─── Version Comparison ────────────────────────────────────────────────────
version_gte() {
# Returns 0 if $1 >= $2
printf '%s\n%s' "$1" "$2" | sort -V -C
}
# ─── Generic command check ─────────────────────────────────────────────────
# check_command COMMAND [MIN_VERSION] [VERSION_FLAG]
# COMMAND — executable to look up via command -v
# MIN_VERSION — minimum required version (optional)
# VERSION_FLAG— flag passed to COMMAND to obtain its version string (default: --version)
check_command() {
local cmd="$1"
local min_ver="${2:-}"
local ver_flag="${3:---version}"
if ! command -v "$cmd" &>/dev/null; then
warn "${cmd}: not installed"
return 1
fi
# No version constraint — just confirm presence
if [[ -z "$min_ver" ]]; then
local ver_str
ver_str=$("$cmd" $ver_flag 2>/dev/null | awk '{print $NF}' | tr -d ',' || echo "0")
log "${cmd}: ${ver_str}"
return 0
fi
# Version-constrained check
local cur_ver
cur_ver=$("$cmd" $ver_flag 2>/dev/null | head -1 | awk '{print $2}' | tr -d 'v,' || echo "0")
if version_gte "$cur_ver" "$min_ver"; then
log "${cmd}: ${cur_ver} (>= ${min_ver} required)"
return 0
else
warn "${cmd}: ${cur_ver} found but < ${min_ver} required"
return 1
fi
}
# ─── Prerequisite Checks ───────────────────────────────────────────────────
check_root() {
if [[ $EUID -ne 0 && "$MODE" != "check" && "$MODE" != "dev" ]]; then
error "This installer must be run as root for Cockpit module installation."
error "Use --dev for standalone development mode (no root required)."
exit 1
fi
}
check_kvm() {
if [[ -e /dev/kvm ]]; then
log "KVM: /dev/kvm available"
return 0
else
warn "KVM: /dev/kvm not found — hardware virtualization may not be enabled"
warn " Enable VT-x/AMD-V in BIOS/UEFI and load the kvm module:"
warn " sudo modprobe kvm_intel (Intel) or sudo modprobe kvm_amd (AMD)"
return 1
fi
}
check_cockpit() { check_command cockpit-bridge "286"; }
check_kata_runtime() { check_command kata-runtime; }
check_containerd() { check_command containerd; }
check_crictl() { check_command crictl; }
check_node() { check_command node "18" "--version"; }
check_npm() { check_command npm; }
check_kata_monitor() {
if curl -sf "http://${KATA_MONITOR_ADDR}:${KATA_MONITOR_PORT}/metrics" >/dev/null 2>&1; then
log "kata-monitor: reachable at ${KATA_MONITOR_ADDR}:${KATA_MONITOR_PORT}"
return 0
else
warn "kata-monitor: not reachable at ${KATA_MONITOR_ADDR}:${KATA_MONITOR_PORT} (metrics will be unavailable)"
return 1
fi
}
# ─── Run all checks (array-driven) ─────────────────────────────────────────
run_all_checks() {
separator
info "Running prerequisite checks..."
separator
# Each entry: "check_function:severity" where severity is pass|fail|warn
local -a checks=(
"check_kvm:fail"
"check_cockpit:warn"
"check_kata_runtime:warn"
"check_containerd:warn"
"check_crictl:warn"
"check_node:fail"
"check_npm:fail"
"check_kata_monitor:warn"
)
local pass=0 fail=0 warn_count=0
local entry check_fn severity
for entry in "${checks[@]}"; do
check_fn="${entry%%:*}"
severity="${entry##*:}"
if "$check_fn"; then
((pass++)) || true
else
case "$severity" in
fail) ((fail++)) || true ;;
warn) ((warn_count++)) || true ;;
esac
fi
done
separator
echo -e " Passed: ${GREEN}${pass}${NC} Failed: ${RED}${fail}${NC} Warnings: ${YELLOW}${warn_count}${NC}"
separator
if [[ $fail -gt 0 ]]; then
if [[ "$MODE" == "check" ]]; then
error "Prerequisites not met. Install missing components and re-run."
return 1
fi
warn "Some critical prerequisites are missing — the installer will attempt to install them."
fi
}
# ─── crictl installation (shared across all OS families) ────────────────────
install_crictl() {
command -v crictl &>/dev/null && return 0
info "Installing crictl v${CRICTL_VERSION}..."
run_cmd curl -Lo /tmp/crictl.tar.gz \
"https://github.com/kubernetes-sigs/cri-tools/releases/download/v${CRICTL_VERSION}/crictl-v${CRICTL_VERSION}-linux-amd64.tar.gz"
run_cmd tar -xzf /tmp/crictl.tar.gz -C /usr/local/bin crictl
run_cmd rm -f /tmp/crictl.tar.gz
}
# ─── Package Installation ──────────────────────────────────────────────────
install_packages_rhel() {
log "Installing packages via dnf..."
# Enable EPEL if needed (CentOS/Rocky/Alma)
if [[ "$OS_ID" == "centos" || "$OS_ID" == "rocky" || "$OS_ID" == "almalinux" ]]; then
run_cmd dnf install -y epel-release || true
fi
local pkgs=(
cockpit
cockpit-bridge
nodejs
npm
qemu-kvm
libvirt
)
# Add Kata Containers repo if kata-runtime is missing
if ! command -v kata-runtime &>/dev/null; then
info "kata-runtime not found — adding Kata Containers repo..."
command -v dnf copr &>/dev/null && run_cmd dnf copr enable -y @katacontainers/kata-containers || true
pkgs+=(kata-containers-runtime)
fi
run_cmd dnf install -y "${pkgs[@]}"
install_crictl
}
install_packages_debian() {
log "Installing packages via apt..."
run_cmd apt-get update -y
local pkgs=(
cockpit
cockpit-bridge
nodejs
npm
qemu-kvm
libvirt-daemon-system
)
# Add Kata Containers apt repo if kata-runtime is missing
if ! command -v kata-runtime &>/dev/null; then
info "kata-runtime not found — adding Kata Containers apt repo..."
local kata_repo_key="https://packages.katacontainers.io/kata-containers.repo.key"
local kata_repo_url="https://packages.katacontainers.io/stable/debian"
run_cmd curl -Lo /usr/share/keyrings/kata-containers.gpg "$kata_repo_key" || true
if [[ -f /usr/share/keyrings/kata-containers.gpg ]]; then
echo "deb [signed-by=/usr/share/keyrings/kata-containers.gpg] $kata_repo_url /" \
> /etc/apt/sources.list.d/kata-containers.list
run_cmd apt-get update -y
fi
pkgs+=(kata-containers-runtime)
fi
run_cmd apt-get install -y "${pkgs[@]}"
install_crictl
}
install_packages_suse() {
log "Installing packages via zypper..."
local pkgs=(
cockpit
cockpit-bridge
nodejs18
npm18
qemu-kvm
libvirt-daemon
)
if ! command -v kata-runtime &>/dev/null; then
info "kata-runtime not found — you may need to install it manually on SUSE."
warn "See: https://github.com/kata-containers/kata-containers/blob/main/docs/install/README.md"
fi
run_cmd zypper install -y "${pkgs[@]}"
install_crictl
}
install_packages_arch() {
log "Installing packages via pacman..."
local pkgs=(
cockpit
nodejs
npm
qemu-headless
libvirt
)
if ! command -v kata-runtime &>/dev/null; then
warn "kata-runtime: Install from AUR (e.g., kata-containers-bin) or build from source."
warn "See: https://github.com/kata-containers/kata-containers/blob/main/docs/install/README.md"
fi
run_cmd pacman -Sy --noconfirm --needed "${pkgs[@]}"
install_crictl
}
install_packages_clear() {
log "Installing bundles via swupd..."
run_cmd swupd bundle-add cockpit
run_cmd swupd bundle-add nodejs-basic
if ! command -v kata-runtime &>/dev/null; then
run_cmd swupd bundle-add containers-virt || warn "kata-runtime bundle not found"
fi
install_crictl
}
install_packages() {
case "$OS_FAMILY" in
rhel) install_packages_rhel ;;
debian) install_packages_debian ;;
suse) install_packages_suse ;;
arch) install_packages_arch ;;
clear) install_packages_clear ;;
*)
error "Unsupported OS family: ${OS_FAMILY}"
error "Install these packages manually: cockpit, nodejs (>=18), npm, qemu-kvm, kata-runtime, crictl, containerd"
exit 1
;;
esac
}
# ─── Service Management ────────────────────────────────────────────────────
enable_services() {
log "Enabling and starting services..."
# Cockpit socket
if systemctl list-unit-files cockpit.socket &>/dev/null; then
run_cmd_verbose systemctl enable --now cockpit.socket
log "Cockpit socket enabled (https://localhost:9090)"
else
warn "cockpit.socket not found — Cockpit may need manual service start"
fi
# libvirtd (for QEMU backend)
if systemctl list-unit-files libvirtd.service &>/dev/null; then
run_cmd_verbose systemctl enable --now libvirtd
log "libvirtd started"
fi
# containerd
if systemctl list-unit-files containerd.service &>/dev/null; then
run_cmd_verbose systemctl enable --now containerd
log "containerd started"
else
warn "containerd.service not found — install containerd for CRI support"
fi
# KVM kernel modules
if [[ ! -e /dev/kvm ]]; then
info "Attempting to load KVM kernel module..."
modprobe kvm_intel 2>/dev/null || modprobe kvm_amd 2>/dev/null || warn "Could not load KVM module"
if [[ -e /dev/kvm ]]; then
log "KVM module loaded successfully"
else
warn "KVM still not available — hardware virtualization may be disabled in BIOS"
fi
fi
# kata-monitor (optional)
if command -v kata-monitor &>/dev/null; then
if ! curl -sf "http://${KATA_MONITOR_ADDR}:${KATA_MONITOR_PORT}/metrics" >/dev/null 2>&1; then
info "Starting kata-monitor on ${KATA_MONITOR_ADDR}:${KATA_MONITOR_PORT}..."
nohup kata-monitor --listen-address "${KATA_MONITOR_ADDR}:${KATA_MONITOR_PORT}" \
>/tmp/kata-monitor.log 2>&1 &
sleep 2
if curl -sf "http://${KATA_MONITOR_ADDR}:${KATA_MONITOR_PORT}/metrics" >/dev/null 2>&1; then
log "kata-monitor started successfully"
else
warn "kata-monitor did not start — metrics will be unavailable"
fi
fi
else
warn "kata-monitor not found — install kata-containers for metrics support"
fi
}
# ─── Build & Deploy ────────────────────────────────────────────────────────
install_qcrows_tools() {
log "Installing QCrows CLI tools to /usr/local/bin/..."
local -a tools=(
"qcrows-pack"
"qcrows-verify"
"qcrows-inspect"
"qcrows-export"
"qcrows-initrd-regen"
)
local installed=0
local skipped=0
# Iterate using array — each tool installed independently
local tool
for tool in "${tools[@]}"; do
local src="${SCRIPT_DIR}/${tool}"
if [[ -f "$src" ]]; then
run_cmd_verbose install -m 0755 "$src" "/usr/local/bin/${tool}"
((installed++)) || true
else
warn "QCrows tool not found: ${src}"
((skipped++)) || true
fi
done
log "QCrows tools: ${installed} installed, ${skipped} skipped"
}
build_cockpit_module() {
log "Building Cockpit module..."
if [[ ! -d "$COCKPIT_MODULE_DIR" ]]; then
error "Cockpit module directory not found: ${COCKPIT_MODULE_DIR}"
error "Make sure you're running this script from the project root."
exit 1
fi
cd "$COCKPIT_MODULE_DIR"
log "Installing build dependencies..."
run_cmd npm install
log "Building production bundle..."
run_cmd npm run build
if [[ ! -f dist/index.js ]]; then
error "Build failed — dist/index.js not found"
exit 1
fi
log "Build successful: $(du -sh dist/ | cut -f1)"
cd "$SCRIPT_DIR"
}
deploy_cockpit_module() {
log "Deploying Cockpit module to ${COCKPIT_INSTALL_DIR}..."
# Remove old installation
if [[ -d "$COCKPIT_INSTALL_DIR" ]]; then
run_cmd_verbose rm -rf "$COCKPIT_INSTALL_DIR"
fi
# Copy built files
run_cmd_verbose mkdir -p "$COCKPIT_INSTALL_DIR"
run_cmd_verbose cp -r "${COCKPIT_MODULE_DIR}/dist/"* "$COCKPIT_INSTALL_DIR/"
if cockpit-bridge --packages 2>/dev/null | grep -q kata; then
log "Cockpit module registered: kata"
else
warn "Module not immediately visible — restart Cockpit to pick it up"
fi
log "Module deployed to ${COCKPIT_INSTALL_DIR}"
}
build_standalone() {
log "Setting up standalone development mode..."
if [[ ! -f "${SCRIPT_DIR}/package.json" ]]; then
error "package.json not found in ${SCRIPT_DIR}"
exit 1
fi
cd "$SCRIPT_DIR"
run_cmd npm install
log "Standalone mode ready. Run: npm run dev"
log "Then open http://localhost:3000"
}
# ─── Uninstall ─────────────────────────────────────────────────────────────
do_uninstall() {
separator
info "Uninstalling Cockpit Kata Containers module..."
separator
if [[ -d "$COCKPIT_INSTALL_DIR" ]]; then
rm -rf "$COCKPIT_INSTALL_DIR"
log "Removed ${COCKPIT_INSTALL_DIR}"
else
warn "Module not installed at ${COCKPIT_INSTALL_DIR}"
fi
# Remove dev symlink
local dev_link="${HOME}/.local/share/cockpit/kata"
if [[ -L "$dev_link" ]]; then
rm -f "$dev_link"
log "Removed dev symlink ${dev_link}"
fi
# Stop kata-monitor if we started it
if pgrep -f "kata-monitor.*${KATA_MONITOR_PORT}" &>/dev/null; then
info "Stopping kata-monitor..."
pkill -f "kata-monitor.*${KATA_MONITOR_PORT}" || true
log "kata-monitor stopped"
fi
# Remove QCrows CLI tools
local -a tools=("qcrows-pack" "qcrows-verify" "qcrows-inspect" "qcrows-export" "qcrows-initrd-regen")
local tool
for tool in "${tools[@]}"; do
if [[ -f "/usr/local/bin/${tool}" ]]; then
run_cmd_verbose rm -f "/usr/local/bin/${tool}"
fi
done
log "Uninstallation complete."
log "Note: System packages (cockpit, kata-runtime, etc.) were NOT removed."
log " Remove them manually if desired."
}
# ─── RuntimeClass Setup ────────────────────────────────────────────────────
# Iteration is intentional — each RuntimeClass is independently created and a
# failure in one must not prevent the others from being attempted.
setup_runtime_classes() {
if ! command -v kubectl &>/dev/null; then
warn "kubectl not found — skipping RuntimeClass setup"
return
fi
info "Checking for Kata RuntimeClasses..."
local classes=("kata-qemu" "kata-clh" "kata-fc" "kata-dragonball")
for rc in "${classes[@]}"; do
if kubectl get runtimeclass "$rc" &>/dev/null; then
log "RuntimeClass ${rc}: already exists"
else
info "Creating RuntimeClass ${rc}..."
kubectl apply -f - <<EOF 2>/dev/null || warn "Could not create RuntimeClass ${rc}"
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: ${rc}
handler: ${rc}
EOF
fi
done
}
# ─── Main ───────────────────────────────────────────────────────────────────
main() {
parse_args "$@"
echo ""
echo -e "${BLUE} ╔══════════════════════════════════════════════╗${NC}"
echo -e "${BLUE} ║ Cockpit Kata Containers — Installer ║${NC}"
echo -e "${BLUE} ║ VM-isolated container management ║${NC}"
echo -e "${BLUE} ╚══════════════════════════════════════════════╝${NC}"
echo ""
# Initialize log
echo "=== Cockpit Kata Containers Install Log ===" > "$LOG_FILE"
echo "Date: $(date)" >> "$LOG_FILE"
echo "Mode: ${MODE}" >> "$LOG_FILE"
echo "" >> "$LOG_FILE"
# Detect OS
detect_os
# Route to mode
case "$MODE" in
check)
run_all_checks
exit 0
;;
uninstall)
check_root
do_uninstall
exit 0
;;
dev)
info "=== Standalone Development Mode ==="
run_all_checks
build_standalone
echo ""
log "Setup complete! Run the following to start:"
echo ""
echo -e " ${CYAN}cd ${SCRIPT_DIR}${NC}"
echo -e " ${CYAN}npm run dev${NC}"
echo ""
echo -e " Open ${CYAN}http://localhost:3000${NC} in your browser."
exit 0
;;
full)
check_root
info "=== Full Cockpit Module Install ==="
;;
*)
error "Unknown mode: ${MODE}"
exit 1
;;
esac
# Full install flow
separator
info "Step 1/6: Checking prerequisites"
separator
run_all_checks
separator
info "Step 2/6: Installing system packages"
separator
install_packages
separator
info "Step 3/6: Enabling services"
separator
enable_services
separator
info "Step 4/6: Installing QCrows CLI tools"
separator
install_qcrows_tools
separator
info "Step 5/6: Building Cockpit module"
separator
build_cockpit_module
separator
info "Step 6/6: Deploying module"
separator
deploy_cockpit_module
# Optional: set up RuntimeClasses
separator
info "Optional: Setting up Kubernetes RuntimeClasses"
separator
setup_runtime_classes
# Final summary
echo ""
separator
log "Installation Complete!"
separator
echo ""
echo -e " Cockpit: ${CYAN}https://localhost:9090${NC}"
echo -e " Module path: ${CYAN}${COCKPIT_INSTALL_DIR}${NC}"
echo -e " kata-monitor: ${CYAN}http://${KATA_MONITOR_ADDR}:${KATA_MONITOR_PORT}${NC}"
echo -e " Install log: ${CYAN}${LOG_FILE}${NC}"
echo ""
echo -e " Open Cockpit and navigate to ${GREEN}Kata Containers${NC} in the sidebar."
echo ""
echo -e " To uninstall: ${CYAN}sudo ./installer.sh --uninstall${NC}"
echo -e " To check: ${CYAN}./installer.sh --check${NC}"
echo ""
}
main "$@"