12 Commits

Author SHA1 Message Date
eduard256 671da40930 Add Umbrel install section to README 2026-04-24 11:26:13 +00:00
eduard256 e0158ea81a Add Podman section to README with required capabilities 2026-04-24 11:26:13 +00:00
eduard256 593f2fc14f Sync README install command 2026-04-16 19:00:45 +00:00
eduard256 0a41dac8e4 Sync install.sh fix 2026-04-16 18:58:46 +00:00
eduard256 1f8414079a Replace install.sh with new modular navigator 2026-04-16 18:57:35 +00:00
eduard256 43a097586a Sync scripts from develop 2026-04-16 18:54:08 +00:00
eduard256 fb4797f6b3 Sync scripts from develop 2026-04-16 18:47:55 +00:00
eduard256 4308039e08 Sync scripts from develop 2026-04-16 18:00:56 +00:00
eduard256 df32774b50 Remove test script 2026-04-16 17:51:33 +00:00
eduard256 100149241c Sync all scripts to main 2026-04-16 17:51:19 +00:00
eduard256 f6c80a2c72 Update install.sh with system detection display 2026-04-16 17:06:23 +00:00
eduard256 c86f1b936e Add modular installer scripts
Worker scripts with JSON event streaming:
- detect.sh: system/Docker/Frigate/go2rtc detection
- prepare.sh: Docker and Compose installation
- strix.sh: deploy Strix standalone
- strix-frigate.sh: deploy Strix + Frigate with HW autodetect
- proxmox-lxc-create.sh: create Ubuntu LXC on Proxmox
- install.sh: animated frontend with owl display
2026-04-16 16:56:17 +00:00
9 changed files with 3069 additions and 801 deletions
+28 -2
View File
@@ -41,12 +41,14 @@ Camera stream discovery and Frigate config generator.
## Install ## Install
Any Linux, one command: Any Linux or Proxmox, one command:
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/eduard256/Strix/main/install.sh | sudo bash bash <(curl -fsSL https://raw.githubusercontent.com/eduard256/Strix/main/install.sh)
``` ```
Run as root (or with `sudo`). Interactive installer detects your system (Linux / Proxmox) and guides you through setup.
Open `http://YOUR_IP:4567` Open `http://YOUR_IP:4567`
## How it works ## How it works
@@ -120,12 +122,36 @@ curl -O https://raw.githubusercontent.com/eduard256/Strix/main/docker-compose.go
docker compose -f docker-compose.go2rtc.yml up -d docker compose -f docker-compose.go2rtc.yml up -d
``` ```
### Podman
Podman drops `NET_RAW` and `NET_ADMIN` by default, which Strix needs for network scanning. Add them explicitly:
```bash
podman run -d \
--name strix \
--network host \
--cap-add=NET_RAW \
--cap-add=NET_ADMIN \
--restart unless-stopped \
eduard256/strix:latest
```
Or run with `--privileged` if you prefer.
### Home Assistant Add-on ### Home Assistant Add-on
1. **Settings** > **Add-ons** > **Add-on Store** 1. **Settings** > **Add-ons** > **Add-on Store**
2. Menu (top right) > **Repositories** > add `https://github.com/eduard256/hassio-strix` 2. Menu (top right) > **Repositories** > add `https://github.com/eduard256/hassio-strix`
3. Install **Strix**, enable **Start on boot** and **Show in sidebar** 3. Install **Strix**, enable **Start on boot** and **Show in sidebar**
### Umbrel
<a href="https://apps.umbrel.com/app/strix">
<img src="https://apps.umbrel.com/api/app/strix/badge-light.svg" alt="Install on Umbrel" height="60">
</a>
Install in one click from the [Umbrel App Store](https://apps.umbrel.com/app/strix).
### Binary ### Binary
Download from [GitHub Releases](https://github.com/eduard256/Strix/releases). No dependencies except `ffmpeg` for screenshot conversion. Download from [GitHub Releases](https://github.com/eduard256/Strix/releases). No dependencies except `ffmpeg` for screenshot conversion.
+405 -799
View File
File diff suppressed because it is too large Load Diff
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bash
# =============================================================================
# Strix -- detect.sh (worker)
#
# Detects system environment: OS type, Docker, Compose, Frigate, go2rtc.
# Fast, silent, returns JSON events to stdout.
#
# Protocol:
# - Every action is reported as a single-line JSON to stdout.
# - Types: check, ok, miss, error, done
# - Exit code: 0 always (detection never "fails", it just reports what it finds)
#
# Usage:
# bash scripts/detect.sh
# =============================================================================
set -uo pipefail
# ---------------------------------------------------------------------------
# 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
}
# ---------------------------------------------------------------------------
# 1. System type
# ---------------------------------------------------------------------------
detect_system() {
emit "check" "Detecting system"
if command -v pveversion &>/dev/null; then
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}" "{\"type\":\"proxmox\",\"pve_version\":\"${pve_ver}\"}"
elif [[ "$(uname -s 2>/dev/null)" == "Darwin" ]]; then
local mac_ver
mac_ver=$(sw_vers -productVersion 2>/dev/null || echo "unknown")
local arch
arch=$(uname -m 2>/dev/null || echo "unknown")
emit "ok" "macOS ${mac_ver} (${arch})" "{\"type\":\"macos\",\"version\":\"${mac_ver}\",\"arch\":\"${arch}\"}"
else
local os_name="Linux"
local os_id="unknown"
local os_ver="unknown"
local arch
arch=$(uname -m 2>/dev/null || echo "unknown")
if [[ -f /etc/os-release ]]; then
. /etc/os-release
os_name="${PRETTY_NAME:-Linux}"
os_id="${ID:-unknown}"
os_ver="${VERSION_ID:-unknown}"
fi
emit "ok" "${os_name} (${arch})" "{\"type\":\"linux\",\"id\":\"${os_id}\",\"version\":\"${os_ver}\",\"arch\":\"${arch}\"}"
fi
}
# ---------------------------------------------------------------------------
# 2. Docker
# ---------------------------------------------------------------------------
detect_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}\"}"
else
emit "miss" "Docker not installed"
fi
}
# ---------------------------------------------------------------------------
# 3. Docker Compose
# ---------------------------------------------------------------------------
detect_compose() {
emit "check" "Checking Docker Compose"
if docker compose version &>/dev/null 2>&1; then
local ver
ver=$(docker compose version --short 2>/dev/null || echo "unknown")
emit "ok" "Compose ${ver}" "{\"version\":\"${ver}\",\"type\":\"plugin\"}"
elif 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")
emit "ok" "Compose ${ver}" "{\"version\":\"${ver}\",\"type\":\"standalone\"}"
else
emit "miss" "Docker Compose not installed"
fi
}
# ---------------------------------------------------------------------------
# 4. Frigate
# ---------------------------------------------------------------------------
detect_frigate() {
emit "check" "Checking Frigate"
if command -v curl &>/dev/null; then
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:5000/api/config" &>/dev/null; then
emit "ok" "Frigate on port 5000" "{\"url\":\"http://localhost:5000\",\"port\":5000}"
return
fi
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:8971/api/config" &>/dev/null; then
emit "ok" "Frigate on port 8971" "{\"url\":\"http://localhost:8971\",\"port\":8971}"
return
fi
fi
emit "miss" "Frigate not found"
}
# ---------------------------------------------------------------------------
# 5. go2rtc
# ---------------------------------------------------------------------------
detect_go2rtc() {
emit "check" "Checking go2rtc"
if command -v curl &>/dev/null; then
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:1984/api" &>/dev/null; then
emit "ok" "go2rtc on port 1984" "{\"url\":\"http://localhost:1984\",\"port\":1984}"
return
fi
if curl -sf --connect-timeout 2 --max-time 3 "http://localhost:11984/api" &>/dev/null; then
emit "ok" "go2rtc on port 11984" "{\"url\":\"http://localhost:11984\",\"port\":11984}"
return
fi
fi
emit "miss" "go2rtc not found"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
detect_system
detect_docker
detect_compose
detect_frigate
detect_go2rtc
printf '{"type":"done","ok":true}\n'
}
main
+325
View File
@@ -0,0 +1,325 @@
#!/usr/bin/env bash
# =============================================================================
# Strix -- linux.sh (navigator for plain Linux / macOS)
# =============================================================================
set -o pipefail
BACKTITLE="Strix Installer | Linux mode"
WT_H=16
WT_W=60
command -v whiptail &>/dev/null || { echo "whiptail required (install: apt install whiptail | dnf install newt)"; exit 1; }
# Dark theme for whiptail
export NEWT_COLORS='
root=,black
window=,black
border=white,black
textbox=white,black
button=black,white
actbutton=white,magenta
compactbutton=white,black
listbox=white,black
actlistbox=white,magenta
title=magenta,black
roottext=white,black
emptyscale=,black
fullscale=,magenta
helpline=white,black
'
# Parameters
INSTALL_MODE=""
FRIGATE_URL=""
GO2RTC_URL=""
STRIX_PORT="4567"
LOG_LEVEL=""
STRIX_TAG="latest"
# ---------------------------------------------------------------------------
# Simple flow
# ---------------------------------------------------------------------------
simple_flow() {
local step=1
while true; do
case $step in
1) # Mode
INSTALL_MODE=$(whiptail --backtitle "$BACKTITLE" --title " Install Mode " \
--menu "" $WT_H $WT_W 3 \
"1" "Strix only" \
"2" "Strix + Frigate" \
"3" "Advanced setup" \
3>&1 1>&2 2>&3) || { clear; exit 0; }
case "$INSTALL_MODE" in
1) INSTALL_MODE="strix"; step=2 ;;
2) INSTALL_MODE="strix-frigate"; step=3 ;;
3) advanced_flow; return ;;
esac
;;
2) # Frigate URL (strix only)
FRIGATE_URL=$(whiptail --backtitle "$BACKTITLE" --title " Frigate " \
--inputbox "Frigate URL (empty to skip):\n\nExample: http://192.168.1.100:5000" \
$WT_H $WT_W "${FRIGATE_URL:-http://}" \
3>&1 1>&2 2>&3) || { step=1; continue; }
[[ "$FRIGATE_URL" == "http://" || "$FRIGATE_URL" == "https://" ]] && FRIGATE_URL=""
step=3
;;
3) # Confirm
local s="Mode: ${INSTALL_MODE}\nPort: ${STRIX_PORT}\n"
[[ -n "$FRIGATE_URL" ]] && s+="Frigate: ${FRIGATE_URL}\n"
whiptail --backtitle "$BACKTITLE" --title " Confirm " \
--yesno "$s" $WT_H $WT_W || { step=1; continue; }
break
;;
esac
done
}
# ---------------------------------------------------------------------------
# Advanced flow
# ---------------------------------------------------------------------------
advanced_flow() {
local step=1
INSTALL_MODE="${INSTALL_MODE:-strix}"
while true; do
case $step in
1) # Mode
local choice
choice=$(whiptail --backtitle "$BACKTITLE" --title " Mode " \
--menu "" $WT_H $WT_W 2 \
"strix" "Strix only" \
"strix-frigate" "Strix + Frigate" \
--default-item "$INSTALL_MODE" \
3>&1 1>&2 2>&3) || { clear; exit 0; }
INSTALL_MODE="$choice"; step=2 ;;
2) # Port
local val
val=$(whiptail --backtitle "$BACKTITLE" --title " Port " \
--inputbox "Strix port:" 9 $WT_W "$STRIX_PORT" \
3>&1 1>&2 2>&3) || { step=1; continue; }
STRIX_PORT="${val:-4567}"; step=3 ;;
3) # Frigate
val=$(whiptail --backtitle "$BACKTITLE" --title " Frigate " \
--inputbox "Frigate URL (empty to skip):" 9 $WT_W "${FRIGATE_URL:-http://}" \
3>&1 1>&2 2>&3) || { step=1; continue; }
[[ "$val" == "http://" || "$val" == "https://" ]] && val=""
FRIGATE_URL="$val"; step=4 ;;
4) # go2rtc
val=$(whiptail --backtitle "$BACKTITLE" --title " go2rtc " \
--inputbox "go2rtc URL (empty to skip):" 9 $WT_W "${GO2RTC_URL:-http://}" \
3>&1 1>&2 2>&3) || { step=1; continue; }
[[ "$val" == "http://" || "$val" == "https://" ]] && val=""
GO2RTC_URL="$val"; step=5 ;;
5) # Log level
val=$(whiptail --backtitle "$BACKTITLE" --title " Log Level " \
--menu "" 14 $WT_W 5 \
"" "default (info)" \
"debug" "debug" \
"info" "info" \
"warn" "warn" \
"error" "error" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LOG_LEVEL="$val"; step=6 ;;
6) # Tag
val=$(whiptail --backtitle "$BACKTITLE" --title " Image Tag " \
--inputbox "Strix image tag:" 9 $WT_W "$STRIX_TAG" \
3>&1 1>&2 2>&3) || { step=1; continue; }
STRIX_TAG="${val:-latest}"; step=7 ;;
7) # Confirm
local s="Mode: ${INSTALL_MODE}\nPort: ${STRIX_PORT}\nTag: ${STRIX_TAG}\n"
[[ -n "$FRIGATE_URL" ]] && s+="Frigate: ${FRIGATE_URL}\n"
[[ -n "$GO2RTC_URL" ]] && s+="go2rtc: ${GO2RTC_URL}\n"
[[ -n "$LOG_LEVEL" ]] && s+="Log: ${LOG_LEVEL}\n"
whiptail --backtitle "$BACKTITLE" --title " Confirm " \
--yesno "$s" $WT_H $WT_W || { step=1; continue; }
break
;;
esac
done
}
# ---------------------------------------------------------------------------
# Colors
# ---------------------------------------------------------------------------
C_RESET="\033[0m"
C_BOLD="\033[1m"
C_DIM="\033[2m"
C_GREEN="\033[32m"
C_RED="\033[31m"
C_YELLOW="\033[33m"
C_CYAN="\033[36m"
C_WHITE="\033[97m"
C_MAGENTA="\033[35m"
# ---------------------------------------------------------------------------
# Worker runner: streams JSON events as status lines
# ---------------------------------------------------------------------------
SCRIPTS_BASE="https://raw.githubusercontent.com/eduard256/Strix/main/scripts"
download_worker() {
local name="$1"
local dest="/tmp/strix-${name}"
curl -fsSL "${SCRIPTS_BASE}/${name}" -o "$dest" 2>/dev/null
echo "$dest"
}
print_events() {
while IFS= read -r line; do
type=""; msg=""
type=$(echo "$line" | grep -oP '"type"\s*:\s*"\K[^"]+' | head -1)
msg=$(echo "$line" | grep -oP '"msg"\s*:\s*"\K[^"]+' | head -1)
case "$type" in
check) echo -e " ${C_CYAN}[..]${C_RESET} ${msg}" ;;
ok) echo -e " ${C_GREEN}[OK]${C_RESET} ${msg}" ;;
miss) echo -e " ${C_YELLOW}[--]${C_RESET} ${msg}" ;;
install) echo -e " ${C_CYAN}[>>]${C_RESET} ${msg}" ;;
error) echo -e " ${C_RED}[XX]${C_RESET} ${msg}" ;;
esac
done
}
json_field() {
echo "$1" | grep -oP "\"$2\"\s*:\s*\"\K[^\"]*" | head -1
}
# ---------------------------------------------------------------------------
# LAN IP detection
# ---------------------------------------------------------------------------
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=$(ifconfig 2>/dev/null | grep -oP 'inet \K[0-9.]+' | grep -v '127.0.0.1' | head -1)
[[ -z "$ip" ]] && ip="localhost"
echo "$ip"
}
# ---------------------------------------------------------------------------
# Final URLs
# ---------------------------------------------------------------------------
show_urls() {
local ip="$1"
local port="$2"
local mode="$3"
echo ""
echo -e " ${C_GREEN}${C_BOLD}====================================${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD} Installation Complete${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD}====================================${C_RESET}"
echo ""
echo -e " ${C_WHITE}${C_BOLD}Strix:${C_RESET} ${C_CYAN}http://${ip}:${port}${C_RESET}"
if [[ "$mode" == "strix-frigate" ]]; then
echo -e " ${C_WHITE}${C_BOLD}Frigate:${C_RESET} ${C_CYAN}http://${ip}:8971${C_RESET}"
echo -e " ${C_WHITE}${C_BOLD}Frigate API:${C_RESET} ${C_CYAN}http://${ip}:5000${C_RESET}"
echo -e " ${C_WHITE}${C_BOLD}go2rtc:${C_RESET} ${C_CYAN}http://${ip}:1984${C_RESET}"
fi
echo ""
echo -e " ${C_DIM}Press Enter to exit${C_RESET}"
read -r
}
# ---------------------------------------------------------------------------
# Check root / docker -- bail early if not sudo and docker missing
# ---------------------------------------------------------------------------
check_sudo_required() {
if [[ "$(id -u)" -eq 0 ]]; then
return 0 # already root
fi
if command -v docker &>/dev/null; then
return 0 # docker present, maybe root not strictly needed
fi
clear
echo ""
echo -e " ${C_RED}${C_BOLD}Root privileges required${C_RESET}"
echo ""
echo -e " Docker is not installed. Installing it needs root."
echo -e " Please re-run the installer with ${C_BOLD}sudo${C_RESET}:"
echo ""
echo -e " ${C_CYAN}${C_BOLD}curl -fsSL https://raw.githubusercontent.com/eduard256/Strix/main/scripts/install.sh | sudo bash${C_RESET}"
echo ""
exit 1
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
simple_flow
clear
echo ""
echo -e " ${C_MAGENTA}${C_BOLD}STRIX INSTALLER${C_RESET} ${C_DIM}(Linux)${C_RESET}"
echo -e " ${C_DIM}Mode: ${INSTALL_MODE} | Port: ${STRIX_PORT}${C_RESET}"
echo ""
check_sudo_required
# Step 1: Check Docker / install via prepare.sh
if ! command -v docker &>/dev/null || ! docker compose version &>/dev/null; then
echo -e " ${C_MAGENTA}${C_BOLD}--- Installing Docker ---${C_RESET}"
echo ""
prepare_script=$(download_worker "prepare.sh")
bash "$prepare_script" 2>/dev/null | print_events
rm -f "$prepare_script"
echo ""
fi
# Step 2: Deploy
if [[ "$INSTALL_MODE" == "strix-frigate" ]]; then
echo -e " ${C_MAGENTA}${C_BOLD}--- Deploying Strix + Frigate ---${C_RESET}"
echo ""
deploy_script=$(download_worker "strix-frigate.sh")
deploy_args="--port $STRIX_PORT --tag $STRIX_TAG"
[[ -n "$GO2RTC_URL" ]] && deploy_args="$deploy_args --go2rtc-url $GO2RTC_URL"
[[ -n "$LOG_LEVEL" ]] && deploy_args="$deploy_args --log-level $LOG_LEVEL"
deploy_output=$(bash "$deploy_script" $deploy_args 2>/dev/null)
deploy_done=$(echo "$deploy_output" | grep '"type":"done"')
echo "$deploy_output" | print_events
rm -f "$deploy_script"
else
echo -e " ${C_MAGENTA}${C_BOLD}--- Deploying Strix ---${C_RESET}"
echo ""
deploy_script=$(download_worker "strix.sh")
deploy_args="--port $STRIX_PORT --tag $STRIX_TAG"
[[ -n "$FRIGATE_URL" ]] && deploy_args="$deploy_args --frigate-url $FRIGATE_URL"
[[ -n "$GO2RTC_URL" ]] && deploy_args="$deploy_args --go2rtc-url $GO2RTC_URL"
[[ -n "$LOG_LEVEL" ]] && deploy_args="$deploy_args --log-level $LOG_LEVEL"
deploy_output=$(bash "$deploy_script" $deploy_args 2>/dev/null)
deploy_done=$(echo "$deploy_output" | grep '"type":"done"')
echo "$deploy_output" | print_events
rm -f "$deploy_script"
fi
# Final URLs
deploy_ok=$(echo "$deploy_done" | grep -oP '"ok"\s*:\s*\K[a-z]+' | head -1)
if [[ "$deploy_ok" == "true" ]]; then
lan_ip=$(detect_lan_ip)
show_urls "$lan_ip" "$STRIX_PORT" "$INSTALL_MODE"
else
echo ""
echo -e " ${C_RED}${C_BOLD}Deployment failed.${C_RESET}"
echo ""
fi
+400
View File
@@ -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
+606
View File
@@ -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
+444
View File
@@ -0,0 +1,444 @@
#!/usr/bin/env bash
# =============================================================================
# Strix -- proxmox.sh (navigator for Proxmox)
# =============================================================================
set -o pipefail
BACKTITLE="Strix Installer | Proxmox mode"
WT_H=16
WT_W=60
command -v whiptail &>/dev/null || { echo "whiptail required"; exit 1; }
# Dark theme for whiptail
export NEWT_COLORS='
root=,black
window=,black
border=white,black
textbox=white,black
button=black,white
actbutton=white,magenta
compactbutton=white,black
listbox=white,black
actlistbox=white,magenta
title=magenta,black
roottext=white,black
emptyscale=,black
fullscale=,magenta
helpline=white,black
'
# Parameters
INSTALL_MODE=""
FRIGATE_URL=""
GO2RTC_URL=""
STRIX_PORT="4567"
LXC_HOSTNAME="strix"
LXC_MEMORY="2048"
LXC_CORES="2"
LXC_DISK="32"
LXC_SWAP="512"
LXC_IP="dhcp"
LXC_GATEWAY=""
LXC_BRIDGE=""
LXC_STORAGE=""
# ---------------------------------------------------------------------------
# Simple flow
# ---------------------------------------------------------------------------
simple_flow() {
local step=1
while true; do
case $step in
1) # Mode
INSTALL_MODE=$(whiptail --backtitle "$BACKTITLE" --title " Install Mode " \
--menu "" $WT_H $WT_W 3 \
"1" "Strix only" \
"2" "Strix + Frigate" \
"3" "Advanced setup" \
3>&1 1>&2 2>&3) || { clear; exit 0; }
case "$INSTALL_MODE" in
1) INSTALL_MODE="strix"; step=2 ;;
2) INSTALL_MODE="strix-frigate"; step=3 ;;
3) advanced_flow; return ;;
esac
;;
2) # Frigate URL (strix only)
FRIGATE_URL=$(whiptail --backtitle "$BACKTITLE" --title " Frigate " \
--inputbox "Frigate URL (empty to skip):\n\nExample: http://192.168.1.100:5000" \
$WT_H $WT_W "${FRIGATE_URL:-http://}" \
3>&1 1>&2 2>&3) || { step=1; continue; }
[[ "$FRIGATE_URL" == "http://" || "$FRIGATE_URL" == "https://" ]] && FRIGATE_URL=""
step=3
;;
3) # Confirm
local s="Mode: ${INSTALL_MODE}\nPort: ${STRIX_PORT}\n"
[[ -n "$FRIGATE_URL" ]] && s+="Frigate: ${FRIGATE_URL}\n"
s+="\nLXC: auto (${LXC_HOSTNAME}, ${LXC_MEMORY}MB, ${LXC_CORES}cpu, ${LXC_DISK}GB)"
whiptail --backtitle "$BACKTITLE" --title " Confirm " \
--yesno "$s" $WT_H $WT_W || { step=1; continue; }
break
;;
esac
done
}
# ---------------------------------------------------------------------------
# Advanced flow
# ---------------------------------------------------------------------------
advanced_flow() {
local step=1
INSTALL_MODE="${INSTALL_MODE:-strix}"
while true; do
case $step in
1) # Mode
local choice
choice=$(whiptail --backtitle "$BACKTITLE" --title " Mode " \
--menu "" $WT_H $WT_W 2 \
"strix" "Strix only" \
"strix-frigate" "Strix + Frigate" \
--default-item "$INSTALL_MODE" \
3>&1 1>&2 2>&3) || { clear; exit 0; }
INSTALL_MODE="$choice"; step=2 ;;
2) # Port
local val
val=$(whiptail --backtitle "$BACKTITLE" --title " Port " \
--inputbox "Strix port:" 9 $WT_W "$STRIX_PORT" \
3>&1 1>&2 2>&3) || { step=1; continue; }
STRIX_PORT="${val:-4567}"; step=3 ;;
3) # Frigate
val=$(whiptail --backtitle "$BACKTITLE" --title " Frigate " \
--inputbox "Frigate URL (empty to skip):" 9 $WT_W "${FRIGATE_URL:-http://}" \
3>&1 1>&2 2>&3) || { step=1; continue; }
[[ "$val" == "http://" || "$val" == "https://" ]] && val=""
FRIGATE_URL="$val"; step=4 ;;
4) # go2rtc
val=$(whiptail --backtitle "$BACKTITLE" --title " go2rtc " \
--inputbox "go2rtc URL (empty to skip):" 9 $WT_W "${GO2RTC_URL:-http://}" \
3>&1 1>&2 2>&3) || { step=1; continue; }
[[ "$val" == "http://" || "$val" == "https://" ]] && val=""
GO2RTC_URL="$val"; step=5 ;;
5) # Hostname
val=$(whiptail --backtitle "$BACKTITLE" --title " LXC Hostname " \
--inputbox "Hostname:" 9 $WT_W "$LXC_HOSTNAME" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_HOSTNAME="${val:-strix}"; step=6 ;;
6) # RAM
val=$(whiptail --backtitle "$BACKTITLE" --title " LXC RAM " \
--inputbox "RAM (MB):" 9 $WT_W "$LXC_MEMORY" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_MEMORY="${val:-2048}"; step=7 ;;
7) # CPU
val=$(whiptail --backtitle "$BACKTITLE" --title " LXC CPU " \
--inputbox "CPU cores:" 9 $WT_W "$LXC_CORES" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_CORES="${val:-2}"; step=8 ;;
8) # Disk
val=$(whiptail --backtitle "$BACKTITLE" --title " LXC Disk " \
--inputbox "Disk (GB):" 9 $WT_W "$LXC_DISK" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_DISK="${val:-32}"; step=9 ;;
9) # Swap
val=$(whiptail --backtitle "$BACKTITLE" --title " LXC Swap " \
--inputbox "Swap (MB):" 9 $WT_W "$LXC_SWAP" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_SWAP="${val:-512}"; step=10 ;;
10) # IP
val=$(whiptail --backtitle "$BACKTITLE" --title " LXC Network " \
--inputbox "IP (dhcp or CIDR e.g. 10.0.20.110/24):" 9 $WT_W "$LXC_IP" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_IP="${val:-dhcp}"
[[ "$LXC_IP" != "dhcp" ]] && step=11 || step=12
;;
11) # Gateway
val=$(whiptail --backtitle "$BACKTITLE" --title " Gateway " \
--inputbox "Gateway:" 9 $WT_W "$LXC_GATEWAY" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_GATEWAY="$val"; step=12 ;;
12) # Bridge
val=$(whiptail --backtitle "$BACKTITLE" --title " Bridge " \
--inputbox "Network bridge (empty=auto):" 9 $WT_W "$LXC_BRIDGE" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_BRIDGE="$val"; step=13 ;;
13) # Storage
val=$(whiptail --backtitle "$BACKTITLE" --title " Storage " \
--inputbox "Storage (empty=auto):" 9 $WT_W "$LXC_STORAGE" \
3>&1 1>&2 2>&3) || { step=1; continue; }
LXC_STORAGE="$val"; step=14 ;;
14) # Confirm
local s="Mode: ${INSTALL_MODE}\nPort: ${STRIX_PORT}\n"
[[ -n "$FRIGATE_URL" ]] && s+="Frigate: ${FRIGATE_URL}\n"
[[ -n "$GO2RTC_URL" ]] && s+="go2rtc: ${GO2RTC_URL}\n"
s+="\nLXC:\n"
s+=" ${LXC_HOSTNAME} | ${LXC_MEMORY}MB | ${LXC_CORES}cpu | ${LXC_DISK}GB\n"
s+=" IP: ${LXC_IP}"
[[ -n "$LXC_GATEWAY" ]] && s+=" gw ${LXC_GATEWAY}"
s+="\n"
[[ -n "$LXC_BRIDGE" ]] && s+=" Bridge: ${LXC_BRIDGE}\n" || s+=" Bridge: auto\n"
[[ -n "$LXC_STORAGE" ]] && s+=" Storage: ${LXC_STORAGE}\n" || s+=" Storage: auto\n"
whiptail --backtitle "$BACKTITLE" --title " Confirm " \
--yesno "$s" 18 $WT_W || { step=1; continue; }
break
;;
esac
done
}
# ---------------------------------------------------------------------------
# Colors
# ---------------------------------------------------------------------------
C_RESET="\033[0m"
C_BOLD="\033[1m"
C_DIM="\033[2m"
C_GREEN="\033[32m"
C_RED="\033[31m"
C_YELLOW="\033[33m"
C_CYAN="\033[36m"
C_WHITE="\033[97m"
C_MAGENTA="\033[35m"
# ---------------------------------------------------------------------------
# Worker runner: streams JSON events and prints status lines
# ---------------------------------------------------------------------------
SCRIPTS_BASE="https://raw.githubusercontent.com/eduard256/Strix/main/scripts"
# Download a worker script to /tmp
download_worker() {
local name="$1"
local dest="/tmp/strix-${name}"
curl -fsSL "${SCRIPTS_BASE}/${name}" -o "$dest" 2>/dev/null
echo "$dest"
}
# Run a worker and display its JSON events as status lines
run_worker() {
local script="$1"
shift
local label="$1"
shift
echo ""
echo -e " ${C_MAGENTA}${C_BOLD}--- ${label} ---${C_RESET}"
echo ""
bash "$script" "$@" 2>/dev/null | while IFS= read -r line; do
type=""; msg=""
type=$(echo "$line" | grep -oP '"type"\s*:\s*"\K[^"]+' | head -1)
msg=$(echo "$line" | grep -oP '"msg"\s*:\s*"\K[^"]+' | head -1)
case "$type" in
check) echo -e " ${C_CYAN}[..]${C_RESET} ${msg}" ;;
ok) echo -e " ${C_GREEN}[OK]${C_RESET} ${msg}" ;;
miss) echo -e " ${C_YELLOW}[--]${C_RESET} ${msg}" ;;
install) echo -e " ${C_CYAN}[>>]${C_RESET} ${msg}" ;;
error) echo -e " ${C_RED}[XX]${C_RESET} ${msg}" ;;
done) ;; # handled after loop
esac
done
echo ""
}
# Extract a field from JSON done line
json_field() {
echo "$1" | grep -oP "\"$2\"\s*:\s*\"\K[^\"]*" | head -1
}
# ---------------------------------------------------------------------------
# Show final URLs
# ---------------------------------------------------------------------------
show_urls() {
local ip="$1"
local port="$2"
local mode="$3"
echo ""
echo -e " ${C_GREEN}${C_BOLD}====================================${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD} Installation Complete${C_RESET}"
echo -e " ${C_GREEN}${C_BOLD}====================================${C_RESET}"
echo ""
echo -e " ${C_WHITE}${C_BOLD}Strix:${C_RESET} ${C_CYAN}http://${ip}:${port}${C_RESET}"
if [[ "$mode" == "strix-frigate" ]]; then
echo -e " ${C_WHITE}${C_BOLD}Frigate:${C_RESET} ${C_CYAN}http://${ip}:8971${C_RESET}"
echo -e " ${C_WHITE}${C_BOLD}Frigate API:${C_RESET} ${C_CYAN}http://${ip}:5000${C_RESET}"
echo -e " ${C_WHITE}${C_BOLD}go2rtc:${C_RESET} ${C_CYAN}http://${ip}:1984${C_RESET}"
fi
echo ""
echo -e " ${C_DIM}Press Enter to exit${C_RESET}"
read -r
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
simple_flow
clear
echo ""
echo -e " ${C_MAGENTA}${C_BOLD}STRIX INSTALLER${C_RESET}"
echo -e " ${C_DIM}Mode: ${INSTALL_MODE} | Port: ${STRIX_PORT}${C_RESET}"
echo ""
# Step 1: Create LXC container
echo -e " ${C_MAGENTA}${C_BOLD}--- Creating LXC Container ---${C_RESET}"
echo ""
lxc_script=$(download_worker "proxmox-lxc-create.sh")
lxc_args=""
[[ -n "$LXC_HOSTNAME" ]] && lxc_args="$lxc_args --hostname $LXC_HOSTNAME"
[[ -n "$LXC_MEMORY" ]] && lxc_args="$lxc_args --memory $LXC_MEMORY"
[[ -n "$LXC_CORES" ]] && lxc_args="$lxc_args --cores $LXC_CORES"
[[ -n "$LXC_DISK" ]] && lxc_args="$lxc_args --disk $LXC_DISK"
[[ -n "$LXC_SWAP" ]] && lxc_args="$lxc_args --swap $LXC_SWAP"
[[ -n "$LXC_BRIDGE" ]] && lxc_args="$lxc_args --bridge $LXC_BRIDGE"
[[ -n "$LXC_STORAGE" ]] && lxc_args="$lxc_args --storage $LXC_STORAGE"
[[ "$LXC_IP" != "dhcp" && -n "$LXC_IP" ]] && lxc_args="$lxc_args --ip $LXC_IP"
[[ -n "$LXC_GATEWAY" ]] && lxc_args="$lxc_args --gateway $LXC_GATEWAY"
# Run LXC creation and capture full output
lxc_output=$(bash "$lxc_script" $lxc_args 2>/dev/null)
lxc_done=$(echo "$lxc_output" | grep '"type":"done"')
# Display LXC creation events
echo "$lxc_output" | while IFS= read -r line; do
type=""; msg=""
type=$(echo "$line" | grep -oP '"type"\s*:\s*"\K[^"]+' | head -1)
msg=$(echo "$line" | grep -oP '"msg"\s*:\s*"\K[^"]+' | head -1)
case "$type" in
check) echo -e " ${C_CYAN}[..]${C_RESET} ${msg}" ;;
ok) echo -e " ${C_GREEN}[OK]${C_RESET} ${msg}" ;;
miss) echo -e " ${C_YELLOW}[--]${C_RESET} ${msg}" ;;
install) echo -e " ${C_CYAN}[>>]${C_RESET} ${msg}" ;;
error) echo -e " ${C_RED}[XX]${C_RESET} ${msg}" ;;
esac
done
# Check if LXC creation succeeded
lxc_ok=$(echo "$lxc_done" | grep -oP '"ok"\s*:\s*\K[a-z]+' | head -1)
if [[ "$lxc_ok" != "true" ]]; then
echo ""
echo -e " ${C_RED}${C_BOLD}LXC creation failed. Aborting.${C_RESET}"
rm -f "$lxc_script"
exit 1
fi
# Extract LXC data
CT_ID=$(json_field "$lxc_done" "id")
CT_IP=$(json_field "$lxc_done" "ip")
CT_PASS=$(json_field "$lxc_done" "password")
echo ""
echo -e " ${C_GREEN}${C_BOLD}LXC ${CT_ID} ready${C_RESET} -- IP: ${C_WHITE}${CT_IP}${C_RESET}"
# Step 2: Run prepare.sh inside LXC (install Docker)
echo ""
echo -e " ${C_MAGENTA}${C_BOLD}--- Installing Docker ---${C_RESET}"
echo ""
prepare_script=$(download_worker "prepare.sh")
pct push "$CT_ID" "$prepare_script" /tmp/prepare.sh &>/dev/null
pct exec "$CT_ID" -- bash /tmp/prepare.sh 2>/dev/null | while IFS= read -r line; do
type=$(echo "$line" | grep -oP '"type"\s*:\s*"\K[^"]+' | head -1)
msg=$(echo "$line" | grep -oP '"msg"\s*:\s*"\K[^"]+' | head -1)
case "$type" in
check) echo -e " ${C_CYAN}[..]${C_RESET} ${msg}" ;;
ok) echo -e " ${C_GREEN}[OK]${C_RESET} ${msg}" ;;
miss) echo -e " ${C_YELLOW}[--]${C_RESET} ${msg}" ;;
install) echo -e " ${C_CYAN}[>>]${C_RESET} ${msg}" ;;
error) echo -e " ${C_RED}[XX]${C_RESET} ${msg}" ;;
esac
done
# Step 3: Deploy Strix (or Strix + Frigate)
if [[ "$INSTALL_MODE" == "strix-frigate" ]]; then
echo ""
echo -e " ${C_MAGENTA}${C_BOLD}--- Deploying Strix + Frigate ---${C_RESET}"
echo ""
deploy_script=$(download_worker "strix-frigate.sh")
pct push "$CT_ID" "$deploy_script" /tmp/deploy.sh &>/dev/null
deploy_args="--port $STRIX_PORT"
[[ -n "$GO2RTC_URL" ]] && deploy_args="$deploy_args --go2rtc-url $GO2RTC_URL"
deploy_output=$(pct exec "$CT_ID" -- bash /tmp/deploy.sh $deploy_args 2>/dev/null)
deploy_done=$(echo "$deploy_output" | grep '"type":"done"')
echo "$deploy_output" | while IFS= read -r line; do
type=$(echo "$line" | grep -oP '"type"\s*:\s*"\K[^"]+' | head -1)
msg=$(echo "$line" | grep -oP '"msg"\s*:\s*"\K[^"]+' | head -1)
case "$type" in
check) echo -e " ${C_CYAN}[..]${C_RESET} ${msg}" ;;
ok) echo -e " ${C_GREEN}[OK]${C_RESET} ${msg}" ;;
miss) echo -e " ${C_YELLOW}[--]${C_RESET} ${msg}" ;;
install) echo -e " ${C_CYAN}[>>]${C_RESET} ${msg}" ;;
error) echo -e " ${C_RED}[XX]${C_RESET} ${msg}" ;;
esac
done
else
echo ""
echo -e " ${C_MAGENTA}${C_BOLD}--- Deploying Strix ---${C_RESET}"
echo ""
deploy_script=$(download_worker "strix.sh")
pct push "$CT_ID" "$deploy_script" /tmp/deploy.sh &>/dev/null
deploy_args="--port $STRIX_PORT"
[[ -n "$FRIGATE_URL" ]] && deploy_args="$deploy_args --frigate-url $FRIGATE_URL"
[[ -n "$GO2RTC_URL" ]] && deploy_args="$deploy_args --go2rtc-url $GO2RTC_URL"
deploy_output=$(pct exec "$CT_ID" -- bash /tmp/deploy.sh $deploy_args 2>/dev/null)
deploy_done=$(echo "$deploy_output" | grep '"type":"done"')
echo "$deploy_output" | while IFS= read -r line; do
type=$(echo "$line" | grep -oP '"type"\s*:\s*"\K[^"]+' | head -1)
msg=$(echo "$line" | grep -oP '"msg"\s*:\s*"\K[^"]+' | head -1)
case "$type" in
check) echo -e " ${C_CYAN}[..]${C_RESET} ${msg}" ;;
ok) echo -e " ${C_GREEN}[OK]${C_RESET} ${msg}" ;;
miss) echo -e " ${C_YELLOW}[--]${C_RESET} ${msg}" ;;
install) echo -e " ${C_CYAN}[>>]${C_RESET} ${msg}" ;;
error) echo -e " ${C_RED}[XX]${C_RESET} ${msg}" ;;
esac
done
fi
# Show final URLs
deploy_ok=$(echo "$deploy_done" | grep -oP '"ok"\s*:\s*\K[a-z]+' | head -1)
if [[ "$deploy_ok" == "true" ]]; then
show_urls "$CT_IP" "$STRIX_PORT" "$INSTALL_MODE"
else
echo ""
echo -e " ${C_RED}${C_BOLD}Deployment failed.${C_RESET}"
echo -e " ${C_DIM}LXC ${CT_ID} (${CT_IP}) is still running. Check logs inside.${C_RESET}"
echo ""
fi
# Cleanup
rm -f "$lxc_script" "$prepare_script" "$deploy_script" 2>/dev/null
+428
View File
@@ -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
+274
View File
@@ -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