Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 671da40930 | |||
| e0158ea81a | |||
| 593f2fc14f | |||
| 0a41dac8e4 | |||
| 1f8414079a | |||
| 43a097586a | |||
| fb4797f6b3 | |||
| 4308039e08 | |||
| df32774b50 | |||
| 100149241c | |||
| f6c80a2c72 | |||
| c86f1b936e |
@@ -41,12 +41,14 @@ Camera stream discovery and Frigate config generator.
|
||||
|
||||
## Install
|
||||
|
||||
Any Linux, one command:
|
||||
Any Linux or Proxmox, one command:
|
||||
|
||||
```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`
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
1. **Settings** > **Add-ons** > **Add-on Store**
|
||||
2. Menu (top right) > **Repositories** > add `https://github.com/eduard256/hassio-strix`
|
||||
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
|
||||
|
||||
Download from [GitHub Releases](https://github.com/eduard256/Strix/releases). No dependencies except `ffmpeg` for screenshot conversion.
|
||||
|
||||
+405
-799
File diff suppressed because it is too large
Load Diff
Executable
+159
@@ -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
|
||||
Executable
+325
@@ -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
|
||||
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
+444
@@ -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
|
||||
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