Replace monolithic install.sh with modular script architecture
- Remove old single-file installer - Add worker scripts with JSON event streaming protocol: - scripts/prepare.sh: system prep, Docker/Compose installation - scripts/strix.sh: deploy Strix standalone via Docker Compose - scripts/strix-frigate.sh: deploy Strix + Frigate with HW autodetect - scripts/proxmox-lxc-create.sh: create Ubuntu LXC on Proxmox - Add scripts/install.sh: animated frontend with owl display - Update docker-compose.frigate.yml: host networking, internal API port, expanded device comments, GPU image hints
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Strix + Frigate
|
# Strix + Frigate
|
||||||
# Usage: docker compose -f docker-compose.frigate.yml up -d
|
# Usage:
|
||||||
|
# curl -O https://raw.githubusercontent.com/eduard256/Strix/main/docker-compose.frigate.yml
|
||||||
|
# mkdir -p frigate/config frigate/storage
|
||||||
|
# docker compose -f docker-compose.frigate.yml up -d
|
||||||
|
#
|
||||||
|
# Strix UI: http://YOUR_IP:4567
|
||||||
|
# Frigate UI: http://YOUR_IP:8971
|
||||||
|
# Frigate API: http://YOUR_IP:5000 (internal, no auth)
|
||||||
|
# go2rtc UI: http://YOUR_IP:1984 (built into Frigate)
|
||||||
|
# RTSP restream: rtsp://YOUR_IP:8554
|
||||||
|
|
||||||
services:
|
services:
|
||||||
strix:
|
strix:
|
||||||
@@ -9,22 +18,31 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
STRIX_LISTEN: ":4567"
|
STRIX_LISTEN: ":4567"
|
||||||
STRIX_FRIGATE_URL: "http://localhost:8971"
|
STRIX_FRIGATE_URL: "http://localhost:5000"
|
||||||
|
# STRIX_GO2RTC_URL: "http://localhost:1984"
|
||||||
# STRIX_LOG_LEVEL: debug
|
# STRIX_LOG_LEVEL: debug
|
||||||
depends_on:
|
depends_on:
|
||||||
- frigate
|
frigate:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
frigate:
|
frigate:
|
||||||
container_name: frigate
|
container_name: frigate
|
||||||
image: ghcr.io/blakeblackshear/frigate:stable
|
image: ghcr.io/blakeblackshear/frigate:stable
|
||||||
privileged: true
|
privileged: true
|
||||||
|
network_mode: host
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
stop_grace_period: 30s
|
stop_grace_period: 30s
|
||||||
shm_size: "512mb"
|
shm_size: "512mb"
|
||||||
|
# Uncomment devices for your hardware:
|
||||||
# devices:
|
# devices:
|
||||||
# - /dev/bus/usb:/dev/bus/usb # USB Coral
|
# - /dev/bus/usb:/dev/bus/usb # USB Coral
|
||||||
# - /dev/apex_0:/dev/apex_0 # PCIe Coral
|
# - /dev/apex_0:/dev/apex_0 # PCIe Coral
|
||||||
# - /dev/dri/renderD128:/dev/dri/renderD128 # Intel/AMD GPU
|
# - /dev/dri:/dev/dri # Intel/AMD GPU
|
||||||
|
# - /dev/accel:/dev/accel # Intel NPU
|
||||||
|
# - /dev/video11:/dev/video11 # Raspberry Pi 4
|
||||||
|
# For Nvidia GPU use image: ghcr.io/blakeblackshear/frigate:stable-tensorrt
|
||||||
|
# For AMD GPU use image: ghcr.io/blakeblackshear/frigate:stable-rocm
|
||||||
|
# For Rockchip use image: ghcr.io/blakeblackshear/frigate:stable-rk
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- ./frigate/config:/config
|
- ./frigate/config:/config
|
||||||
@@ -33,10 +51,5 @@ services:
|
|||||||
target: /tmp/cache
|
target: /tmp/cache
|
||||||
tmpfs:
|
tmpfs:
|
||||||
size: 1000000000
|
size: 1000000000
|
||||||
ports:
|
|
||||||
- "8971:8971"
|
|
||||||
- "8554:8554"
|
|
||||||
- "8555:8555/tcp"
|
|
||||||
- "8555:8555/udp"
|
|
||||||
environment:
|
environment:
|
||||||
FRIGATE_RTSP_PASSWORD: "password"
|
FRIGATE_RTSP_PASSWORD: "password"
|
||||||
|
|||||||
-894
@@ -1,894 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# =============================================================================
|
|
||||||
# Strix Installer
|
|
||||||
# Universal installer for any Linux distribution.
|
|
||||||
# Installs Docker (via get.docker.com), Docker Compose, detects Frigate/go2rtc,
|
|
||||||
# and deploys Strix via docker compose.
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# curl -fsSL https://raw.githubusercontent.com/eduard256/Strix/main/install.sh | sudo bash
|
|
||||||
# sudo bash install.sh [OPTIONS]
|
|
||||||
#
|
|
||||||
# Options:
|
|
||||||
# --no-logo Suppress ASCII logo (useful when called from another script)
|
|
||||||
# --no-color Disable colored output
|
|
||||||
# --yes, -y Non-interactive mode, accept all defaults
|
|
||||||
# --version TAG Set image tag without prompt (latest/dev/1.0.9)
|
|
||||||
# --verbose, -v Show detailed output from docker commands
|
|
||||||
#
|
|
||||||
# Exit codes:
|
|
||||||
# 0 = success
|
|
||||||
# 1 = docker installation failed
|
|
||||||
# 2 = docker compose not available
|
|
||||||
# 3 = image pull failed
|
|
||||||
# 4 = container failed to start
|
|
||||||
# 5 = healthcheck failed
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Globals
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
STRIX_DIR="/opt/strix"
|
|
||||||
STRIX_PORT="4567"
|
|
||||||
IMAGE="eduard256/strix"
|
|
||||||
LOG_FILE="${STRIX_DIR}/install.log"
|
|
||||||
DIALOG_TIMEOUT=10
|
|
||||||
|
|
||||||
# Flags (overridden by CLI args)
|
|
||||||
SHOW_LOGO=true
|
|
||||||
USE_COLOR=true
|
|
||||||
INTERACTIVE=true
|
|
||||||
VERBOSE=false
|
|
||||||
TAG=""
|
|
||||||
|
|
||||||
# Result variables (printed at the end for parent scripts)
|
|
||||||
INSTALL_MODE="" # install | update
|
|
||||||
FRIGATE_STATUS="none" # found | set | none
|
|
||||||
GO2RTC_STATUS="none" # found | set | none
|
|
||||||
FINAL_VERSION=""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Parse CLI arguments
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case "$1" in
|
|
||||||
--no-logo) SHOW_LOGO=false; shift ;;
|
|
||||||
--no-color) USE_COLOR=false; shift ;;
|
|
||||||
--yes|-y) INTERACTIVE=false; shift ;;
|
|
||||||
--verbose|-v) VERBOSE=true; shift ;;
|
|
||||||
--version) TAG="$2"; shift 2 ;;
|
|
||||||
*) shift ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Color setup
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
setup_colors() {
|
|
||||||
if [[ "$USE_COLOR" == false ]] || [[ "${NO_COLOR:-}" != "" ]] || [[ ! -t 1 ]]; then
|
|
||||||
USE_COLOR=false
|
|
||||||
C_RESET="" C_BOLD="" C_DIM=""
|
|
||||||
C_RED="" C_GREEN="" C_YELLOW="" C_CYAN="" C_MAGENTA="" C_WHITE=""
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
local colors
|
|
||||||
colors=$(tput colors 2>/dev/null || echo 0)
|
|
||||||
if [[ "$colors" -lt 8 ]]; then
|
|
||||||
USE_COLOR=false
|
|
||||||
C_RESET="" C_BOLD="" C_DIM=""
|
|
||||||
C_RED="" C_GREEN="" C_YELLOW="" C_CYAN="" C_MAGENTA="" C_WHITE=""
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
C_RESET="\033[0m"
|
|
||||||
C_BOLD="\033[1m"
|
|
||||||
C_DIM="\033[2m"
|
|
||||||
C_RED="\033[31m"
|
|
||||||
C_GREEN="\033[32m"
|
|
||||||
C_YELLOW="\033[33m"
|
|
||||||
C_CYAN="\033[36m"
|
|
||||||
C_MAGENTA="\033[35m"
|
|
||||||
C_WHITE="\033[97m"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Terminal width helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
term_width() {
|
|
||||||
local w
|
|
||||||
w=$(tput cols 2>/dev/null || echo 80)
|
|
||||||
[[ "$w" -lt 40 ]] && w=40
|
|
||||||
echo "$w"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Print text centered in terminal
|
|
||||||
center() {
|
|
||||||
local text="$1"
|
|
||||||
local w
|
|
||||||
w=$(term_width)
|
|
||||||
# Strip ANSI codes for length calculation
|
|
||||||
local stripped
|
|
||||||
stripped=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g')
|
|
||||||
local len=${#stripped}
|
|
||||||
local pad=$(( (w - len) / 2 ))
|
|
||||||
[[ "$pad" -lt 0 ]] && pad=0
|
|
||||||
printf "%*s%b\n" "$pad" "" "$text"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Logging
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
mkdir -p "$STRIX_DIR" 2>/dev/null || true
|
|
||||||
|
|
||||||
log_raw() {
|
|
||||||
local ts
|
|
||||||
ts=$(date '+%H:%M:%S')
|
|
||||||
echo -e "$ts $1" >> "$LOG_FILE" 2>/dev/null || true
|
|
||||||
}
|
|
||||||
|
|
||||||
log() {
|
|
||||||
log_raw "$1"
|
|
||||||
if [[ "$VERBOSE" == true ]]; then
|
|
||||||
local ts
|
|
||||||
ts=$(date '+%H:%M:%S')
|
|
||||||
echo -e " ${C_DIM}${ts} $1${C_RESET}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Status line helpers
|
|
||||||
status_ok() { echo -e " ${C_GREEN}${C_BOLD}[OK]${C_RESET} $1"; log "[OK] $1"; }
|
|
||||||
status_warn() { echo -e " ${C_YELLOW}${C_BOLD}[!!]${C_RESET} $1"; log "[!!] $1"; }
|
|
||||||
status_fail() { echo -e " ${C_RED}${C_BOLD}[XX]${C_RESET} $1"; log "[XX] $1"; }
|
|
||||||
status_info() { echo -e " ${C_CYAN}${C_BOLD}[..]${C_RESET} $1"; log "[..] $1"; }
|
|
||||||
status_skip() { echo -e " ${C_DIM}[--]${C_RESET} $1"; log "[--] $1"; }
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# ASCII art
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
show_logo() {
|
|
||||||
[[ "$SHOW_LOGO" == false ]] && return
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Block STRIX title
|
|
||||||
center "${C_MAGENTA}${C_BOLD}███████╗████████╗██████╗ ██╗██╗ ██╗${C_RESET}"
|
|
||||||
center "${C_MAGENTA}${C_BOLD}██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝${C_RESET}"
|
|
||||||
center "${C_MAGENTA}${C_BOLD}███████╗ ██║ ██████╔╝██║ ╚███╔╝${C_RESET}"
|
|
||||||
center "${C_MAGENTA}${C_BOLD}╚════██║ ██║ ██╔══██╗██║ ██╔██╗${C_RESET}"
|
|
||||||
center "${C_MAGENTA}${C_BOLD}███████║ ██║ ██║ ██║██║██╔╝ ██╗${C_RESET}"
|
|
||||||
center "${C_MAGENTA}${C_BOLD}╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝${C_RESET}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Owl
|
|
||||||
center "${C_CYAN} __________-------____ ____-------__________${C_RESET}"
|
|
||||||
center "${C_CYAN} \\------____-------___--__---------__--___-------____------/${C_RESET}"
|
|
||||||
center "${C_CYAN} \\//////// / / / / / \\ _-------_ / \\ \\ \\ \\ \\ \\\\\\\\\\\\\\\\/${C_RESET}"
|
|
||||||
center "${C_CYAN} \\////-/-/------/_/_| /___ ___\\ |_\\_\\------\\-\\-\\\\\\\\/${C_RESET}"
|
|
||||||
center "${C_CYAN} --//// / / / //|| ${C_YELLOW}(O)${C_CYAN}\\ /${C_YELLOW}(O)${C_CYAN} ||\\\\ \\ \\ \\ \\\\\\\\--${C_RESET}"
|
|
||||||
center "${C_CYAN} ---__/ // /| \\_ /V\\ _/ |\\ \\\\ \\__---${C_RESET}"
|
|
||||||
center "${C_CYAN} -// / /\\_ ------- _/\\ \\ \\\\-${C_RESET}"
|
|
||||||
center "${C_CYAN} \\_/_/ /\\---------/\\ \\_\\_/${C_RESET}"
|
|
||||||
center "${C_CYAN} ----\\ | /----${C_RESET}"
|
|
||||||
center "${C_CYAN} | -|- |${C_RESET}"
|
|
||||||
center "${C_CYAN} / | \\${C_RESET}"
|
|
||||||
center "${C_CYAN} ---- ----${C_RESET}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
center "${C_WHITE}${C_BOLD}Smart IP Camera Stream Finder${C_RESET}"
|
|
||||||
center "${C_DIM}────────────────────────────────────────${C_RESET}"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
show_owl_small() {
|
|
||||||
echo -e " ${C_CYAN} ,___,${C_RESET}"
|
|
||||||
echo -e " ${C_CYAN} /(6 6)\\_${C_RESET}"
|
|
||||||
echo -e " ${C_CYAN} /\\\` ' \`'\\\\_${C_RESET}"
|
|
||||||
echo -e " ${C_CYAN} \\\\\\\\_''''|\\\\\\\\${C_RESET}"
|
|
||||||
echo -e " ${C_CYAN} )\\\\\\\\\\\\''//||/${C_RESET}"
|
|
||||||
echo -e " ${C_CYAN} ._,--/////\"\"------${C_RESET}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Dialog boxes
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Draw a box around content.
|
|
||||||
# Usage: draw_box "Title" "line1" "line2" ...
|
|
||||||
draw_box() {
|
|
||||||
local title="$1"; shift
|
|
||||||
local w
|
|
||||||
w=$(term_width)
|
|
||||||
local box_w=$(( w - 4 ))
|
|
||||||
[[ "$box_w" -gt 60 ]] && box_w=60
|
|
||||||
|
|
||||||
local top_line=""
|
|
||||||
local bot_line=""
|
|
||||||
local i
|
|
||||||
|
|
||||||
# Build horizontal lines
|
|
||||||
for (( i = 0; i < box_w - 2; i++ )); do
|
|
||||||
top_line="${top_line}─"
|
|
||||||
bot_line="${bot_line}─"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Center the box
|
|
||||||
local pad=$(( (w - box_w) / 2 ))
|
|
||||||
[[ "$pad" -lt 0 ]] && pad=0
|
|
||||||
local sp
|
|
||||||
sp=$(printf "%*s" "$pad" "")
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${sp}${C_CYAN}┌─ ${C_WHITE}${C_BOLD}${title}${C_RESET}${C_CYAN} ${top_line:$(( ${#title} + 3 ))}┐${C_RESET}"
|
|
||||||
echo -e "${sp}${C_CYAN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}"
|
|
||||||
|
|
||||||
for line in "$@"; do
|
|
||||||
local stripped
|
|
||||||
stripped=$(echo -e "$line" | sed 's/\x1b\[[0-9;]*m//g')
|
|
||||||
local line_len=${#stripped}
|
|
||||||
local right_pad=$(( box_w - 2 - line_len ))
|
|
||||||
[[ "$right_pad" -lt 0 ]] && right_pad=0
|
|
||||||
echo -e "${sp}${C_CYAN}│${C_RESET} ${line}$(printf "%*s" "$right_pad" "")${C_CYAN}│${C_RESET}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo -e "${sp}${C_CYAN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_CYAN}└${bot_line}┘${C_RESET}"
|
|
||||||
echo ""
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prompt with timeout. Returns user input or default.
|
|
||||||
# Usage: timed_prompt "prompt text" "default" timeout_seconds
|
|
||||||
timed_prompt() {
|
|
||||||
local prompt_text="$1"
|
|
||||||
local default="$2"
|
|
||||||
local timeout="$3"
|
|
||||||
local result=""
|
|
||||||
|
|
||||||
if [[ "$INTERACTIVE" == false ]]; then
|
|
||||||
echo "$default"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Show countdown hint
|
|
||||||
echo -ne " ${C_YELLOW}${prompt_text}${C_RESET} ${C_DIM}(${timeout}s -> ${default})${C_RESET}: "
|
|
||||||
|
|
||||||
if read -r -t "$timeout" result 2>/dev/null; then
|
|
||||||
if [[ -z "$result" ]]; then
|
|
||||||
echo "$default"
|
|
||||||
else
|
|
||||||
echo "$result"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "" # newline after timeout
|
|
||||||
echo "$default"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# System detection
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
detect_system() {
|
|
||||||
log "Detecting OS from /etc/os-release"
|
|
||||||
|
|
||||||
if [[ -f /etc/os-release ]]; then
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
. /etc/os-release
|
|
||||||
OS_ID="${ID:-unknown}"
|
|
||||||
OS_VERSION="${VERSION_ID:-unknown}"
|
|
||||||
OS_NAME="${PRETTY_NAME:-${ID} ${VERSION_ID}}"
|
|
||||||
else
|
|
||||||
OS_ID="unknown"
|
|
||||||
OS_VERSION="unknown"
|
|
||||||
OS_NAME="Unknown Linux"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ARCH=$(uname -m)
|
|
||||||
case "$ARCH" in
|
|
||||||
x86_64) ARCH_LABEL="amd64" ;;
|
|
||||||
aarch64) ARCH_LABEL="arm64" ;;
|
|
||||||
armv7l) ARCH_LABEL="armv7" ;;
|
|
||||||
*) ARCH_LABEL="$ARCH" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
log "Detected: ${OS_ID} ${OS_VERSION} (${ARCH_LABEL})"
|
|
||||||
status_ok "System: ${C_WHITE}${C_BOLD}${OS_NAME}${C_RESET} (${ARCH_LABEL})"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Ensure root
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
ensure_root() {
|
|
||||||
if [[ "$(id -u)" -ne 0 ]]; then
|
|
||||||
status_info "Root privileges required, re-running with sudo..."
|
|
||||||
exec sudo "$0" "$@"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Install curl if missing
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
ensure_curl() {
|
|
||||||
if command -v curl &>/dev/null; then
|
|
||||||
log "curl found"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
status_info "Installing curl..."
|
|
||||||
log "curl not found, installing"
|
|
||||||
|
|
||||||
case "$OS_ID" in
|
|
||||||
ubuntu|debian|raspbian|linuxmint|pop)
|
|
||||||
apt-get update -qq && apt-get install -y -qq curl ;;
|
|
||||||
centos|rhel|rocky|almalinux|ol)
|
|
||||||
yum install -y -q curl ;;
|
|
||||||
fedora)
|
|
||||||
dnf install -y -q curl ;;
|
|
||||||
alpine)
|
|
||||||
apk add --no-cache curl ;;
|
|
||||||
arch|manjaro)
|
|
||||||
pacman -Sy --noconfirm curl ;;
|
|
||||||
opensuse*|sles)
|
|
||||||
zypper install -y curl ;;
|
|
||||||
*)
|
|
||||||
status_fail "Cannot install curl: unknown package manager for ${OS_ID}"
|
|
||||||
status_fail "Please install curl manually and re-run the script"
|
|
||||||
exit 1 ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if command -v curl &>/dev/null; then
|
|
||||||
status_ok "curl installed"
|
|
||||||
else
|
|
||||||
status_fail "Failed to install curl"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Docker installation
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
check_docker() {
|
|
||||||
if command -v docker &>/dev/null; then
|
|
||||||
local ver
|
|
||||||
ver=$(docker --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "unknown")
|
|
||||||
status_ok "Docker ${C_WHITE}${C_BOLD}${ver}${C_RESET}"
|
|
||||||
log "Docker found: ${ver}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
install_docker() {
|
|
||||||
status_info "Installing Docker via ${C_WHITE}get.docker.com${C_RESET}..."
|
|
||||||
log "Downloading and running get.docker.com"
|
|
||||||
|
|
||||||
if [[ "$VERBOSE" == true ]]; then
|
|
||||||
curl -fsSL https://get.docker.com | sh
|
|
||||||
else
|
|
||||||
curl -fsSL https://get.docker.com | sh &>/dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Enable and start docker
|
|
||||||
if command -v systemctl &>/dev/null; then
|
|
||||||
systemctl enable docker &>/dev/null || true
|
|
||||||
systemctl start docker &>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if check_docker; then
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
status_fail "Docker installation failed"
|
|
||||||
log "Docker installation failed"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Docker Compose check
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
check_compose() {
|
|
||||||
# Check plugin first (docker compose v2)
|
|
||||||
if docker compose version &>/dev/null; then
|
|
||||||
local ver
|
|
||||||
ver=$(docker compose version --short 2>/dev/null || echo "unknown")
|
|
||||||
status_ok "Docker Compose ${C_WHITE}${C_BOLD}${ver}${C_RESET}"
|
|
||||||
log "Docker Compose found: ${ver}"
|
|
||||||
COMPOSE_CMD="docker compose"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check standalone docker-compose
|
|
||||||
if command -v docker-compose &>/dev/null; then
|
|
||||||
local ver
|
|
||||||
ver=$(docker-compose --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "unknown")
|
|
||||||
status_ok "Docker Compose ${C_WHITE}${C_BOLD}${ver}${C_RESET} (standalone)"
|
|
||||||
log "Docker Compose standalone found: ${ver}"
|
|
||||||
COMPOSE_CMD="docker-compose"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
install_compose() {
|
|
||||||
status_info "Installing Docker Compose plugin..."
|
|
||||||
log "Installing Docker Compose plugin"
|
|
||||||
|
|
||||||
# Docker Compose V2 is typically bundled with Docker now.
|
|
||||||
# Try installing the plugin package.
|
|
||||||
case "$OS_ID" in
|
|
||||||
ubuntu|debian|raspbian|linuxmint|pop)
|
|
||||||
apt-get update -qq && apt-get install -y -qq docker-compose-plugin 2>/dev/null ;;
|
|
||||||
centos|rhel|rocky|almalinux|ol|fedora)
|
|
||||||
yum install -y -q docker-compose-plugin 2>/dev/null || dnf install -y -q docker-compose-plugin 2>/dev/null ;;
|
|
||||||
*)
|
|
||||||
# Fallback: download binary
|
|
||||||
local compose_ver="v2.29.1"
|
|
||||||
local compose_arch
|
|
||||||
case "$ARCH_LABEL" in
|
|
||||||
amd64) compose_arch="x86_64" ;;
|
|
||||||
arm64) compose_arch="aarch64" ;;
|
|
||||||
*) compose_arch="$ARCH" ;;
|
|
||||||
esac
|
|
||||||
mkdir -p /usr/local/lib/docker/cli-plugins
|
|
||||||
curl -fsSL "https://github.com/docker/compose/releases/download/${compose_ver}/docker-compose-linux-${compose_arch}" \
|
|
||||||
-o /usr/local/lib/docker/cli-plugins/docker-compose
|
|
||||||
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if check_compose; then
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
status_fail "Docker Compose installation failed"
|
|
||||||
log "Docker Compose installation failed"
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Check if Strix is already installed
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
check_existing() {
|
|
||||||
if [[ -f "${STRIX_DIR}/docker-compose.yml" ]]; then
|
|
||||||
if $COMPOSE_CMD -f "${STRIX_DIR}/docker-compose.yml" ps --format '{{.State}}' 2>/dev/null | grep -qi "running"; then
|
|
||||||
INSTALL_MODE="update"
|
|
||||||
status_info "Strix is already running -- ${C_WHITE}${C_BOLD}update mode${C_RESET}"
|
|
||||||
log "Existing Strix installation found, switching to update mode"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
# compose file exists but not running
|
|
||||||
INSTALL_MODE="update"
|
|
||||||
status_info "Strix config found but not running -- ${C_WHITE}${C_BOLD}update mode${C_RESET}"
|
|
||||||
log "Existing Strix config found (not running), switching to update mode"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
INSTALL_MODE="install"
|
|
||||||
log "No existing Strix installation found"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Version selection dialog
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
select_version() {
|
|
||||||
# If already set via --version flag
|
|
||||||
if [[ -n "$TAG" ]]; then
|
|
||||||
FINAL_VERSION="$TAG"
|
|
||||||
status_ok "Version: ${C_WHITE}${C_BOLD}${TAG}${C_RESET} (from --version flag)"
|
|
||||||
log "Version set via flag: ${TAG}"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$INTERACTIVE" == false ]]; then
|
|
||||||
FINAL_VERSION="latest"
|
|
||||||
status_ok "Version: ${C_WHITE}${C_BOLD}latest${C_RESET} (non-interactive default)"
|
|
||||||
log "Version defaulted to latest (non-interactive)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
draw_box "Select Version" \
|
|
||||||
" ${C_GREEN}${C_BOLD}[1]${C_RESET} latest ${C_DIM}(recommended)${C_RESET}" \
|
|
||||||
" ${C_YELLOW}${C_BOLD}[2]${C_RESET} dev ${C_DIM}(development)${C_RESET}" \
|
|
||||||
" ${C_CYAN}${C_BOLD}[3]${C_RESET} custom tag ${C_DIM}(e.g. 1.0.9)${C_RESET}"
|
|
||||||
|
|
||||||
local choice
|
|
||||||
choice=$(timed_prompt "Choice [1/2/3]" "1" "$DIALOG_TIMEOUT")
|
|
||||||
|
|
||||||
case "$choice" in
|
|
||||||
1|"") FINAL_VERSION="latest" ;;
|
|
||||||
2) FINAL_VERSION="dev" ;;
|
|
||||||
3)
|
|
||||||
echo -ne " ${C_YELLOW}Enter tag: ${C_RESET}"
|
|
||||||
local custom_tag
|
|
||||||
if read -r -t "$DIALOG_TIMEOUT" custom_tag 2>/dev/null && [[ -n "$custom_tag" ]]; then
|
|
||||||
FINAL_VERSION="$custom_tag"
|
|
||||||
else
|
|
||||||
FINAL_VERSION="latest"
|
|
||||||
echo ""
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
*) FINAL_VERSION="latest" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
status_ok "Version: ${C_WHITE}${C_BOLD}${FINAL_VERSION}${C_RESET}"
|
|
||||||
log "Version selected: ${FINAL_VERSION}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Service detection (Frigate / go2rtc)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
probe_frigate() {
|
|
||||||
log "Probing Frigate at localhost:5000"
|
|
||||||
|
|
||||||
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:5000/api/config" &>/dev/null; then
|
|
||||||
FRIGATE_STATUS="found"
|
|
||||||
FRIGATE_URL="http://localhost:5000"
|
|
||||||
status_ok "Frigate: ${C_WHITE}${C_BOLD}localhost:5000${C_RESET}"
|
|
||||||
log "Frigate found at localhost:5000"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Frigate not found locally"
|
|
||||||
|
|
||||||
if [[ "$INTERACTIVE" == false ]]; then
|
|
||||||
FRIGATE_STATUS="none"
|
|
||||||
FRIGATE_URL=""
|
|
||||||
status_skip "Frigate: not found (skipped)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
show_owl_small
|
|
||||||
draw_box "Frigate Not Found" \
|
|
||||||
" Frigate was not detected on this machine." \
|
|
||||||
" Enter Frigate URL or leave empty to skip." \
|
|
||||||
"" \
|
|
||||||
" ${C_DIM}Example: http://192.168.1.100:5000${C_RESET}"
|
|
||||||
|
|
||||||
local input
|
|
||||||
input=$(timed_prompt "Frigate URL" "" "$DIALOG_TIMEOUT")
|
|
||||||
|
|
||||||
if [[ -n "$input" ]]; then
|
|
||||||
FRIGATE_STATUS="set"
|
|
||||||
FRIGATE_URL="$input"
|
|
||||||
status_ok "Frigate: ${C_WHITE}${C_BOLD}${input}${C_RESET} (manual)"
|
|
||||||
log "Frigate URL set manually: ${input}"
|
|
||||||
else
|
|
||||||
FRIGATE_STATUS="none"
|
|
||||||
FRIGATE_URL=""
|
|
||||||
status_skip "Frigate: not configured"
|
|
||||||
log "Frigate skipped"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
probe_go2rtc() {
|
|
||||||
log "Probing go2rtc at localhost:1984 and localhost:11984"
|
|
||||||
|
|
||||||
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:1984/api" &>/dev/null; then
|
|
||||||
GO2RTC_STATUS="found"
|
|
||||||
GO2RTC_URL="http://localhost:1984"
|
|
||||||
status_ok "go2rtc: ${C_WHITE}${C_BOLD}localhost:1984${C_RESET}"
|
|
||||||
log "go2rtc found at localhost:1984"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:11984/api" &>/dev/null; then
|
|
||||||
GO2RTC_STATUS="found"
|
|
||||||
GO2RTC_URL="http://localhost:11984"
|
|
||||||
status_ok "go2rtc: ${C_WHITE}${C_BOLD}localhost:11984${C_RESET}"
|
|
||||||
log "go2rtc found at localhost:11984"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "go2rtc not found locally"
|
|
||||||
|
|
||||||
if [[ "$INTERACTIVE" == false ]]; then
|
|
||||||
GO2RTC_STATUS="none"
|
|
||||||
GO2RTC_URL=""
|
|
||||||
status_skip "go2rtc: not found (skipped)"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
show_owl_small
|
|
||||||
draw_box "go2rtc Not Found" \
|
|
||||||
" go2rtc was not detected on this machine." \
|
|
||||||
" Enter go2rtc URL or leave empty to skip." \
|
|
||||||
"" \
|
|
||||||
" ${C_DIM}Example: http://192.168.1.100:1984${C_RESET}"
|
|
||||||
|
|
||||||
local input
|
|
||||||
input=$(timed_prompt "go2rtc URL" "" "$DIALOG_TIMEOUT")
|
|
||||||
|
|
||||||
if [[ -n "$input" ]]; then
|
|
||||||
GO2RTC_STATUS="set"
|
|
||||||
GO2RTC_URL="$input"
|
|
||||||
status_ok "go2rtc: ${C_WHITE}${C_BOLD}${input}${C_RESET} (manual)"
|
|
||||||
log "go2rtc URL set manually: ${input}"
|
|
||||||
else
|
|
||||||
GO2RTC_STATUS="none"
|
|
||||||
GO2RTC_URL=""
|
|
||||||
status_skip "go2rtc: not configured"
|
|
||||||
log "go2rtc skipped"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Generate config files
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
generate_env() {
|
|
||||||
log "Generating ${STRIX_DIR}/.env"
|
|
||||||
|
|
||||||
cat > "${STRIX_DIR}/.env" <<EOF
|
|
||||||
# Strix configuration -- generated by install.sh
|
|
||||||
STRIX_TAG=${FINAL_VERSION}
|
|
||||||
STRIX_PORT=${STRIX_PORT}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
if [[ -n "${FRIGATE_URL:-}" ]]; then
|
|
||||||
echo "STRIX_FRIGATE_URL=${FRIGATE_URL}" >> "${STRIX_DIR}/.env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "${GO2RTC_URL:-}" ]]; then
|
|
||||||
echo "STRIX_GO2RTC_URL=${GO2RTC_URL}" >> "${STRIX_DIR}/.env"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Generated .env: TAG=${FINAL_VERSION}, FRIGATE=${FRIGATE_URL:-}, GO2RTC=${GO2RTC_URL:-}"
|
|
||||||
}
|
|
||||||
|
|
||||||
generate_compose() {
|
|
||||||
log "Generating ${STRIX_DIR}/docker-compose.yml"
|
|
||||||
|
|
||||||
local env_section=""
|
|
||||||
if [[ -n "${FRIGATE_URL:-}" ]] || [[ -n "${GO2RTC_URL:-}" ]]; then
|
|
||||||
env_section=" environment:"
|
|
||||||
[[ -n "${FRIGATE_URL:-}" ]] && env_section="${env_section}
|
|
||||||
- STRIX_FRIGATE_URL=\${STRIX_FRIGATE_URL}"
|
|
||||||
[[ -n "${GO2RTC_URL:-}" ]] && env_section="${env_section}
|
|
||||||
- STRIX_GO2RTC_URL=\${STRIX_GO2RTC_URL}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat > "${STRIX_DIR}/docker-compose.yml" <<EOF
|
|
||||||
# Strix -- Smart IP Camera Stream Finder
|
|
||||||
# Generated by install.sh -- do not edit manually, re-run installer to update.
|
|
||||||
|
|
||||||
services:
|
|
||||||
strix:
|
|
||||||
image: ${IMAGE}:\${STRIX_TAG:-latest}
|
|
||||||
container_name: strix
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
${env_section}
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:\${STRIX_PORT:-4567}/api/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 3
|
|
||||||
EOF
|
|
||||||
|
|
||||||
log "Generated docker-compose.yml"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Deploy
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
pull_image() {
|
|
||||||
status_info "Pulling ${C_WHITE}${C_BOLD}${IMAGE}:${FINAL_VERSION}${C_RESET}..."
|
|
||||||
log "Pulling image ${IMAGE}:${FINAL_VERSION}"
|
|
||||||
|
|
||||||
if [[ "$VERBOSE" == true ]]; then
|
|
||||||
$COMPOSE_CMD -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" pull
|
|
||||||
else
|
|
||||||
$COMPOSE_CMD -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" pull 2>&1 | tail -1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $? -eq 0 ]]; then
|
|
||||||
status_ok "Image pulled: ${C_WHITE}${C_BOLD}${IMAGE}:${FINAL_VERSION}${C_RESET}"
|
|
||||||
log "Image pulled successfully"
|
|
||||||
else
|
|
||||||
status_fail "Failed to pull image ${IMAGE}:${FINAL_VERSION}"
|
|
||||||
log "Image pull failed"
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
start_container() {
|
|
||||||
log "Starting container"
|
|
||||||
|
|
||||||
if [[ "$INSTALL_MODE" == "update" ]]; then
|
|
||||||
status_info "Recreating container..."
|
|
||||||
$COMPOSE_CMD -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" up -d --force-recreate 2>&1 | \
|
|
||||||
if [[ "$VERBOSE" == true ]]; then cat; else tail -1; fi
|
|
||||||
else
|
|
||||||
status_info "Starting container..."
|
|
||||||
$COMPOSE_CMD -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" up -d 2>&1 | \
|
|
||||||
if [[ "$VERBOSE" == true ]]; then cat; else tail -1; fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $? -ne 0 ]]; then
|
|
||||||
status_fail "Container failed to start"
|
|
||||||
log "Container failed to start"
|
|
||||||
exit 4
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Container started"
|
|
||||||
}
|
|
||||||
|
|
||||||
healthcheck() {
|
|
||||||
status_info "Running healthcheck..."
|
|
||||||
log "Waiting for healthcheck on localhost:${STRIX_PORT}"
|
|
||||||
|
|
||||||
local retries=10
|
|
||||||
local i
|
|
||||||
for (( i = 1; i <= retries; i++ )); do
|
|
||||||
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:${STRIX_PORT}/api/health" &>/dev/null; then
|
|
||||||
local version
|
|
||||||
version=$(curl -sf --max-time 3 "http://localhost:${STRIX_PORT}/api" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")
|
|
||||||
status_ok "Strix is running ${C_WHITE}${C_BOLD}v${version}${C_RESET} on port ${C_WHITE}${C_BOLD}${STRIX_PORT}${C_RESET}"
|
|
||||||
log "Healthcheck passed, version: ${version}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
|
|
||||||
status_fail "Healthcheck failed after ${retries} attempts"
|
|
||||||
log "Healthcheck failed"
|
|
||||||
exit 5
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Detect LAN IP address
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
detect_lan_ip() {
|
|
||||||
local ip=""
|
|
||||||
|
|
||||||
# Method 1: ip route (most reliable on modern Linux)
|
|
||||||
ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1)
|
|
||||||
|
|
||||||
# Method 2: hostname -I
|
|
||||||
if [[ -z "$ip" ]]; then
|
|
||||||
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Method 3: ifconfig fallback
|
|
||||||
if [[ -z "$ip" ]]; then
|
|
||||||
ip=$(ifconfig 2>/dev/null | grep -oP 'inet \K[0-9.]+' | grep -v '127.0.0.1' | head -1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fallback to localhost
|
|
||||||
if [[ -z "$ip" ]]; then
|
|
||||||
ip="localhost"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "$ip"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Summary
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
show_summary() {
|
|
||||||
echo ""
|
|
||||||
local w
|
|
||||||
w=$(term_width)
|
|
||||||
local line=""
|
|
||||||
local box_w=$(( w - 4 ))
|
|
||||||
[[ "$box_w" -gt 60 ]] && box_w=60
|
|
||||||
for (( i = 0; i < box_w - 2; i++ )); do line="${line}─"; done
|
|
||||||
local pad=$(( (w - box_w) / 2 ))
|
|
||||||
[[ "$pad" -lt 0 ]] && pad=0
|
|
||||||
local sp
|
|
||||||
sp=$(printf "%*s" "$pad" "")
|
|
||||||
|
|
||||||
local lan_ip
|
|
||||||
lan_ip=$(detect_lan_ip)
|
|
||||||
local open_url="http://${lan_ip}:${STRIX_PORT}"
|
|
||||||
|
|
||||||
echo -e "${sp}${C_GREEN}┌─ ${C_WHITE}${C_BOLD}Complete${C_RESET} ${C_GREEN}${line:11}┐${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│${C_RESET} Mode: ${C_WHITE}${C_BOLD}${INSTALL_MODE}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#INSTALL_MODE} )) "")${C_GREEN}│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│${C_RESET} Version: ${C_WHITE}${C_BOLD}${FINAL_VERSION}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#FINAL_VERSION} )) "")${C_GREEN}│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│${C_RESET} Port: ${C_WHITE}${C_BOLD}${STRIX_PORT}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#STRIX_PORT} )) "")${C_GREEN}│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│${C_RESET} Frigate: ${C_WHITE}${FRIGATE_STATUS}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#FRIGATE_STATUS} )) "")${C_GREEN}│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│${C_RESET} go2rtc: ${C_WHITE}${GO2RTC_STATUS}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#GO2RTC_STATUS} )) "")${C_GREEN}│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│${C_RESET} Config: ${C_DIM}${STRIX_DIR}/${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#STRIX_DIR} - 1 )) "")${C_GREEN}│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│${C_RESET} Log: ${C_DIM}${LOG_FILE}${C_RESET}$(printf "%*s" $(( box_w - 16 - ${#LOG_FILE} )) "")${C_GREEN}│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│${C_RESET} ${C_CYAN}Open: ${C_WHITE}${C_BOLD}${open_url}${C_RESET}$(printf "%*s" $(( box_w - 9 - ${#open_url} )) "")${C_GREEN}│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}│$(printf "%*s" $(( box_w - 2 )) "")│${C_RESET}"
|
|
||||||
echo -e "${sp}${C_GREEN}└${line}┘${C_RESET}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Machine-readable output for parent scripts (always last line)
|
|
||||||
echo "STRIX_RESULT=OK MODE=${INSTALL_MODE} VERSION=${FINAL_VERSION} FRIGATE=${FRIGATE_STATUS} GO2RTC=${GO2RTC_STATUS}"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Verbose log tail (shown on error)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
show_error_log() {
|
|
||||||
if [[ -f "$LOG_FILE" ]]; then
|
|
||||||
echo ""
|
|
||||||
echo -e " ${C_RED}${C_BOLD}── Last log entries ──${C_RESET}"
|
|
||||||
tail -20 "$LOG_FILE" | while IFS= read -r line; do
|
|
||||||
echo -e " ${C_RED}${line}${C_RESET}"
|
|
||||||
done
|
|
||||||
echo -e " ${C_RED}${C_BOLD}──────────────────────${C_RESET}"
|
|
||||||
echo ""
|
|
||||||
echo -e " Full log: ${C_WHITE}${LOG_FILE}${C_RESET}"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Main
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
main() {
|
|
||||||
# Trap errors to show log
|
|
||||||
trap 'show_error_log' ERR
|
|
||||||
|
|
||||||
# Initialize
|
|
||||||
setup_colors
|
|
||||||
ensure_root "$@"
|
|
||||||
|
|
||||||
# Show logo
|
|
||||||
show_logo
|
|
||||||
|
|
||||||
# Detect system
|
|
||||||
detect_system
|
|
||||||
|
|
||||||
# Ensure curl is available
|
|
||||||
ensure_curl
|
|
||||||
|
|
||||||
# Docker
|
|
||||||
if ! check_docker; then
|
|
||||||
install_docker
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Docker Compose
|
|
||||||
if ! check_compose; then
|
|
||||||
install_compose
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Version selection
|
|
||||||
select_version
|
|
||||||
|
|
||||||
# Check existing installation
|
|
||||||
check_existing || true
|
|
||||||
|
|
||||||
# Detect services (only for fresh install)
|
|
||||||
if [[ "$INSTALL_MODE" == "install" ]]; then
|
|
||||||
probe_frigate
|
|
||||||
probe_go2rtc
|
|
||||||
else
|
|
||||||
# For updates, load existing env
|
|
||||||
if [[ -f "${STRIX_DIR}/.env" ]]; then
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
source "${STRIX_DIR}/.env" 2>/dev/null || true
|
|
||||||
FRIGATE_URL="${STRIX_FRIGATE_URL:-}"
|
|
||||||
GO2RTC_URL="${STRIX_GO2RTC_URL:-}"
|
|
||||||
[[ -n "$FRIGATE_URL" ]] && FRIGATE_STATUS="set"
|
|
||||||
[[ -n "$GO2RTC_URL" ]] && GO2RTC_STATUS="set"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e " ${C_DIM}────────────────────────────────────────${C_RESET}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Generate configs
|
|
||||||
generate_env
|
|
||||||
generate_compose
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
pull_image
|
|
||||||
start_container
|
|
||||||
healthcheck
|
|
||||||
|
|
||||||
# Done
|
|
||||||
show_summary
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
Executable
+262
@@ -0,0 +1,262 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Strix -- install.sh (navigator / frontend)
|
||||||
|
#
|
||||||
|
# Main entry point for Strix installation.
|
||||||
|
# Shows animated owl + STRIX logo while running background checks.
|
||||||
|
# Detects system type (Proxmox / Linux / macOS), Docker, Frigate, go2rtc.
|
||||||
|
# Then guides the user through installation by calling worker scripts.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# curl -fsSL https://raw.githubusercontent.com/eduard256/Strix/main/scripts/install.sh | bash
|
||||||
|
# bash scripts/install.sh
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Owl definitions (5 lines each: line1, line2, line3, line4, name)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
OWL_COUNT=5
|
||||||
|
|
||||||
|
# Wide-eyed owl
|
||||||
|
OWL_0_1=" ___"
|
||||||
|
OWL_0_2=" <O,O>"
|
||||||
|
OWL_0_3=" [\`-']"
|
||||||
|
OWL_0_4=" -\"-\"-"
|
||||||
|
OWL_0_NAME="wide-eyed owl"
|
||||||
|
|
||||||
|
# Happy owl
|
||||||
|
OWL_1_1=" ___"
|
||||||
|
OWL_1_2=" <^,^>"
|
||||||
|
OWL_1_3=" [\`-']"
|
||||||
|
OWL_1_4=" -\"-\"-"
|
||||||
|
OWL_1_NAME="happy owl"
|
||||||
|
|
||||||
|
# Winking owl
|
||||||
|
OWL_2_1=" ___"
|
||||||
|
OWL_2_2=" <*,->"
|
||||||
|
OWL_2_3=" [\`-']"
|
||||||
|
OWL_2_4=" -\"-\"-"
|
||||||
|
OWL_2_NAME="winking owl"
|
||||||
|
|
||||||
|
# Flying owl
|
||||||
|
OWL_3_1=" ___"
|
||||||
|
OWL_3_2=" <*,*>"
|
||||||
|
OWL_3_3=" =^\`-'^="
|
||||||
|
OWL_3_4=" \" \""
|
||||||
|
OWL_3_NAME="flying owl"
|
||||||
|
|
||||||
|
# Super owl
|
||||||
|
OWL_4_1=" ___"
|
||||||
|
OWL_4_2=" <*,*>"
|
||||||
|
OWL_4_3=" [\`S']"
|
||||||
|
OWL_4_4=" -\"-\"-"
|
||||||
|
OWL_4_NAME="super owl"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Terminal helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
term_width() {
|
||||||
|
local w
|
||||||
|
w=$(tput cols 2>/dev/null || echo 80)
|
||||||
|
[[ "$w" -lt 20 ]] && w=80
|
||||||
|
echo "$w"
|
||||||
|
}
|
||||||
|
|
||||||
|
term_height() {
|
||||||
|
local h
|
||||||
|
h=$(tput lines 2>/dev/null || echo 24)
|
||||||
|
[[ "$h" -lt 10 ]] && h=24
|
||||||
|
echo "$h"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print text centered horizontally at a specific row
|
||||||
|
# Usage: print_at_center ROW "text" [color_code]
|
||||||
|
print_at_center() {
|
||||||
|
local row="$1"
|
||||||
|
local text="$2"
|
||||||
|
local color="${3:-}"
|
||||||
|
local reset="\033[0m"
|
||||||
|
|
||||||
|
local w
|
||||||
|
w=$(term_width)
|
||||||
|
|
||||||
|
# Strip ANSI for length calc
|
||||||
|
local stripped
|
||||||
|
stripped=$(echo -e "$text" | sed 's/\x1b\[[0-9;]*m//g')
|
||||||
|
local len=${#stripped}
|
||||||
|
local col=$(( (w - len) / 2 ))
|
||||||
|
[[ "$col" -lt 0 ]] && col=0
|
||||||
|
|
||||||
|
tput cup "$row" "$col" 2>/dev/null
|
||||||
|
if [[ -n "$color" ]]; then
|
||||||
|
echo -ne "${color}${text}${reset}"
|
||||||
|
else
|
||||||
|
echo -ne "${text}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Show a single owl centered on screen
|
||||||
|
# Usage: show_owl INDEX [brightness]
|
||||||
|
# brightness: "bright" "dim" "verydim" "hidden"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
show_owl() {
|
||||||
|
local idx="$1"
|
||||||
|
local brightness="${2:-bright}"
|
||||||
|
|
||||||
|
local color=""
|
||||||
|
case "$brightness" in
|
||||||
|
bright) color="\033[97m" ;; # bright white
|
||||||
|
dim) color="\033[37m" ;; # normal white
|
||||||
|
verydim) color="\033[90m" ;; # dark gray
|
||||||
|
hidden) color="\033[30m" ;; # black (invisible)
|
||||||
|
esac
|
||||||
|
|
||||||
|
local name_color=""
|
||||||
|
case "$brightness" in
|
||||||
|
bright) name_color="\033[36m" ;; # cyan
|
||||||
|
dim) name_color="\033[2;36m" ;; # dim cyan
|
||||||
|
verydim) name_color="\033[90m" ;; # dark gray
|
||||||
|
hidden) name_color="\033[30m" ;; # black
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Get owl lines by index
|
||||||
|
local l1 l2 l3 l4 name
|
||||||
|
eval "l1=\"\$OWL_${idx}_1\""
|
||||||
|
eval "l2=\"\$OWL_${idx}_2\""
|
||||||
|
eval "l3=\"\$OWL_${idx}_3\""
|
||||||
|
eval "l4=\"\$OWL_${idx}_4\""
|
||||||
|
eval "name=\"\$OWL_${idx}_NAME\""
|
||||||
|
|
||||||
|
# Position: top area of screen
|
||||||
|
local start_row=2
|
||||||
|
|
||||||
|
print_at_center "$start_row" "$l1" "$color"
|
||||||
|
print_at_center "$((start_row+1))" "$l2" "$color"
|
||||||
|
print_at_center "$((start_row+2))" "$l3" "$color"
|
||||||
|
print_at_center "$((start_row+3))" "$l4" "$color"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Clear owl area (rows 2-6)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
clear_owl_area() {
|
||||||
|
local w
|
||||||
|
w=$(term_width)
|
||||||
|
local blank
|
||||||
|
blank=$(printf "%*s" "$w" "")
|
||||||
|
|
||||||
|
for row in 2 3 4 5 6; do
|
||||||
|
tput cup "$row" 0 2>/dev/null
|
||||||
|
echo -ne "$blank"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Transition: fade out current owl, fade in next
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
transition_owl() {
|
||||||
|
local from_idx="$1"
|
||||||
|
local to_idx="$2"
|
||||||
|
|
||||||
|
# Fade out: bright -> dim -> verydim -> hidden
|
||||||
|
show_owl "$from_idx" "dim"
|
||||||
|
sleep 0.1
|
||||||
|
show_owl "$from_idx" "verydim"
|
||||||
|
sleep 0.1
|
||||||
|
clear_owl_area
|
||||||
|
|
||||||
|
# Fade in: hidden -> verydim -> dim -> bright
|
||||||
|
show_owl "$to_idx" "verydim"
|
||||||
|
sleep 0.1
|
||||||
|
show_owl "$to_idx" "dim"
|
||||||
|
sleep 0.1
|
||||||
|
show_owl "$to_idx" "bright"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Cycle through all owls with animation
|
||||||
|
# Usage: cycle_owls [cycles] [delay_between_seconds]
|
||||||
|
# cycles=0 means infinite
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
cycle_owls() {
|
||||||
|
local cycles="${1:-3}"
|
||||||
|
local delay="${2:-2}"
|
||||||
|
local infinite=false
|
||||||
|
[[ "$cycles" -eq 0 ]] && infinite=true
|
||||||
|
|
||||||
|
local current=0
|
||||||
|
local i=0
|
||||||
|
|
||||||
|
while [[ "$infinite" == true ]] || [[ "$i" -lt $((cycles * OWL_COUNT)) ]]; do
|
||||||
|
[[ "${_OWL_RUNNING:-true}" == false ]] && return
|
||||||
|
|
||||||
|
local next=$(( (current + 1) % OWL_COUNT ))
|
||||||
|
|
||||||
|
if [[ "$i" -eq 0 ]]; then
|
||||||
|
show_owl "$current" "verydim"
|
||||||
|
sleep 0.1 || return
|
||||||
|
show_owl "$current" "dim"
|
||||||
|
sleep 0.1 || return
|
||||||
|
show_owl "$current" "bright"
|
||||||
|
else
|
||||||
|
transition_owl "$current" "$next"
|
||||||
|
current=$next
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep "$delay" || return
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Setup screen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
setup_screen() {
|
||||||
|
tput civis 2>/dev/null
|
||||||
|
clear
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Restore screen
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
restore_screen() {
|
||||||
|
tput cnorm 2>/dev/null
|
||||||
|
echo -ne "\033[0m"
|
||||||
|
clear
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# STRIX block title (static, drawn once below owl area)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
show_title() {
|
||||||
|
local c="\033[35;1m" # bold magenta
|
||||||
|
local tr=7
|
||||||
|
print_at_center "$((tr))" '███████╗████████╗██████╗ ██╗██╗ ██╗' "$c"
|
||||||
|
print_at_center "$((tr+1))" '██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝' "$c"
|
||||||
|
print_at_center "$((tr+2))" '███████╗ ██║ ██████╔╝██║ ╚███╔╝' "$c"
|
||||||
|
print_at_center "$((tr+3))" '╚════██║ ██║ ██╔══██╗██║ ██╔██╗' "$c"
|
||||||
|
print_at_center "$((tr+4))" '███████║ ██║ ██║ ██║██║██╔╝ ██╗' "$c"
|
||||||
|
print_at_center "$((tr+5))" '╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝' "$c"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
|
_OWL_RUNNING=true
|
||||||
|
|
||||||
|
_owl_cleanup() {
|
||||||
|
_OWL_RUNNING=false
|
||||||
|
restore_screen
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
trap _owl_cleanup INT TERM
|
||||||
|
trap restore_screen EXIT
|
||||||
|
|
||||||
|
setup_screen
|
||||||
|
show_title
|
||||||
|
cycle_owls 0 2
|
||||||
|
restore_screen
|
||||||
|
fi
|
||||||
Executable
+400
@@ -0,0 +1,400 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Strix -- prepare.sh (worker)
|
||||||
|
#
|
||||||
|
# Silent backend worker that prepares the system for Strix deployment.
|
||||||
|
# Detects OS, installs Docker and Docker Compose if missing.
|
||||||
|
#
|
||||||
|
# Protocol:
|
||||||
|
# - Every action is reported as a single-line JSON to stdout.
|
||||||
|
# - Types: check, ok, miss, install, error, done
|
||||||
|
# - Field "msg" is always human-readable.
|
||||||
|
# - Field "data" is optional, carries machine-readable details.
|
||||||
|
# - Last line is always: {"type":"done","ok":true} or {"type":"done","ok":false,"error":"..."}
|
||||||
|
# - All internal command output goes to /dev/null or stderr (never stdout).
|
||||||
|
# - Exit code: 0 = success, 1 = failure.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/prepare.sh
|
||||||
|
# result=$(bash scripts/prepare.sh)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# JSON helpers (no jq dependency)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Emit a JSON event line to stdout.
|
||||||
|
# Usage: emit "type" "msg" '{"key":"val"}'
|
||||||
|
emit() {
|
||||||
|
local type="$1"
|
||||||
|
local msg="$2"
|
||||||
|
local data="${3:-}"
|
||||||
|
|
||||||
|
# Escape double quotes in msg
|
||||||
|
msg="${msg//\\/\\\\}"
|
||||||
|
msg="${msg//\"/\\\"}"
|
||||||
|
|
||||||
|
if [[ -n "$data" ]]; then
|
||||||
|
printf '{"type":"%s","msg":"%s","data":%s}\n' "$type" "$msg" "$data"
|
||||||
|
else
|
||||||
|
printf '{"type":"%s","msg":"%s"}\n' "$type" "$msg"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Emit final done event and exit.
|
||||||
|
emit_done() {
|
||||||
|
local ok="$1"
|
||||||
|
local error="${2:-}"
|
||||||
|
|
||||||
|
if [[ "$ok" == "true" ]]; then
|
||||||
|
printf '{"type":"done","ok":true}\n'
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
error="${error//\\/\\\\}"
|
||||||
|
error="${error//\"/\\\"}"
|
||||||
|
printf '{"type":"done","ok":false,"error":"%s"}\n' "$error"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# OS detection
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
detect_os() {
|
||||||
|
emit "check" "Detecting operating system"
|
||||||
|
|
||||||
|
local kernel
|
||||||
|
kernel=$(uname -s 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
case "$kernel" in
|
||||||
|
Linux)
|
||||||
|
local os_id="unknown"
|
||||||
|
local os_ver="unknown"
|
||||||
|
local os_name="Unknown Linux"
|
||||||
|
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /etc/os-release
|
||||||
|
os_id="${ID:-unknown}"
|
||||||
|
os_ver="${VERSION_ID:-unknown}"
|
||||||
|
os_name="${PRETTY_NAME:-${ID} ${VERSION_ID}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local arch
|
||||||
|
arch=$(uname -m 2>/dev/null || echo "unknown")
|
||||||
|
local arch_label="$arch"
|
||||||
|
case "$arch" in
|
||||||
|
x86_64) arch_label="amd64" ;;
|
||||||
|
aarch64) arch_label="arm64" ;;
|
||||||
|
armv7l) arch_label="armv7" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
OS_TYPE="linux"
|
||||||
|
OS_ID="$os_id"
|
||||||
|
OS_VER="$os_ver"
|
||||||
|
OS_NAME="$os_name"
|
||||||
|
OS_ARCH="$arch_label"
|
||||||
|
|
||||||
|
emit "ok" "${os_name} (${arch_label})" \
|
||||||
|
"{\"os\":\"linux\",\"id\":\"${os_id}\",\"ver\":\"${os_ver}\",\"arch\":\"${arch_label}\"}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
Darwin)
|
||||||
|
local mac_ver
|
||||||
|
mac_ver=$(sw_vers -productVersion 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
local arch
|
||||||
|
arch=$(uname -m 2>/dev/null || echo "unknown")
|
||||||
|
local arch_label="$arch"
|
||||||
|
case "$arch" in
|
||||||
|
x86_64) arch_label="amd64" ;;
|
||||||
|
arm64) arch_label="arm64" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
OS_TYPE="mac"
|
||||||
|
OS_ID="macos"
|
||||||
|
OS_VER="$mac_ver"
|
||||||
|
OS_NAME="macOS ${mac_ver}"
|
||||||
|
OS_ARCH="$arch_label"
|
||||||
|
|
||||||
|
emit "ok" "macOS ${mac_ver} (${arch_label})" \
|
||||||
|
"{\"os\":\"mac\",\"id\":\"macos\",\"ver\":\"${mac_ver}\",\"arch\":\"${arch_label}\"}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
emit "error" "Unsupported OS: ${kernel}" \
|
||||||
|
"{\"kernel\":\"${kernel}\"}"
|
||||||
|
emit_done "false" "Unsupported operating system: ${kernel}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Root check (Linux only)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_root() {
|
||||||
|
if [[ "$OS_TYPE" == "mac" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "check" "Checking root privileges"
|
||||||
|
|
||||||
|
if [[ "$(id -u)" -eq 0 ]]; then
|
||||||
|
emit "ok" "Running as root"
|
||||||
|
else
|
||||||
|
emit "error" "Root privileges required. Run with sudo."
|
||||||
|
emit_done "false" "Not running as root"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# curl (required for Docker install and compose download)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ensure_curl() {
|
||||||
|
emit "check" "Checking curl"
|
||||||
|
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
emit "ok" "curl available"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "miss" "curl not found"
|
||||||
|
emit "install" "Installing curl"
|
||||||
|
|
||||||
|
local pkg_mgr="unknown"
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
pkg_mgr="apt"
|
||||||
|
emit "check" "Updating apt package lists"
|
||||||
|
if ! apt-get update -qq &>/dev/null; then
|
||||||
|
emit "error" "apt-get update failed"
|
||||||
|
emit_done "false" "Failed to update package lists"
|
||||||
|
fi
|
||||||
|
emit "ok" "Package lists updated"
|
||||||
|
emit "install" "Installing curl via apt"
|
||||||
|
apt-get install -y -qq curl &>/dev/null
|
||||||
|
elif command -v yum &>/dev/null; then
|
||||||
|
pkg_mgr="yum"
|
||||||
|
emit "install" "Installing curl via yum"
|
||||||
|
yum install -y -q curl &>/dev/null
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
pkg_mgr="dnf"
|
||||||
|
emit "install" "Installing curl via dnf"
|
||||||
|
dnf install -y -q curl &>/dev/null
|
||||||
|
elif command -v apk &>/dev/null; then
|
||||||
|
pkg_mgr="apk"
|
||||||
|
emit "install" "Installing curl via apk"
|
||||||
|
apk add --no-cache curl &>/dev/null
|
||||||
|
elif command -v pacman &>/dev/null; then
|
||||||
|
pkg_mgr="pacman"
|
||||||
|
emit "install" "Installing curl via pacman"
|
||||||
|
pacman -Sy --noconfirm curl &>/dev/null
|
||||||
|
elif command -v zypper &>/dev/null; then
|
||||||
|
pkg_mgr="zypper"
|
||||||
|
emit "install" "Installing curl via zypper"
|
||||||
|
zypper install -y curl &>/dev/null
|
||||||
|
else
|
||||||
|
emit "error" "No supported package manager found" "{\"tried\":\"apt,yum,dnf,apk,pacman,zypper\"}"
|
||||||
|
emit_done "false" "Cannot install curl: no supported package manager"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v curl &>/dev/null; then
|
||||||
|
emit "ok" "curl installed via ${pkg_mgr}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "error" "curl installation failed via ${pkg_mgr}"
|
||||||
|
emit_done "false" "curl installation failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Docker
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_docker() {
|
||||||
|
emit "check" "Checking Docker"
|
||||||
|
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
local ver
|
||||||
|
ver=$(docker --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "unknown")
|
||||||
|
emit "ok" "Docker ${ver}" "{\"version\":\"${ver}\"}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "miss" "Docker not found"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_linux() {
|
||||||
|
emit "install" "Downloading Docker install script from get.docker.com"
|
||||||
|
|
||||||
|
local tmp_script="/tmp/get-docker.sh"
|
||||||
|
if ! curl -fsSL https://get.docker.com -o "$tmp_script" 2>/dev/null; then
|
||||||
|
emit "error" "Failed to download get.docker.com"
|
||||||
|
emit_done "false" "Docker download failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "ok" "Docker install script downloaded"
|
||||||
|
emit "install" "Running Docker install script (this may take a minute)"
|
||||||
|
|
||||||
|
if sh "$tmp_script" &>/dev/null; then
|
||||||
|
rm -f "$tmp_script"
|
||||||
|
emit "ok" "Docker install script completed"
|
||||||
|
else
|
||||||
|
rm -f "$tmp_script"
|
||||||
|
emit "error" "Docker install script failed"
|
||||||
|
emit_done "false" "Docker installation failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable and start via systemd
|
||||||
|
if command -v systemctl &>/dev/null; then
|
||||||
|
emit "check" "Enabling Docker service"
|
||||||
|
systemctl enable docker &>/dev/null || true
|
||||||
|
systemctl start docker &>/dev/null || true
|
||||||
|
|
||||||
|
if systemctl is-active docker &>/dev/null; then
|
||||||
|
emit "ok" "Docker service started"
|
||||||
|
else
|
||||||
|
emit "error" "Docker service failed to start"
|
||||||
|
emit_done "false" "Docker service failed to start"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify docker binary works
|
||||||
|
if command -v docker &>/dev/null; then
|
||||||
|
local ver
|
||||||
|
ver=$(docker --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "unknown")
|
||||||
|
emit "ok" "Docker ${ver} installed" "{\"version\":\"${ver}\"}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "error" "Docker binary not found after install"
|
||||||
|
emit_done "false" "Docker installation failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_docker_mac() {
|
||||||
|
emit "check" "Checking Docker Desktop for Mac"
|
||||||
|
|
||||||
|
# Docker Desktop should already be installed on Mac.
|
||||||
|
# We can't silently install it -- it requires GUI interaction.
|
||||||
|
emit "error" "Docker not found. Install Docker Desktop from https://docker.com/products/docker-desktop"
|
||||||
|
emit_done "false" "Docker Desktop not installed on Mac"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Docker Compose
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_compose() {
|
||||||
|
emit "check" "Checking Docker Compose"
|
||||||
|
|
||||||
|
# Plugin (v2): docker compose
|
||||||
|
if docker compose version &>/dev/null; then
|
||||||
|
local ver
|
||||||
|
ver=$(docker compose version --short 2>/dev/null || echo "unknown")
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
emit "ok" "Docker Compose ${ver} (plugin)" "{\"version\":\"${ver}\",\"type\":\"plugin\"}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Standalone: docker-compose
|
||||||
|
if command -v docker-compose &>/dev/null; then
|
||||||
|
local ver
|
||||||
|
ver=$(docker-compose --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+' | head -1 || echo "unknown")
|
||||||
|
COMPOSE_CMD="docker-compose"
|
||||||
|
emit "ok" "Docker Compose ${ver} (standalone)" "{\"version\":\"${ver}\",\"type\":\"standalone\"}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "miss" "Docker Compose not found"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_compose_linux() {
|
||||||
|
emit "install" "Installing Docker Compose plugin"
|
||||||
|
|
||||||
|
local installed=false
|
||||||
|
|
||||||
|
# Try package manager first
|
||||||
|
if command -v apt-get &>/dev/null; then
|
||||||
|
apt-get update -qq &>/dev/null && apt-get install -y -qq docker-compose-plugin &>/dev/null && installed=true
|
||||||
|
elif command -v yum &>/dev/null; then
|
||||||
|
yum install -y -q docker-compose-plugin &>/dev/null && installed=true
|
||||||
|
elif command -v dnf &>/dev/null; then
|
||||||
|
dnf install -y -q docker-compose-plugin &>/dev/null && installed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: download binary
|
||||||
|
if [[ "$installed" == false ]]; then
|
||||||
|
emit "install" "Downloading Docker Compose binary"
|
||||||
|
|
||||||
|
local compose_ver="v2.29.1"
|
||||||
|
local compose_arch
|
||||||
|
case "$OS_ARCH" in
|
||||||
|
amd64) compose_arch="x86_64" ;;
|
||||||
|
arm64) compose_arch="aarch64" ;;
|
||||||
|
*) compose_arch="$(uname -m)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p /usr/local/lib/docker/cli-plugins &>/dev/null
|
||||||
|
if curl -fsSL "https://github.com/docker/compose/releases/download/${compose_ver}/docker-compose-linux-${compose_arch}" \
|
||||||
|
-o /usr/local/lib/docker/cli-plugins/docker-compose &>/dev/null; then
|
||||||
|
chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
|
||||||
|
installed=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
if [[ "$installed" == true ]] && docker compose version &>/dev/null; then
|
||||||
|
local ver
|
||||||
|
ver=$(docker compose version --short 2>/dev/null || echo "unknown")
|
||||||
|
COMPOSE_CMD="docker compose"
|
||||||
|
emit "ok" "Docker Compose ${ver} installed" "{\"version\":\"${ver}\"}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "error" "Docker Compose installation failed"
|
||||||
|
emit_done "false" "Docker Compose installation failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_compose_mac() {
|
||||||
|
# On Mac, Docker Compose comes with Docker Desktop
|
||||||
|
emit "error" "Docker Compose not found. It should be included with Docker Desktop."
|
||||||
|
emit_done "false" "Docker Compose missing on Mac"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
# 1. Detect OS
|
||||||
|
detect_os
|
||||||
|
|
||||||
|
# 2. Root check
|
||||||
|
check_root
|
||||||
|
|
||||||
|
# 3. curl (needed for Docker install, always present on Mac)
|
||||||
|
if [[ "$OS_TYPE" == "linux" ]]; then
|
||||||
|
ensure_curl
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Docker
|
||||||
|
if ! check_docker; then
|
||||||
|
case "$OS_TYPE" in
|
||||||
|
linux) install_docker_linux ;;
|
||||||
|
mac) install_docker_mac ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Docker Compose
|
||||||
|
if ! check_compose; then
|
||||||
|
case "$OS_TYPE" in
|
||||||
|
linux) install_compose_linux ;;
|
||||||
|
mac) install_compose_mac ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. All good
|
||||||
|
emit_done "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
Executable
+606
@@ -0,0 +1,606 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Strix -- proxmox-lxc-create.sh (worker)
|
||||||
|
#
|
||||||
|
# Creates an unprivileged Ubuntu LXC container on Proxmox with Docker support.
|
||||||
|
# Runs ON the Proxmox host. Uses only official CLI tools (pct, pveam, pvesm).
|
||||||
|
# Does NOT install anything inside the container -- just creates and starts it.
|
||||||
|
#
|
||||||
|
# Protocol:
|
||||||
|
# - Every action is reported as a single-line JSON to stdout.
|
||||||
|
# - Types: check, ok, miss, install, error, done
|
||||||
|
# - Last line: {"type":"done","ok":true,"data":{...}} or {"type":"done","ok":false,"error":"..."}
|
||||||
|
# - Exit code: 0 = success, 1 = failure.
|
||||||
|
#
|
||||||
|
# Parameters (all optional):
|
||||||
|
# --id ID Container ID (default: auto, next free)
|
||||||
|
# --hostname NAME Hostname (default: strix)
|
||||||
|
# --memory MB RAM in MB (default: 2048)
|
||||||
|
# --swap MB Swap in MB (default: 512)
|
||||||
|
# --disk GB Disk size in GB (default: 32)
|
||||||
|
# --cores N CPU cores (default: 2)
|
||||||
|
# --storage NAME Storage for container disk (default: auto)
|
||||||
|
# --bridge NAME Network bridge (default: auto, first vmbr*)
|
||||||
|
# --ip CIDR IP address, e.g. 10.0.99.110/24 (default: dhcp)
|
||||||
|
# --gateway IP Gateway (required if --ip is static)
|
||||||
|
# --password PASS Root password (default: auto-generated)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/proxmox-lxc-create.sh
|
||||||
|
# bash scripts/proxmox-lxc-create.sh --hostname strix --memory 4096 --cores 4
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Defaults
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CT_ID=""
|
||||||
|
CT_HOSTNAME="strix"
|
||||||
|
CT_MEMORY="2048"
|
||||||
|
CT_SWAP="512"
|
||||||
|
CT_DISK="32"
|
||||||
|
CT_CORES="2"
|
||||||
|
CT_STORAGE=""
|
||||||
|
CT_BRIDGE=""
|
||||||
|
CT_IP="dhcp"
|
||||||
|
CT_GATEWAY=""
|
||||||
|
CT_PASSWORD=""
|
||||||
|
|
||||||
|
TEMPLATE_STORAGE=""
|
||||||
|
TEMPLATE=""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parse CLI arguments
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--id) CT_ID="$2"; shift 2 ;;
|
||||||
|
--hostname) CT_HOSTNAME="$2"; shift 2 ;;
|
||||||
|
--memory) CT_MEMORY="$2"; shift 2 ;;
|
||||||
|
--swap) CT_SWAP="$2"; shift 2 ;;
|
||||||
|
--disk) CT_DISK="$2"; shift 2 ;;
|
||||||
|
--cores) CT_CORES="$2"; shift 2 ;;
|
||||||
|
--storage) CT_STORAGE="$2"; shift 2 ;;
|
||||||
|
--bridge) CT_BRIDGE="$2"; shift 2 ;;
|
||||||
|
--ip) CT_IP="$2"; shift 2 ;;
|
||||||
|
--gateway) CT_GATEWAY="$2"; shift 2 ;;
|
||||||
|
--password) CT_PASSWORD="$2"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# JSON helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
emit() {
|
||||||
|
local type="$1"
|
||||||
|
local msg="$2"
|
||||||
|
local data="${3:-}"
|
||||||
|
|
||||||
|
msg="${msg//\\/\\\\}"
|
||||||
|
msg="${msg//\"/\\\"}"
|
||||||
|
|
||||||
|
if [[ -n "$data" ]]; then
|
||||||
|
printf '{"type":"%s","msg":"%s","data":%s}\n' "$type" "$msg" "$data"
|
||||||
|
else
|
||||||
|
printf '{"type":"%s","msg":"%s"}\n' "$type" "$msg"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_done_ok() {
|
||||||
|
local data="$1"
|
||||||
|
printf '{"type":"done","ok":true,"data":%s}\n' "$data"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_done_fail() {
|
||||||
|
local error="$1"
|
||||||
|
error="${error//\\/\\\\}"
|
||||||
|
error="${error//\"/\\\"}"
|
||||||
|
printf '{"type":"done","ok":false,"error":"%s"}\n' "$error"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup on failure: destroy container if it was partially created
|
||||||
|
cleanup_on_fail() {
|
||||||
|
local id="$1"
|
||||||
|
local msg="$2"
|
||||||
|
if pct status "$id" &>/dev/null; then
|
||||||
|
pct stop "$id" &>/dev/null || true
|
||||||
|
pct destroy "$id" --purge &>/dev/null || true
|
||||||
|
emit "ok" "Rolled back: container ${id} destroyed"
|
||||||
|
fi
|
||||||
|
emit "error" "$msg"
|
||||||
|
emit_done_fail "$msg"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Verify Proxmox environment
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_proxmox() {
|
||||||
|
emit "check" "Verifying Proxmox environment"
|
||||||
|
|
||||||
|
if ! command -v pct &>/dev/null; then
|
||||||
|
emit "error" "pct not found -- this script must run on a Proxmox host"
|
||||||
|
emit_done_fail "Not a Proxmox host"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v pveam &>/dev/null; then
|
||||||
|
emit "error" "pveam not found -- this script must run on a Proxmox host"
|
||||||
|
emit_done_fail "Not a Proxmox host"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pve_ver
|
||||||
|
pve_ver=$(pveversion 2>/dev/null | grep -oP 'pve-manager/\K[0-9]+\.[0-9]+' || echo "unknown")
|
||||||
|
emit "ok" "Proxmox VE ${pve_ver}" "{\"pve_version\":\"${pve_ver}\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Auto-detect container ID
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
resolve_ct_id() {
|
||||||
|
emit "check" "Resolving container ID"
|
||||||
|
|
||||||
|
if [[ -n "$CT_ID" ]]; then
|
||||||
|
# Verify it's free
|
||||||
|
if pct status "$CT_ID" &>/dev/null || qm status "$CT_ID" &>/dev/null; then
|
||||||
|
emit "error" "Container/VM ID ${CT_ID} is already in use"
|
||||||
|
emit_done_fail "CT ID ${CT_ID} already in use"
|
||||||
|
fi
|
||||||
|
emit "ok" "Using specified ID: ${CT_ID}"
|
||||||
|
else
|
||||||
|
CT_ID=$(pvesh get /cluster/nextid 2>/dev/null || echo "")
|
||||||
|
if [[ -z "$CT_ID" ]]; then
|
||||||
|
emit "error" "Failed to get next free container ID"
|
||||||
|
emit_done_fail "Cannot get next free CT ID"
|
||||||
|
fi
|
||||||
|
# Double-check it's actually free
|
||||||
|
if pct status "$CT_ID" &>/dev/null || qm status "$CT_ID" &>/dev/null; then
|
||||||
|
CT_ID=$((CT_ID + 1))
|
||||||
|
fi
|
||||||
|
emit "ok" "Auto-assigned ID: ${CT_ID}" "{\"id\":\"${CT_ID}\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Auto-detect storage
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
resolve_storage() {
|
||||||
|
# Container storage (rootdir)
|
||||||
|
emit "check" "Resolving container storage"
|
||||||
|
|
||||||
|
if [[ -n "$CT_STORAGE" ]]; then
|
||||||
|
if ! pvesm status 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "$CT_STORAGE"; then
|
||||||
|
emit "error" "Storage '${CT_STORAGE}' not found"
|
||||||
|
emit_done_fail "Storage ${CT_STORAGE} not found"
|
||||||
|
fi
|
||||||
|
emit "ok" "Using specified storage: ${CT_STORAGE}"
|
||||||
|
else
|
||||||
|
# Find first storage that supports rootdir content
|
||||||
|
CT_STORAGE=$(pvesm status -content rootdir 2>/dev/null | awk 'NR>1 && $2=="active"{print $1; exit}')
|
||||||
|
if [[ -z "$CT_STORAGE" ]]; then
|
||||||
|
# Fallback: try local-lvm, then local
|
||||||
|
if pvesm status 2>/dev/null | awk 'NR>1{print $1}' | grep -qx "local-lvm"; then
|
||||||
|
CT_STORAGE="local-lvm"
|
||||||
|
else
|
||||||
|
CT_STORAGE="local"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
emit "ok" "Auto-detected storage: ${CT_STORAGE}" "{\"storage\":\"${CT_STORAGE}\"}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Template storage (vztmpl)
|
||||||
|
emit "check" "Resolving template storage"
|
||||||
|
TEMPLATE_STORAGE=$(pvesm status -content vztmpl 2>/dev/null | awk 'NR>1 && $2=="active"{print $1; exit}')
|
||||||
|
if [[ -z "$TEMPLATE_STORAGE" ]]; then
|
||||||
|
TEMPLATE_STORAGE="local"
|
||||||
|
fi
|
||||||
|
emit "ok" "Template storage: ${TEMPLATE_STORAGE}" "{\"template_storage\":\"${TEMPLATE_STORAGE}\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Check free space
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
check_free_space() {
|
||||||
|
emit "check" "Checking free space on ${CT_STORAGE}"
|
||||||
|
|
||||||
|
local avail_kb
|
||||||
|
avail_kb=$(pvesm status 2>/dev/null | awk -v s="$CT_STORAGE" '$1==s{print $6}')
|
||||||
|
|
||||||
|
if [[ -n "$avail_kb" ]]; then
|
||||||
|
local avail_gb=$((avail_kb / 1024 / 1024))
|
||||||
|
local required_gb=$CT_DISK
|
||||||
|
|
||||||
|
if [[ "$avail_gb" -lt "$required_gb" ]]; then
|
||||||
|
emit "error" "Not enough space: ${avail_gb}GB available, ${required_gb}GB required"
|
||||||
|
emit_done_fail "Not enough disk space on ${CT_STORAGE}"
|
||||||
|
fi
|
||||||
|
emit "ok" "${avail_gb}GB available, ${required_gb}GB required"
|
||||||
|
else
|
||||||
|
emit "ok" "Could not determine free space, proceeding"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. Auto-detect network bridge
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
resolve_bridge() {
|
||||||
|
emit "check" "Resolving network bridge"
|
||||||
|
|
||||||
|
if [[ -n "$CT_BRIDGE" ]]; then
|
||||||
|
emit "ok" "Using specified bridge: ${CT_BRIDGE}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find first vmbr* interface
|
||||||
|
CT_BRIDGE=$(ip link show 2>/dev/null | grep -oP 'vmbr\d+' | head -1)
|
||||||
|
|
||||||
|
if [[ -z "$CT_BRIDGE" ]]; then
|
||||||
|
CT_BRIDGE="vmbr0"
|
||||||
|
emit "ok" "Defaulting to bridge: vmbr0"
|
||||||
|
else
|
||||||
|
emit "ok" "Auto-detected bridge: ${CT_BRIDGE}" "{\"bridge\":\"${CT_BRIDGE}\"}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. Generate password
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
resolve_password() {
|
||||||
|
if [[ -n "$CT_PASSWORD" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "check" "Generating root password"
|
||||||
|
CT_PASSWORD=$(openssl rand -base64 12 2>/dev/null | tr -d '/+=' | head -c 16)
|
||||||
|
if [[ -z "$CT_PASSWORD" ]]; then
|
||||||
|
# Fallback if openssl not available
|
||||||
|
CT_PASSWORD=$(head -c 32 /dev/urandom | base64 | tr -d '/+=' | head -c 16)
|
||||||
|
fi
|
||||||
|
emit "ok" "Root password generated"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7. Download Ubuntu template
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
download_template() {
|
||||||
|
emit "check" "Searching for Ubuntu template"
|
||||||
|
|
||||||
|
# Check if already downloaded locally
|
||||||
|
TEMPLATE=$(pveam list "$TEMPLATE_STORAGE" 2>/dev/null \
|
||||||
|
| awk '$1 ~ /ubuntu-24\.04.*-standard_/ {print $1}' \
|
||||||
|
| sed 's|.*/||' \
|
||||||
|
| sort -V \
|
||||||
|
| tail -1)
|
||||||
|
|
||||||
|
if [[ -n "$TEMPLATE" ]]; then
|
||||||
|
emit "ok" "Template found locally: ${TEMPLATE}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Not local, try online
|
||||||
|
emit "miss" "No local Ubuntu 24.04 template"
|
||||||
|
emit "install" "Updating template catalog"
|
||||||
|
|
||||||
|
if command -v timeout &>/dev/null; then
|
||||||
|
timeout 30 pveam update &>/dev/null || true
|
||||||
|
else
|
||||||
|
pveam update &>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Search for Ubuntu 24.04
|
||||||
|
TEMPLATE=$(pveam available --section system 2>/dev/null \
|
||||||
|
| awk '$2 ~ /ubuntu-24\.04.*-standard_/ {print $2}' \
|
||||||
|
| sort -V \
|
||||||
|
| tail -1)
|
||||||
|
|
||||||
|
# Fallback to 22.04
|
||||||
|
if [[ -z "$TEMPLATE" ]]; then
|
||||||
|
emit "miss" "Ubuntu 24.04 not available, trying 22.04"
|
||||||
|
TEMPLATE=$(pveam available --section system 2>/dev/null \
|
||||||
|
| awk '$2 ~ /ubuntu-22\.04.*-standard_/ {print $2}' \
|
||||||
|
| sort -V \
|
||||||
|
| tail -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$TEMPLATE" ]]; then
|
||||||
|
emit "error" "No Ubuntu template found"
|
||||||
|
emit_done_fail "No Ubuntu template available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "install" "Downloading template: ${TEMPLATE}"
|
||||||
|
|
||||||
|
local attempt
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
if pveam download "$TEMPLATE_STORAGE" "$TEMPLATE" &>/dev/null; then
|
||||||
|
emit "ok" "Template downloaded: ${TEMPLATE}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [[ "$attempt" -lt 3 ]]; then
|
||||||
|
emit "check" "Download failed, retrying (${attempt}/3)"
|
||||||
|
sleep $((attempt * 5))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
emit "error" "Template download failed after 3 attempts"
|
||||||
|
emit_done_fail "Template download failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 8. Ensure subuid/subgid (required for unprivileged containers)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
fix_subuid_subgid() {
|
||||||
|
emit "check" "Checking subuid/subgid mappings"
|
||||||
|
|
||||||
|
local changed=false
|
||||||
|
|
||||||
|
if ! grep -q "root:100000:65536" /etc/subuid 2>/dev/null; then
|
||||||
|
echo "root:100000:65536" >> /etc/subuid
|
||||||
|
changed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! grep -q "root:100000:65536" /etc/subgid 2>/dev/null; then
|
||||||
|
echo "root:100000:65536" >> /etc/subgid
|
||||||
|
changed=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$changed" == true ]]; then
|
||||||
|
emit "ok" "subuid/subgid mappings added"
|
||||||
|
else
|
||||||
|
emit "ok" "subuid/subgid mappings present"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 9. Create container
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
create_container() {
|
||||||
|
emit "install" "Creating LXC container ${CT_ID}"
|
||||||
|
|
||||||
|
# Build network string
|
||||||
|
local net_string="name=eth0,bridge=${CT_BRIDGE}"
|
||||||
|
if [[ "$CT_IP" == "dhcp" ]]; then
|
||||||
|
net_string="${net_string},ip=dhcp,ip6=dhcp"
|
||||||
|
else
|
||||||
|
net_string="${net_string},ip=${CT_IP}"
|
||||||
|
[[ -n "$CT_GATEWAY" ]] && net_string="${net_string},gw=${CT_GATEWAY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pct_cmd=(
|
||||||
|
pct create "$CT_ID" "${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}"
|
||||||
|
-hostname "$CT_HOSTNAME"
|
||||||
|
-cores "$CT_CORES"
|
||||||
|
-memory "$CT_MEMORY"
|
||||||
|
-swap "$CT_SWAP"
|
||||||
|
-rootfs "${CT_STORAGE}:${CT_DISK}"
|
||||||
|
-net0 "$net_string"
|
||||||
|
-features "nesting=1,keyctl=1"
|
||||||
|
-unprivileged 1
|
||||||
|
-onboot 1
|
||||||
|
-password "$CT_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "${pct_cmd[@]}" &>/dev/null; then
|
||||||
|
emit "ok" "Container ${CT_ID} created"
|
||||||
|
else
|
||||||
|
# Retry once -- could be race condition on ID
|
||||||
|
if pct status "$CT_ID" &>/dev/null; then
|
||||||
|
emit "error" "Container ID ${CT_ID} was claimed by another process"
|
||||||
|
CT_ID=$((CT_ID + 1))
|
||||||
|
pct_cmd[2]="$CT_ID"
|
||||||
|
if "${pct_cmd[@]}" &>/dev/null; then
|
||||||
|
emit "ok" "Container ${CT_ID} created (reassigned ID)"
|
||||||
|
else
|
||||||
|
emit "error" "Container creation failed"
|
||||||
|
emit_done_fail "pct create failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
emit "error" "Container creation failed"
|
||||||
|
emit_done_fail "pct create failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 10. Start container
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
start_container() {
|
||||||
|
emit "install" "Starting container ${CT_ID}"
|
||||||
|
|
||||||
|
if pct start "$CT_ID" &>/dev/null; then
|
||||||
|
emit "ok" "Container ${CT_ID} started"
|
||||||
|
else
|
||||||
|
cleanup_on_fail "$CT_ID" "Failed to start container ${CT_ID}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 11. Setup autologin for Proxmox console
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
setup_autologin() {
|
||||||
|
emit "check" "Configuring console autologin"
|
||||||
|
|
||||||
|
# Wait a moment for systemd to initialize inside the container
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
pct exec "$CT_ID" -- bash -c '
|
||||||
|
mkdir -p /etc/systemd/system/container-getty@1.service.d
|
||||||
|
cat > /etc/systemd/system/container-getty@1.service.d/override.conf <<AUTOLOGIN
|
||||||
|
[Service]
|
||||||
|
ExecStart=
|
||||||
|
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud tty%I 115200,38400,9600 \$TERM
|
||||||
|
AUTOLOGIN
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart container-getty@1.service
|
||||||
|
' &>/dev/null
|
||||||
|
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
emit "ok" "Console autologin enabled"
|
||||||
|
else
|
||||||
|
# Non-fatal -- container works fine without it
|
||||||
|
emit "ok" "Console autologin skipped (non-critical)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 12. Select fastest apt mirror and update
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
setup_apt_mirror() {
|
||||||
|
emit "check" "Selecting fastest apt mirror"
|
||||||
|
|
||||||
|
# Wait for network inside container first
|
||||||
|
local net_ready=false
|
||||||
|
for (( i = 1; i <= 15; i++ )); do
|
||||||
|
if pct exec "$CT_ID" -- ping -c 1 -W 2 archive.ubuntu.com &>/dev/null; then
|
||||||
|
net_ready=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$net_ready" == false ]]; then
|
||||||
|
emit "ok" "Network not ready, skipping mirror selection"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ping mirrors in parallel, pick fastest
|
||||||
|
local best_mirror="archive.ubuntu.com"
|
||||||
|
local best_time=9999
|
||||||
|
|
||||||
|
local mirrors=(
|
||||||
|
"archive.ubuntu.com"
|
||||||
|
"mirror.yandex.ru"
|
||||||
|
"de.archive.ubuntu.com"
|
||||||
|
"nl.archive.ubuntu.com"
|
||||||
|
"us.archive.ubuntu.com"
|
||||||
|
"mirror.linux-ia64.org"
|
||||||
|
)
|
||||||
|
|
||||||
|
local tmpdir
|
||||||
|
tmpdir=$(pct exec "$CT_ID" -- mktemp -d 2>/dev/null || echo "/tmp/mirror-test")
|
||||||
|
|
||||||
|
# Launch all pings in parallel inside the container
|
||||||
|
pct exec "$CT_ID" -- bash -c "
|
||||||
|
mkdir -p ${tmpdir}
|
||||||
|
for m in ${mirrors[*]}; do
|
||||||
|
(ping -c 1 -W 2 \$m 2>/dev/null | grep -oP 'time=\K[0-9.]+' > ${tmpdir}/\$m || echo 9999 > ${tmpdir}/\$m) &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
" &>/dev/null
|
||||||
|
|
||||||
|
# Read results
|
||||||
|
for m in "${mirrors[@]}"; do
|
||||||
|
local ms
|
||||||
|
ms=$(pct exec "$CT_ID" -- cat "${tmpdir}/${m}" 2>/dev/null | head -1)
|
||||||
|
ms="${ms:-9999}"
|
||||||
|
|
||||||
|
# Compare as integers (strip decimal)
|
||||||
|
local ms_int="${ms%%.*}"
|
||||||
|
ms_int="${ms_int:-9999}"
|
||||||
|
|
||||||
|
if [[ "$ms_int" -lt "$best_time" ]]; then
|
||||||
|
best_time="$ms_int"
|
||||||
|
best_mirror="$m"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
pct exec "$CT_ID" -- rm -rf "$tmpdir" &>/dev/null
|
||||||
|
|
||||||
|
emit "ok" "Fastest mirror: ${best_mirror} (${best_time}ms)" "{\"mirror\":\"${best_mirror}\",\"latency_ms\":${best_time}}"
|
||||||
|
|
||||||
|
# Apply mirror if different from default
|
||||||
|
if [[ "$best_mirror" != "archive.ubuntu.com" ]]; then
|
||||||
|
emit "install" "Configuring apt mirror: ${best_mirror}"
|
||||||
|
pct exec "$CT_ID" -- bash -c "
|
||||||
|
sed -i 's|http://archive.ubuntu.com|http://${best_mirror}|g' /etc/apt/sources.list
|
||||||
|
" &>/dev/null
|
||||||
|
emit "ok" "Apt mirror set to ${best_mirror}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run apt update
|
||||||
|
emit "install" "Updating package lists"
|
||||||
|
if pct exec "$CT_ID" -- bash -c "apt-get update -qq" &>/dev/null; then
|
||||||
|
emit "ok" "Package lists updated"
|
||||||
|
else
|
||||||
|
emit "ok" "Package lists update had warnings (non-critical)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 13. Wait for network and get IP
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
wait_for_network() {
|
||||||
|
emit "check" "Waiting for network"
|
||||||
|
|
||||||
|
local ip=""
|
||||||
|
local retries=30
|
||||||
|
|
||||||
|
for (( i = 1; i <= retries; i++ )); do
|
||||||
|
ip=$(pct exec "$CT_ID" -- ip -4 -o addr show dev eth0 2>/dev/null \
|
||||||
|
| awk '{print $4}' \
|
||||||
|
| cut -d/ -f1 \
|
||||||
|
| head -1)
|
||||||
|
|
||||||
|
if [[ -n "$ip" && "$ip" != "127.0.0.1" ]]; then
|
||||||
|
emit "ok" "Container IP: ${ip}" "{\"ip\":\"${ip}\"}"
|
||||||
|
CT_ACTUAL_IP="$ip"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Fallback: no IP but container is running
|
||||||
|
CT_ACTUAL_IP="unknown"
|
||||||
|
emit "ok" "Container running but IP not detected (check network manually)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
# 1. Verify we're on Proxmox
|
||||||
|
check_proxmox
|
||||||
|
|
||||||
|
# 2. Container ID
|
||||||
|
resolve_ct_id
|
||||||
|
|
||||||
|
# 3. Storage
|
||||||
|
resolve_storage
|
||||||
|
|
||||||
|
# 4. Free space
|
||||||
|
check_free_space
|
||||||
|
|
||||||
|
# 5. Network bridge
|
||||||
|
resolve_bridge
|
||||||
|
|
||||||
|
# 6. Password
|
||||||
|
resolve_password
|
||||||
|
|
||||||
|
# 7. Template
|
||||||
|
download_template
|
||||||
|
|
||||||
|
# 8. subuid/subgid
|
||||||
|
fix_subuid_subgid
|
||||||
|
|
||||||
|
# 9. Create
|
||||||
|
create_container
|
||||||
|
|
||||||
|
# 10. Start
|
||||||
|
start_container
|
||||||
|
|
||||||
|
# 11. Autologin
|
||||||
|
setup_autologin
|
||||||
|
|
||||||
|
# 12. Apt mirror + update
|
||||||
|
setup_apt_mirror
|
||||||
|
|
||||||
|
# 13. Network
|
||||||
|
wait_for_network
|
||||||
|
|
||||||
|
# 14. Done
|
||||||
|
emit_done_ok "{\"id\":\"${CT_ID}\",\"hostname\":\"${CT_HOSTNAME}\",\"ip\":\"${CT_ACTUAL_IP}\",\"password\":\"${CT_PASSWORD}\",\"memory\":\"${CT_MEMORY}\",\"swap\":\"${CT_SWAP}\",\"disk\":\"${CT_DISK}\",\"cores\":\"${CT_CORES}\",\"storage\":\"${CT_STORAGE}\",\"bridge\":\"${CT_BRIDGE}\",\"template\":\"${TEMPLATE}\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
Executable
+428
@@ -0,0 +1,428 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Strix -- strix-frigate.sh (worker)
|
||||||
|
#
|
||||||
|
# Deploys Strix + Frigate together via Docker Compose.
|
||||||
|
# Generates docker-compose.yml dynamically (devices depend on hardware),
|
||||||
|
# creates .env, pulls images, starts containers, runs healthchecks.
|
||||||
|
#
|
||||||
|
# Protocol:
|
||||||
|
# - Every action is reported as a single-line JSON to stdout.
|
||||||
|
# - Types: check, ok, miss, install, error, done
|
||||||
|
# - Last line is always: {"type":"done","ok":true,...} or {"type":"done","ok":false,"error":"..."}
|
||||||
|
# - Exit code: 0 = success, 1 = failure.
|
||||||
|
#
|
||||||
|
# Parameters (all optional):
|
||||||
|
# --port PORT Strix listen port (default: 4567)
|
||||||
|
# --tag TAG Strix image tag (default: latest)
|
||||||
|
# --log-level LEVEL Log level: debug, info, warn, error, trace
|
||||||
|
# --go2rtc-url URL External go2rtc URL
|
||||||
|
# --shm-size SIZE Frigate shm_size (default: 512mb)
|
||||||
|
# --frigate-tag TAG Frigate image tag (default: stable)
|
||||||
|
# --dir DIR Working directory (default: /opt/strix)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/strix-frigate.sh
|
||||||
|
# bash scripts/strix-frigate.sh --port 4567 --frigate-tag stable-tensorrt
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Defaults
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
STRIX_PORT="4567"
|
||||||
|
STRIX_TAG="latest"
|
||||||
|
STRIX_LOG_LEVEL=""
|
||||||
|
STRIX_GO2RTC_URL=""
|
||||||
|
FRIGATE_SHM="512mb"
|
||||||
|
FRIGATE_TAG="stable"
|
||||||
|
STRIX_DIR="/opt/strix"
|
||||||
|
STRIX_IMAGE="eduard256/strix"
|
||||||
|
FRIGATE_IMAGE="ghcr.io/blakeblackshear/frigate"
|
||||||
|
|
||||||
|
# Detected devices (populated by detect_devices)
|
||||||
|
DEVICES=()
|
||||||
|
DEVICE_NAMES=()
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parse CLI arguments
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--port) STRIX_PORT="$2"; shift 2 ;;
|
||||||
|
--tag) STRIX_TAG="$2"; shift 2 ;;
|
||||||
|
--log-level) STRIX_LOG_LEVEL="$2"; shift 2 ;;
|
||||||
|
--go2rtc-url) STRIX_GO2RTC_URL="$2"; shift 2 ;;
|
||||||
|
--shm-size) FRIGATE_SHM="$2"; shift 2 ;;
|
||||||
|
--frigate-tag) FRIGATE_TAG="$2"; shift 2 ;;
|
||||||
|
--dir) STRIX_DIR="$2"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# JSON helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
emit() {
|
||||||
|
local type="$1"
|
||||||
|
local msg="$2"
|
||||||
|
local data="${3:-}"
|
||||||
|
|
||||||
|
msg="${msg//\\/\\\\}"
|
||||||
|
msg="${msg//\"/\\\"}"
|
||||||
|
|
||||||
|
if [[ -n "$data" ]]; then
|
||||||
|
printf '{"type":"%s","msg":"%s","data":%s}\n' "$type" "$msg" "$data"
|
||||||
|
else
|
||||||
|
printf '{"type":"%s","msg":"%s"}\n' "$type" "$msg"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_done_ok() {
|
||||||
|
# Accepts raw JSON data string
|
||||||
|
local data="$1"
|
||||||
|
printf '{"type":"done","ok":true,"data":%s}\n' "$data"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_done_fail() {
|
||||||
|
local error="$1"
|
||||||
|
error="${error//\\/\\\\}"
|
||||||
|
error="${error//\"/\\\"}"
|
||||||
|
printf '{"type":"done","ok":false,"error":"%s"}\n' "$error"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Detect LAN IP
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
detect_lan_ip() {
|
||||||
|
local ip=""
|
||||||
|
ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1)
|
||||||
|
[[ -z "$ip" ]] && ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
|
[[ -z "$ip" ]] && ip="localhost"
|
||||||
|
echo "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Working directory
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
setup_dir() {
|
||||||
|
emit "check" "Checking working directory ${STRIX_DIR}"
|
||||||
|
|
||||||
|
if [[ -d "$STRIX_DIR" ]]; then
|
||||||
|
emit "ok" "Directory exists: ${STRIX_DIR}"
|
||||||
|
else
|
||||||
|
emit "install" "Creating directory ${STRIX_DIR}"
|
||||||
|
if mkdir -p "$STRIX_DIR" 2>/dev/null; then
|
||||||
|
emit "ok" "Directory created: ${STRIX_DIR}"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to create directory ${STRIX_DIR}"
|
||||||
|
emit_done_fail "Cannot create ${STRIX_DIR}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Frigate subdirectories
|
||||||
|
emit "check" "Checking Frigate directories"
|
||||||
|
|
||||||
|
mkdir -p "${STRIX_DIR}/frigate/config" 2>/dev/null
|
||||||
|
mkdir -p "${STRIX_DIR}/frigate/storage" 2>/dev/null
|
||||||
|
|
||||||
|
if [[ -d "${STRIX_DIR}/frigate/config" ]] && [[ -d "${STRIX_DIR}/frigate/storage" ]]; then
|
||||||
|
emit "ok" "Frigate directories ready"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to create Frigate directories"
|
||||||
|
emit_done_fail "Cannot create Frigate directories"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Detect hardware devices
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
detect_devices() {
|
||||||
|
emit "check" "Detecting hardware accelerators"
|
||||||
|
|
||||||
|
local found=0
|
||||||
|
|
||||||
|
# USB Coral
|
||||||
|
emit "check" "Checking for USB Coral"
|
||||||
|
if command -v lsusb &>/dev/null && lsusb 2>/dev/null | grep -qE "1a6e:089a|18d1:9302"; then
|
||||||
|
DEVICES+=("/dev/bus/usb:/dev/bus/usb")
|
||||||
|
DEVICE_NAMES+=("usb_coral")
|
||||||
|
emit "ok" "USB Coral detected" "{\"device\":\"usb_coral\",\"path\":\"/dev/bus/usb\"}"
|
||||||
|
found=$((found + 1))
|
||||||
|
else
|
||||||
|
emit "miss" "USB Coral not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PCIe Coral
|
||||||
|
emit "check" "Checking for PCIe Coral"
|
||||||
|
if [[ -e /dev/apex_0 ]]; then
|
||||||
|
DEVICES+=("/dev/apex_0:/dev/apex_0")
|
||||||
|
DEVICE_NAMES+=("pcie_coral")
|
||||||
|
emit "ok" "PCIe Coral detected" "{\"device\":\"pcie_coral\",\"path\":\"/dev/apex_0\"}"
|
||||||
|
found=$((found + 1))
|
||||||
|
else
|
||||||
|
emit "miss" "PCIe Coral not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Intel / AMD GPU
|
||||||
|
emit "check" "Checking for Intel/AMD GPU"
|
||||||
|
if [[ -e /dev/dri/renderD128 ]]; then
|
||||||
|
DEVICES+=("/dev/dri:/dev/dri")
|
||||||
|
DEVICE_NAMES+=("gpu")
|
||||||
|
emit "ok" "GPU detected (Intel/AMD)" "{\"device\":\"gpu\",\"path\":\"/dev/dri\"}"
|
||||||
|
found=$((found + 1))
|
||||||
|
else
|
||||||
|
emit "miss" "Intel/AMD GPU not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Intel NPU
|
||||||
|
emit "check" "Checking for Intel NPU"
|
||||||
|
if [[ -e /dev/accel ]]; then
|
||||||
|
DEVICES+=("/dev/accel:/dev/accel")
|
||||||
|
DEVICE_NAMES+=("intel_npu")
|
||||||
|
emit "ok" "Intel NPU detected" "{\"device\":\"intel_npu\",\"path\":\"/dev/accel\"}"
|
||||||
|
found=$((found + 1))
|
||||||
|
else
|
||||||
|
emit "miss" "Intel NPU not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Raspberry Pi 4 video
|
||||||
|
emit "check" "Checking for Raspberry Pi video device"
|
||||||
|
if [[ -e /dev/video11 ]]; then
|
||||||
|
DEVICES+=("/dev/video11:/dev/video11")
|
||||||
|
DEVICE_NAMES+=("rpi_video")
|
||||||
|
emit "ok" "Raspberry Pi video device detected" "{\"device\":\"rpi_video\",\"path\":\"/dev/video11\"}"
|
||||||
|
found=$((found + 1))
|
||||||
|
else
|
||||||
|
emit "miss" "Raspberry Pi video device not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$found" -eq 0 ]]; then
|
||||||
|
emit "ok" "No hardware accelerators found, using CPU only"
|
||||||
|
else
|
||||||
|
emit "ok" "${found} hardware accelerator(s) detected"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Generate docker-compose.yml
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
generate_compose() {
|
||||||
|
emit "check" "Generating docker-compose.yml"
|
||||||
|
|
||||||
|
# Build devices section
|
||||||
|
local devices_block=""
|
||||||
|
if [[ ${#DEVICES[@]} -gt 0 ]]; then
|
||||||
|
devices_block=" devices:"
|
||||||
|
for dev in "${DEVICES[@]}"; do
|
||||||
|
devices_block="${devices_block}
|
||||||
|
- ${dev}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build compose file
|
||||||
|
cat > "${STRIX_DIR}/docker-compose.yml" <<EOF
|
||||||
|
# Strix + Frigate
|
||||||
|
# Generated by strix-frigate.sh
|
||||||
|
|
||||||
|
services:
|
||||||
|
strix:
|
||||||
|
container_name: strix
|
||||||
|
image: ${STRIX_IMAGE}:\${STRIX_TAG:-latest}
|
||||||
|
network_mode: host
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file: .env
|
||||||
|
depends_on:
|
||||||
|
frigate:
|
||||||
|
condition: service_started
|
||||||
|
|
||||||
|
frigate:
|
||||||
|
container_name: frigate
|
||||||
|
image: ${FRIGATE_IMAGE}:${FRIGATE_TAG}
|
||||||
|
privileged: true
|
||||||
|
network_mode: host
|
||||||
|
restart: unless-stopped
|
||||||
|
stop_grace_period: 30s
|
||||||
|
shm_size: "${FRIGATE_SHM}"
|
||||||
|
${devices_block}
|
||||||
|
volumes:
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- ./frigate/config:/config
|
||||||
|
- ./frigate/storage:/media/frigate
|
||||||
|
- type: tmpfs
|
||||||
|
target: /tmp/cache
|
||||||
|
tmpfs:
|
||||||
|
size: 1000000000
|
||||||
|
environment:
|
||||||
|
FRIGATE_RTSP_PASSWORD: "password"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
emit "ok" "docker-compose.yml generated" "{\"frigate_tag\":\"${FRIGATE_TAG}\",\"shm_size\":\"${FRIGATE_SHM}\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Generate .env
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
generate_env() {
|
||||||
|
emit "check" "Generating .env configuration"
|
||||||
|
|
||||||
|
cat > "${STRIX_DIR}/.env" <<EOF
|
||||||
|
# Strix configuration -- generated by strix-frigate.sh
|
||||||
|
STRIX_TAG=${STRIX_TAG}
|
||||||
|
STRIX_LISTEN=:${STRIX_PORT}
|
||||||
|
STRIX_FRIGATE_URL=http://localhost:5000
|
||||||
|
EOF
|
||||||
|
|
||||||
|
emit "ok" "Frigate URL: http://localhost:5000 (internal API)"
|
||||||
|
|
||||||
|
if [[ -n "$STRIX_GO2RTC_URL" ]]; then
|
||||||
|
echo "STRIX_GO2RTC_URL=${STRIX_GO2RTC_URL}" >> "${STRIX_DIR}/.env"
|
||||||
|
emit "ok" "go2rtc URL: ${STRIX_GO2RTC_URL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$STRIX_LOG_LEVEL" ]]; then
|
||||||
|
echo "STRIX_LOG_LEVEL=${STRIX_LOG_LEVEL}" >> "${STRIX_DIR}/.env"
|
||||||
|
emit "ok" "Log level: ${STRIX_LOG_LEVEL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "ok" ".env generated (port ${STRIX_PORT})"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. Pull images
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
pull_images() {
|
||||||
|
emit "check" "Pulling Frigate image ${FRIGATE_IMAGE}:${FRIGATE_TAG} (this may take a while)"
|
||||||
|
|
||||||
|
if docker pull "${FRIGATE_IMAGE}:${FRIGATE_TAG}" &>/dev/null; then
|
||||||
|
emit "ok" "Frigate image pulled: ${FRIGATE_TAG}"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to pull Frigate image ${FRIGATE_IMAGE}:${FRIGATE_TAG}"
|
||||||
|
emit_done_fail "Frigate image pull failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "check" "Pulling Strix image ${STRIX_IMAGE}:${STRIX_TAG}"
|
||||||
|
|
||||||
|
if docker pull "${STRIX_IMAGE}:${STRIX_TAG}" &>/dev/null; then
|
||||||
|
emit "ok" "Strix image pulled: ${STRIX_TAG}"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to pull Strix image ${STRIX_IMAGE}:${STRIX_TAG}"
|
||||||
|
emit_done_fail "Strix image pull failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. Start containers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
start_containers() {
|
||||||
|
local running_frigate=false
|
||||||
|
local running_strix=false
|
||||||
|
|
||||||
|
docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^frigate$' && running_frigate=true
|
||||||
|
docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^strix$' && running_strix=true
|
||||||
|
|
||||||
|
if [[ "$running_frigate" == true ]] || [[ "$running_strix" == true ]]; then
|
||||||
|
emit "check" "Existing containers found, recreating"
|
||||||
|
if docker compose -f "${STRIX_DIR}/docker-compose.yml" up -d --force-recreate &>/dev/null; then
|
||||||
|
emit "ok" "Containers recreated"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to recreate containers"
|
||||||
|
emit_done_fail "Container recreate failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
emit "install" "Starting Frigate and Strix containers"
|
||||||
|
if docker compose -f "${STRIX_DIR}/docker-compose.yml" up -d &>/dev/null; then
|
||||||
|
emit "ok" "Containers started"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to start containers"
|
||||||
|
emit_done_fail "Container start failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7. Healthchecks
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
healthcheck_frigate() {
|
||||||
|
emit "check" "Waiting for Frigate to respond on port 5000"
|
||||||
|
|
||||||
|
local retries=30
|
||||||
|
for (( i = 1; i <= retries; i++ )); do
|
||||||
|
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:5000/api/config" &>/dev/null; then
|
||||||
|
emit "ok" "Frigate is running on port 5000"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
emit "error" "Frigate healthcheck failed after ${retries} attempts"
|
||||||
|
emit_done_fail "Frigate healthcheck failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
healthcheck_strix() {
|
||||||
|
emit "check" "Waiting for Strix to respond on port ${STRIX_PORT}"
|
||||||
|
|
||||||
|
local retries=15
|
||||||
|
for (( i = 1; i <= retries; i++ )); do
|
||||||
|
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:${STRIX_PORT}/api/health" &>/dev/null; then
|
||||||
|
local version
|
||||||
|
version=$(curl -sf --max-time 3 "http://localhost:${STRIX_PORT}/api" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||||
|
emit "ok" "Strix v${version} is running on port ${STRIX_PORT}" "{\"version\":\"${version}\"}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
emit "error" "Strix healthcheck failed after ${retries} attempts"
|
||||||
|
emit_done_fail "Strix healthcheck failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
# 1. Working directory
|
||||||
|
setup_dir
|
||||||
|
|
||||||
|
# 2. Detect hardware
|
||||||
|
detect_devices
|
||||||
|
|
||||||
|
# 3. Generate compose (with detected devices)
|
||||||
|
generate_compose
|
||||||
|
|
||||||
|
# 4. Generate .env
|
||||||
|
generate_env
|
||||||
|
|
||||||
|
# 5. Pull images
|
||||||
|
pull_images
|
||||||
|
|
||||||
|
# 6. Start containers
|
||||||
|
start_containers
|
||||||
|
|
||||||
|
# 7. Healthchecks
|
||||||
|
healthcheck_frigate
|
||||||
|
healthcheck_strix
|
||||||
|
|
||||||
|
# 8. Done -- all URLs
|
||||||
|
local lan_ip
|
||||||
|
lan_ip=$(detect_lan_ip)
|
||||||
|
|
||||||
|
local strix_version
|
||||||
|
strix_version=$(curl -sf --max-time 3 "http://localhost:${STRIX_PORT}/api" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||||
|
|
||||||
|
# Build device names JSON array
|
||||||
|
local devices_json="["
|
||||||
|
local first=true
|
||||||
|
for name in "${DEVICE_NAMES[@]}"; do
|
||||||
|
[[ "$first" == true ]] && first=false || devices_json="${devices_json},"
|
||||||
|
devices_json="${devices_json}\"${name}\""
|
||||||
|
done
|
||||||
|
devices_json="${devices_json}]"
|
||||||
|
|
||||||
|
emit_done_ok "{\"ip\":\"${lan_ip}\",\"strix_url\":\"http://${lan_ip}:${STRIX_PORT}\",\"strix_version\":\"${strix_version}\",\"frigate_url\":\"http://${lan_ip}:8971\",\"frigate_internal\":\"http://${lan_ip}:5000\",\"go2rtc_url\":\"http://${lan_ip}:1984\",\"frigate_tag\":\"${FRIGATE_TAG}\",\"port\":\"${STRIX_PORT}\",\"devices\":${devices_json}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
Executable
+274
@@ -0,0 +1,274 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Strix -- strix.sh (worker)
|
||||||
|
#
|
||||||
|
# Deploys Strix container via Docker Compose.
|
||||||
|
# Downloads docker-compose.yml from GitHub (if not already present),
|
||||||
|
# generates .env from parameters, pulls image, starts container, healthchecks.
|
||||||
|
#
|
||||||
|
# Protocol:
|
||||||
|
# - Every action is reported as a single-line JSON to stdout.
|
||||||
|
# - Types: check, ok, miss, install, error, done
|
||||||
|
# - Field "msg" is always human-readable.
|
||||||
|
# - Field "data" is optional, carries machine-readable details.
|
||||||
|
# - Last line is always: {"type":"done","ok":true,...} or {"type":"done","ok":false,"error":"..."}
|
||||||
|
# - Exit code: 0 = success, 1 = failure.
|
||||||
|
#
|
||||||
|
# Parameters (all optional):
|
||||||
|
# --port PORT Strix listen port (default: 4567)
|
||||||
|
# --frigate-url URL Frigate URL, e.g. http://192.168.1.50:5000
|
||||||
|
# --go2rtc-url URL go2rtc URL, e.g. http://192.168.1.50:1984
|
||||||
|
# --log-level LEVEL Log level: debug, info, warn, error, trace (default: info)
|
||||||
|
# --tag TAG Docker image tag (default: latest)
|
||||||
|
# --dir DIR Working directory (default: /opt/strix)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash scripts/strix.sh
|
||||||
|
# bash scripts/strix.sh --port 4567 --frigate-url http://192.168.1.50:5000
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Defaults
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
STRIX_PORT="4567"
|
||||||
|
STRIX_FRIGATE_URL=""
|
||||||
|
STRIX_GO2RTC_URL=""
|
||||||
|
STRIX_LOG_LEVEL=""
|
||||||
|
STRIX_TAG="latest"
|
||||||
|
STRIX_DIR="/opt/strix"
|
||||||
|
COMPOSE_URL="https://raw.githubusercontent.com/eduard256/Strix/main/docker-compose.yml"
|
||||||
|
IMAGE="eduard256/strix"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Parse CLI arguments
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--port) STRIX_PORT="$2"; shift 2 ;;
|
||||||
|
--frigate-url) STRIX_FRIGATE_URL="$2"; shift 2 ;;
|
||||||
|
--go2rtc-url) STRIX_GO2RTC_URL="$2"; shift 2 ;;
|
||||||
|
--log-level) STRIX_LOG_LEVEL="$2"; shift 2 ;;
|
||||||
|
--tag) STRIX_TAG="$2"; shift 2 ;;
|
||||||
|
--dir) STRIX_DIR="$2"; shift 2 ;;
|
||||||
|
*) shift ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# JSON helpers (same protocol as prepare.sh)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
emit() {
|
||||||
|
local type="$1"
|
||||||
|
local msg="$2"
|
||||||
|
local data="${3:-}"
|
||||||
|
|
||||||
|
msg="${msg//\\/\\\\}"
|
||||||
|
msg="${msg//\"/\\\"}"
|
||||||
|
|
||||||
|
if [[ -n "$data" ]]; then
|
||||||
|
printf '{"type":"%s","msg":"%s","data":%s}\n' "$type" "$msg" "$data"
|
||||||
|
else
|
||||||
|
printf '{"type":"%s","msg":"%s"}\n' "$type" "$msg"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_done() {
|
||||||
|
local ok="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
if [[ "$ok" == "true" ]]; then
|
||||||
|
# Remaining args are key:value pairs for data
|
||||||
|
local data="{"
|
||||||
|
local first=true
|
||||||
|
while [[ $# -ge 2 ]]; do
|
||||||
|
local key="$1" val="$2"; shift 2
|
||||||
|
val="${val//\\/\\\\}"
|
||||||
|
val="${val//\"/\\\"}"
|
||||||
|
[[ "$first" == true ]] && first=false || data="${data},"
|
||||||
|
data="${data}\"${key}\":\"${val}\""
|
||||||
|
done
|
||||||
|
data="${data}}"
|
||||||
|
printf '{"type":"done","ok":true,"data":%s}\n' "$data"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
local error="${1:-unknown}"
|
||||||
|
error="${error//\\/\\\\}"
|
||||||
|
error="${error//\"/\\\"}"
|
||||||
|
printf '{"type":"done","ok":false,"error":"%s"}\n' "$error"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Detect LAN IP
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
detect_lan_ip() {
|
||||||
|
local ip=""
|
||||||
|
ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' | head -1)
|
||||||
|
[[ -z "$ip" ]] && ip=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||||
|
[[ -z "$ip" ]] && ip="localhost"
|
||||||
|
echo "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Working directory
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
setup_dir() {
|
||||||
|
emit "check" "Checking working directory ${STRIX_DIR}"
|
||||||
|
|
||||||
|
if [[ -d "$STRIX_DIR" ]]; then
|
||||||
|
emit "ok" "Directory exists: ${STRIX_DIR}"
|
||||||
|
else
|
||||||
|
emit "install" "Creating directory ${STRIX_DIR}"
|
||||||
|
if mkdir -p "$STRIX_DIR" 2>/dev/null; then
|
||||||
|
emit "ok" "Directory created: ${STRIX_DIR}"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to create directory ${STRIX_DIR}"
|
||||||
|
emit_done "false" "Cannot create ${STRIX_DIR}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Download docker-compose.yml
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
download_compose() {
|
||||||
|
emit "check" "Checking docker-compose.yml"
|
||||||
|
|
||||||
|
if [[ -f "${STRIX_DIR}/docker-compose.yml" ]]; then
|
||||||
|
emit "ok" "docker-compose.yml already exists"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "install" "Downloading docker-compose.yml from GitHub"
|
||||||
|
|
||||||
|
if curl -fsSL "$COMPOSE_URL" -o "${STRIX_DIR}/docker-compose.yml" 2>/dev/null; then
|
||||||
|
emit "ok" "docker-compose.yml downloaded"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to download docker-compose.yml"
|
||||||
|
emit_done "false" "docker-compose.yml download failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Generate .env
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
generate_env() {
|
||||||
|
emit "check" "Generating .env configuration"
|
||||||
|
|
||||||
|
cat > "${STRIX_DIR}/.env" <<EOF
|
||||||
|
# Strix configuration -- generated by strix.sh
|
||||||
|
STRIX_LISTEN=:${STRIX_PORT}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [[ -n "$STRIX_FRIGATE_URL" ]]; then
|
||||||
|
echo "STRIX_FRIGATE_URL=${STRIX_FRIGATE_URL}" >> "${STRIX_DIR}/.env"
|
||||||
|
emit "ok" "Frigate URL: ${STRIX_FRIGATE_URL}" "{\"frigate_url\":\"${STRIX_FRIGATE_URL}\"}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$STRIX_GO2RTC_URL" ]]; then
|
||||||
|
echo "STRIX_GO2RTC_URL=${STRIX_GO2RTC_URL}" >> "${STRIX_DIR}/.env"
|
||||||
|
emit "ok" "go2rtc URL: ${STRIX_GO2RTC_URL}" "{\"go2rtc_url\":\"${STRIX_GO2RTC_URL}\"}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$STRIX_LOG_LEVEL" ]]; then
|
||||||
|
echo "STRIX_LOG_LEVEL=${STRIX_LOG_LEVEL}" >> "${STRIX_DIR}/.env"
|
||||||
|
emit "ok" "Log level: ${STRIX_LOG_LEVEL}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
emit "ok" ".env generated (port ${STRIX_PORT})" "{\"port\":\"${STRIX_PORT}\"}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pull image
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
pull_image() {
|
||||||
|
emit "check" "Pulling image ${IMAGE}:${STRIX_TAG}"
|
||||||
|
|
||||||
|
if docker compose -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" pull &>/dev/null; then
|
||||||
|
emit "ok" "Image pulled: ${IMAGE}:${STRIX_TAG}"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to pull image ${IMAGE}:${STRIX_TAG}"
|
||||||
|
emit_done "false" "Image pull failed"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Start container
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
start_container() {
|
||||||
|
# Check if strix container is already running
|
||||||
|
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^strix$'; then
|
||||||
|
emit "check" "Strix container is running, recreating"
|
||||||
|
if docker compose -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" up -d --force-recreate &>/dev/null; then
|
||||||
|
emit "ok" "Container recreated"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to recreate container"
|
||||||
|
emit_done "false" "Container recreate failed"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
emit "install" "Starting Strix container"
|
||||||
|
if docker compose -f "${STRIX_DIR}/docker-compose.yml" --env-file "${STRIX_DIR}/.env" up -d &>/dev/null; then
|
||||||
|
emit "ok" "Container started"
|
||||||
|
else
|
||||||
|
emit "error" "Failed to start container"
|
||||||
|
emit_done "false" "Container start failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Healthcheck
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
healthcheck() {
|
||||||
|
emit "check" "Waiting for Strix to respond"
|
||||||
|
|
||||||
|
local retries=15
|
||||||
|
local i
|
||||||
|
for (( i = 1; i <= retries; i++ )); do
|
||||||
|
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:${STRIX_PORT}/api/health" &>/dev/null; then
|
||||||
|
local version
|
||||||
|
version=$(curl -sf --max-time 3 "http://localhost:${STRIX_PORT}/api" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")
|
||||||
|
emit "ok" "Strix v${version} is running on port ${STRIX_PORT}" "{\"version\":\"${version}\",\"port\":\"${STRIX_PORT}\"}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
emit "error" "Healthcheck failed after ${retries} attempts"
|
||||||
|
emit_done "false" "Healthcheck failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
main() {
|
||||||
|
# 1. Working directory
|
||||||
|
setup_dir
|
||||||
|
|
||||||
|
# 2. Download compose file (if not present)
|
||||||
|
download_compose
|
||||||
|
|
||||||
|
# 3. Generate .env from parameters
|
||||||
|
generate_env
|
||||||
|
|
||||||
|
# 4. Pull image
|
||||||
|
pull_image
|
||||||
|
|
||||||
|
# 5. Start / recreate container
|
||||||
|
start_container
|
||||||
|
|
||||||
|
# 6. Healthcheck
|
||||||
|
healthcheck
|
||||||
|
|
||||||
|
# 7. Done -- include URL for navigator
|
||||||
|
local lan_ip
|
||||||
|
lan_ip=$(detect_lan_ip)
|
||||||
|
local url="http://${lan_ip}:${STRIX_PORT}"
|
||||||
|
|
||||||
|
emit_done "true" "url" "$url" "version" "$(curl -sf --max-time 3 "http://localhost:${STRIX_PORT}/api" 2>/dev/null | grep -oP '"version"\s*:\s*"\K[^"]+' || echo "unknown")" "port" "$STRIX_PORT" "ip" "$lan_ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
Reference in New Issue
Block a user