#!/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 - </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 "$@"