- Move nested functions outside parent functions (ash doesn't support local functions) - Fix _build_instance_entry and _print_instance_json syntax Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
891 lines
21 KiB
Bash
891 lines
21 KiB
Bash
#!/bin/sh
|
|
# SecuBox Streamlit Platform Controller
|
|
# Copyright (C) 2025 CyberMind.fr
|
|
#
|
|
# Manages Streamlit in LXC container
|
|
|
|
CONFIG="streamlit"
|
|
LXC_NAME="streamlit"
|
|
|
|
# Paths
|
|
LXC_PATH="/srv/lxc"
|
|
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
|
|
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
|
|
APPS_PATH="/srv/streamlit/apps"
|
|
DEFAULT_APP="/usr/share/streamlit/hello.py"
|
|
|
|
# Logging
|
|
log_info() { echo "[INFO] $*"; logger -t streamlit "$*"; }
|
|
log_error() { echo "[ERROR] $*" >&2; logger -t streamlit -p err "$*"; }
|
|
log_debug() { [ "$DEBUG" = "1" ] && echo "[DEBUG] $*"; }
|
|
|
|
# Helpers
|
|
require_root() {
|
|
[ "$(id -u)" -eq 0 ] || {
|
|
log_error "This command requires root privileges"
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
has_lxc() { command -v lxc-start >/dev/null 2>&1; }
|
|
|
|
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; }
|
|
|
|
uci_get() { uci -q get ${CONFIG}.$1; }
|
|
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
|
|
|
|
# Load configuration
|
|
load_config() {
|
|
http_port="$(uci_get main.http_port)" || http_port="8501"
|
|
http_host="$(uci_get main.http_host)" || http_host="0.0.0.0"
|
|
data_path="$(uci_get main.data_path)" || data_path="/srv/streamlit"
|
|
memory_limit="$(uci_get main.memory_limit)" || memory_limit="512M"
|
|
active_app="$(uci_get main.active_app)" || active_app="hello"
|
|
|
|
# Server settings
|
|
headless="$(uci_get server.headless)" || headless="true"
|
|
gather_stats="$(uci_get server.browser_gather_usage_stats)" || gather_stats="false"
|
|
theme_base="$(uci_get server.theme_base)" || theme_base="dark"
|
|
theme_primary="$(uci_get server.theme_primary_color)" || theme_primary="#0ff"
|
|
|
|
ensure_dir "$data_path"
|
|
ensure_dir "$data_path/apps"
|
|
ensure_dir "$data_path/logs"
|
|
APPS_PATH="$data_path/apps"
|
|
}
|
|
|
|
# Usage
|
|
usage() {
|
|
cat <<EOF
|
|
SecuBox Streamlit Platform Controller
|
|
|
|
Usage: $(basename $0) <command> [options]
|
|
|
|
Commands:
|
|
install Download Alpine rootfs and setup LXC container
|
|
uninstall Remove container (preserves apps)
|
|
update Update Streamlit package in container
|
|
status Show service status (JSON format)
|
|
logs Show container logs
|
|
shell Open shell in container
|
|
|
|
app list List deployed apps
|
|
app add <name> <path> Deploy new app
|
|
app remove <name> Remove app
|
|
|
|
instance list List running instances
|
|
instance add <name> <app> <port> Add instance config
|
|
instance remove <name> Remove instance config
|
|
instance enable <name> Enable instance
|
|
instance disable <name> Disable instance
|
|
|
|
service-run Start service (used by init)
|
|
service-stop Stop service (used by init)
|
|
|
|
Configuration:
|
|
/etc/config/streamlit
|
|
|
|
Data directory:
|
|
/srv/streamlit
|
|
|
|
Multi-Instance Mode:
|
|
Add instances in /etc/config/streamlit:
|
|
config instance 'myapp'
|
|
option app 'myapp.py'
|
|
option port '8502'
|
|
option enabled '1'
|
|
|
|
Requirements:
|
|
Place requirements.txt in /srv/streamlit/apps/
|
|
- <appname>.requirements.txt
|
|
- <appname>_requirements.txt
|
|
- requirements.txt (global)
|
|
|
|
EOF
|
|
}
|
|
|
|
# Check prerequisites
|
|
lxc_check_prereqs() {
|
|
if ! has_lxc; then
|
|
log_error "LXC not installed. Install with: opkg install lxc lxc-common"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Create Python LXC rootfs from Alpine
|
|
lxc_create_rootfs() {
|
|
local rootfs="$LXC_ROOTFS"
|
|
local arch=$(uname -m)
|
|
|
|
log_info "Creating Alpine rootfs for Streamlit..."
|
|
|
|
ensure_dir "$rootfs"
|
|
|
|
# Use Alpine mini rootfs
|
|
local alpine_version="3.21"
|
|
case "$arch" in
|
|
x86_64) alpine_arch="x86_64" ;;
|
|
aarch64) alpine_arch="aarch64" ;;
|
|
armv7l) alpine_arch="armv7" ;;
|
|
*) log_error "Unsupported architecture: $arch"; return 1 ;;
|
|
esac
|
|
|
|
local alpine_url="https://dl-cdn.alpinelinux.org/alpine/v${alpine_version}/releases/${alpine_arch}/alpine-minirootfs-${alpine_version}.0-${alpine_arch}.tar.gz"
|
|
local tmpfile="/tmp/alpine-rootfs.tar.gz"
|
|
|
|
log_info "Downloading Alpine ${alpine_version} rootfs..."
|
|
wget -q -O "$tmpfile" "$alpine_url" || {
|
|
log_error "Failed to download Alpine rootfs"
|
|
return 1
|
|
}
|
|
|
|
log_info "Extracting rootfs..."
|
|
tar -xzf "$tmpfile" -C "$rootfs" || {
|
|
log_error "Failed to extract rootfs"
|
|
return 1
|
|
}
|
|
rm -f "$tmpfile"
|
|
|
|
# Setup resolv.conf
|
|
cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \
|
|
echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf"
|
|
|
|
# Create startup script - Multi-instance support
|
|
cat > "$rootfs/opt/start-streamlit.sh" << 'STARTUP'
|
|
#!/bin/sh
|
|
|
|
# Install Python and Streamlit on first run
|
|
if [ ! -f /opt/.installed ]; then
|
|
echo "Installing Python 3.12 and dependencies..."
|
|
apk update
|
|
apk add --no-cache python3 py3-pip procps
|
|
|
|
echo "Installing Streamlit..."
|
|
pip3 install --break-system-packages streamlit 2>/dev/null || \
|
|
pip3 install streamlit 2>/dev/null
|
|
|
|
touch /opt/.installed
|
|
echo "Installation complete!"
|
|
fi
|
|
|
|
# Create default hello app if missing
|
|
mkdir -p /srv/apps
|
|
if [ ! -f "/srv/apps/hello.py" ]; then
|
|
cat > /srv/apps/hello.py << 'HELLO'
|
|
import streamlit as st
|
|
st.set_page_config(page_title="SecuBox Streamlit", page_icon="⚡", layout="wide")
|
|
st.title("⚡ SECUBOX STREAMLIT ⚡")
|
|
st.markdown("### Neural Data App Platform")
|
|
col1, col2, col3 = st.columns(3)
|
|
with col1:
|
|
st.metric("Status", "ONLINE", delta="Active")
|
|
with col2:
|
|
st.metric("Apps", "1", delta="+1")
|
|
with col3:
|
|
st.metric("Platform", "SecuBox")
|
|
st.info("Deploy your Streamlit apps via LuCI dashboard")
|
|
HELLO
|
|
fi
|
|
|
|
# Function to install requirements for an app
|
|
install_requirements() {
|
|
local app_name="$1"
|
|
for req_file in "/srv/apps/${app_name}.requirements.txt" \
|
|
"/srv/apps/${app_name}_requirements.txt" \
|
|
"/srv/apps/requirements.txt"; do
|
|
if [ -f "$req_file" ]; then
|
|
REQ_HASH=$(md5sum "$req_file" 2>/dev/null | cut -d' ' -f1)
|
|
REQ_MARKER="/opt/.req_${app_name}_${REQ_HASH}"
|
|
if [ ! -f "$REQ_MARKER" ]; then
|
|
echo "Installing requirements for $app_name from: $req_file"
|
|
pip3 install --break-system-packages -r "$req_file" 2>/dev/null || \
|
|
pip3 install -r "$req_file" 2>/dev/null || true
|
|
touch "$REQ_MARKER"
|
|
fi
|
|
break
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Function to start a single Streamlit instance
|
|
start_instance() {
|
|
local app_file="$1"
|
|
local port="$2"
|
|
local app_name=$(basename "$app_file" .py)
|
|
|
|
if [ ! -f "/srv/apps/$app_file" ]; then
|
|
echo "App not found: $app_file"
|
|
return 1
|
|
fi
|
|
|
|
install_requirements "$app_name"
|
|
|
|
echo "Starting instance: $app_name on port $port"
|
|
cd /srv/apps
|
|
streamlit run "$app_file" \
|
|
--server.address="0.0.0.0" \
|
|
--server.port="$port" \
|
|
--server.headless=true \
|
|
--browser.gatherUsageStats=false \
|
|
--theme.base="${STREAMLIT_THEME_BASE:-dark}" \
|
|
--theme.primaryColor="${STREAMLIT_THEME_PRIMARY:-#0ff}" &
|
|
echo $! > "/tmp/streamlit_${app_name}.pid"
|
|
}
|
|
|
|
# Parse STREAMLIT_INSTANCES env var (format: "app1.py:8501,app2.py:8502")
|
|
if [ -n "$STREAMLIT_INSTANCES" ]; then
|
|
echo "Multi-instance mode: $STREAMLIT_INSTANCES"
|
|
IFS=','
|
|
for instance in $STREAMLIT_INSTANCES; do
|
|
app_file=$(echo "$instance" | cut -d: -f1)
|
|
port=$(echo "$instance" | cut -d: -f2)
|
|
start_instance "$app_file" "$port"
|
|
done
|
|
unset IFS
|
|
else
|
|
# Single instance mode (backward compatible)
|
|
ACTIVE_APP="${STREAMLIT_APP:-hello.py}"
|
|
PORT="${STREAMLIT_PORT:-8501}"
|
|
start_instance "$ACTIVE_APP" "$PORT"
|
|
fi
|
|
|
|
# Keep container running and monitor processes
|
|
echo "Streamlit instances started. Monitoring..."
|
|
while true; do
|
|
sleep 30
|
|
# Check if any streamlit process is running
|
|
if ! pgrep -f "streamlit" >/dev/null; then
|
|
echo "No streamlit processes running, exiting..."
|
|
exit 1
|
|
fi
|
|
done
|
|
STARTUP
|
|
chmod +x "$rootfs/opt/start-streamlit.sh"
|
|
|
|
log_info "Rootfs created successfully"
|
|
return 0
|
|
}
|
|
|
|
# Build instances string from UCI config
|
|
_instances_result=""
|
|
_build_instance_entry() {
|
|
local section="$1"
|
|
local inst_enabled inst_app inst_port
|
|
config_get inst_enabled "$section" enabled "0"
|
|
config_get inst_app "$section" app ""
|
|
config_get inst_port "$section" port ""
|
|
|
|
if [ "$inst_enabled" = "1" ] && [ -n "$inst_app" ] && [ -n "$inst_port" ]; then
|
|
[ -n "$_instances_result" ] && _instances_result="${_instances_result},"
|
|
_instances_result="${_instances_result}${inst_app}:${inst_port}"
|
|
fi
|
|
}
|
|
|
|
build_instances_string() {
|
|
_instances_result=""
|
|
config_load "$CONFIG"
|
|
config_foreach _build_instance_entry instance
|
|
echo "$_instances_result"
|
|
}
|
|
|
|
# Create LXC config
|
|
lxc_create_config() {
|
|
load_config
|
|
|
|
ensure_dir "$(dirname "$LXC_CONFIG")"
|
|
|
|
# Convert memory limit to bytes
|
|
local mem_bytes
|
|
case "$memory_limit" in
|
|
*G|*g) mem_bytes=$((${memory_limit%[Gg]} * 1024 * 1024 * 1024)) ;;
|
|
*M|*m) mem_bytes=$((${memory_limit%[Mm]} * 1024 * 1024)) ;;
|
|
*K|*k) mem_bytes=$((${memory_limit%[Kk]} * 1024)) ;;
|
|
*) mem_bytes="$memory_limit" ;;
|
|
esac
|
|
|
|
# Build multi-instance string or fallback to single app
|
|
local instances_str
|
|
instances_str=$(build_instances_string)
|
|
|
|
# Fallback: if no instances defined, use active_app
|
|
local app_file=""
|
|
if [ -z "$instances_str" ]; then
|
|
if [ -f "$APPS_PATH/${active_app}.py" ]; then
|
|
app_file="${active_app}.py"
|
|
elif [ -f "$APPS_PATH/${active_app}" ]; then
|
|
app_file="${active_app}"
|
|
else
|
|
app_file="hello.py"
|
|
fi
|
|
fi
|
|
|
|
cat > "$LXC_CONFIG" << EOF
|
|
# Streamlit Platform LXC Configuration
|
|
lxc.uts.name = $LXC_NAME
|
|
lxc.rootfs.path = dir:$LXC_ROOTFS
|
|
lxc.arch = $(uname -m)
|
|
|
|
# Network: use host network
|
|
lxc.net.0.type = none
|
|
|
|
# Mount points
|
|
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
|
lxc.mount.entry = $APPS_PATH srv/apps none bind,create=dir 0 0
|
|
lxc.mount.entry = $data_path/logs srv/logs none bind,create=dir 0 0
|
|
|
|
# Environment
|
|
lxc.environment = STREAMLIT_THEME_BASE=$theme_base
|
|
lxc.environment = STREAMLIT_THEME_PRIMARY=$theme_primary
|
|
EOF
|
|
|
|
# Add multi-instance or single-instance env vars
|
|
if [ -n "$instances_str" ]; then
|
|
echo "lxc.environment = STREAMLIT_INSTANCES=$instances_str" >> "$LXC_CONFIG"
|
|
else
|
|
cat >> "$LXC_CONFIG" << EOF
|
|
lxc.environment = STREAMLIT_APP=$app_file
|
|
lxc.environment = STREAMLIT_PORT=$http_port
|
|
EOF
|
|
fi
|
|
|
|
cat >> "$LXC_CONFIG" << EOF
|
|
|
|
# Security
|
|
lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio
|
|
|
|
# Resource limits
|
|
lxc.cgroup.memory.limit_in_bytes = $mem_bytes
|
|
|
|
# Init command
|
|
lxc.init.cmd = /opt/start-streamlit.sh
|
|
EOF
|
|
|
|
log_info "LXC config created"
|
|
}
|
|
|
|
# Container control
|
|
lxc_running() {
|
|
lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"
|
|
}
|
|
|
|
lxc_exists() {
|
|
[ -f "$LXC_CONFIG" ] && [ -d "$LXC_ROOTFS" ]
|
|
}
|
|
|
|
lxc_stop() {
|
|
if lxc_running; then
|
|
log_info "Stopping Streamlit container..."
|
|
lxc-stop -n "$LXC_NAME" -k 2>/dev/null || true
|
|
sleep 2
|
|
fi
|
|
}
|
|
|
|
lxc_run() {
|
|
load_config
|
|
lxc_stop
|
|
|
|
if ! lxc_exists; then
|
|
log_error "Container not installed. Run: streamlitctl install"
|
|
return 1
|
|
fi
|
|
|
|
# Regenerate config in case settings changed
|
|
lxc_create_config
|
|
|
|
log_info "Starting Streamlit container..."
|
|
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
|
|
}
|
|
|
|
# App management
|
|
cmd_app_list() {
|
|
load_config
|
|
|
|
echo "{"
|
|
echo ' "apps": ['
|
|
|
|
local first=1
|
|
if [ -d "$APPS_PATH" ]; then
|
|
for app in "$APPS_PATH"/*.py; do
|
|
[ -f "$app" ] || continue
|
|
local name=$(basename "$app" .py)
|
|
local size=$(ls -la "$app" 2>/dev/null | awk '{print $5}')
|
|
local mtime=$(ls -la "$app" 2>/dev/null | awk '{print $6, $7, $8}')
|
|
|
|
# Check if this is the active app
|
|
local is_active="false"
|
|
if [ "$name" = "$active_app" ] || [ "${name}.py" = "$active_app" ]; then
|
|
is_active="true"
|
|
fi
|
|
|
|
[ $first -eq 0 ] && echo ","
|
|
first=0
|
|
printf ' {"name": "%s", "path": "%s", "size": "%s", "modified": "%s", "active": %s}' \
|
|
"$name" "$app" "$size" "$mtime" "$is_active"
|
|
done
|
|
fi
|
|
|
|
echo ""
|
|
echo " ],"
|
|
echo " \"active_app\": \"$active_app\","
|
|
echo " \"apps_path\": \"$APPS_PATH\""
|
|
echo "}"
|
|
}
|
|
|
|
cmd_app_add() {
|
|
local name="$1"
|
|
local path="$2"
|
|
|
|
if [ -z "$name" ] || [ -z "$path" ]; then
|
|
log_error "Usage: streamlitctl app add <name> <path>"
|
|
return 1
|
|
fi
|
|
|
|
load_config
|
|
|
|
if [ ! -f "$path" ]; then
|
|
log_error "Source file not found: $path"
|
|
return 1
|
|
fi
|
|
|
|
# Validate it looks like a Python file
|
|
if ! echo "$path" | grep -q '\.py$'; then
|
|
log_error "Source file must be a .py file"
|
|
return 1
|
|
fi
|
|
|
|
ensure_dir "$APPS_PATH"
|
|
local dest="$APPS_PATH/${name}.py"
|
|
|
|
cp "$path" "$dest" || {
|
|
log_error "Failed to copy app to $dest"
|
|
return 1
|
|
}
|
|
|
|
# Register in UCI
|
|
uci set "${CONFIG}.${name}=app"
|
|
uci set "${CONFIG}.${name}.name=$name"
|
|
uci set "${CONFIG}.${name}.path=${name}.py"
|
|
uci set "${CONFIG}.${name}.enabled=1"
|
|
uci commit "$CONFIG"
|
|
|
|
log_info "App '$name' added successfully"
|
|
echo '{"success": true, "message": "App added", "name": "'"$name"'"}'
|
|
}
|
|
|
|
cmd_app_remove() {
|
|
local name="$1"
|
|
|
|
if [ -z "$name" ]; then
|
|
log_error "Usage: streamlitctl app remove <name>"
|
|
return 1
|
|
fi
|
|
|
|
if [ "$name" = "hello" ]; then
|
|
log_error "Cannot remove the default hello app"
|
|
return 1
|
|
fi
|
|
|
|
load_config
|
|
|
|
local app_file="$APPS_PATH/${name}.py"
|
|
if [ -f "$app_file" ]; then
|
|
rm -f "$app_file"
|
|
fi
|
|
|
|
# Remove from UCI
|
|
uci -q delete "${CONFIG}.${name}" 2>/dev/null || true
|
|
uci commit "$CONFIG"
|
|
|
|
# If this was the active app, switch to hello
|
|
if [ "$active_app" = "$name" ]; then
|
|
uci_set main.active_app "hello"
|
|
fi
|
|
|
|
log_info "App '$name' removed"
|
|
echo '{"success": true, "message": "App removed", "name": "'"$name"'"}'
|
|
}
|
|
|
|
cmd_app_run() {
|
|
local name="$1"
|
|
|
|
if [ -z "$name" ]; then
|
|
log_error "Usage: streamlitctl app run <name>"
|
|
return 1
|
|
fi
|
|
|
|
load_config
|
|
|
|
local app_file="$APPS_PATH/${name}.py"
|
|
if [ ! -f "$app_file" ]; then
|
|
log_error "App not found: $name"
|
|
return 1
|
|
fi
|
|
|
|
uci_set main.active_app "$name"
|
|
|
|
# Restart if running
|
|
if lxc_running; then
|
|
log_info "Switching to app: $name (restarting container)"
|
|
/etc/init.d/streamlit restart
|
|
else
|
|
log_info "Active app set to: $name"
|
|
fi
|
|
|
|
echo '{"success": true, "message": "Active app changed", "active_app": "'"$name"'"}'
|
|
}
|
|
|
|
# Commands
|
|
cmd_install() {
|
|
require_root
|
|
load_config
|
|
|
|
log_info "Installing Streamlit Platform..."
|
|
|
|
lxc_check_prereqs || exit 1
|
|
|
|
# Create container
|
|
if ! lxc_exists; then
|
|
lxc_create_rootfs || exit 1
|
|
fi
|
|
|
|
lxc_create_config || exit 1
|
|
|
|
# Setup default app
|
|
ensure_dir "$APPS_PATH"
|
|
if [ -f "$DEFAULT_APP" ] && [ ! -f "$APPS_PATH/hello.py" ]; then
|
|
cp "$DEFAULT_APP" "$APPS_PATH/hello.py"
|
|
log_info "Default hello app installed"
|
|
fi
|
|
|
|
# Enable service
|
|
uci_set main.enabled '1'
|
|
/etc/init.d/streamlit enable 2>/dev/null || true
|
|
|
|
log_info "Installation complete!"
|
|
log_info ""
|
|
log_info "Start with: /etc/init.d/streamlit start"
|
|
log_info "Web interface: http://<router-ip>:$http_port"
|
|
}
|
|
|
|
cmd_uninstall() {
|
|
require_root
|
|
|
|
log_info "Uninstalling Streamlit Platform..."
|
|
|
|
# Stop service
|
|
/etc/init.d/streamlit stop 2>/dev/null || true
|
|
/etc/init.d/streamlit disable 2>/dev/null || true
|
|
|
|
lxc_stop
|
|
|
|
# Remove container (keep apps)
|
|
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
|
|
rm -rf "$LXC_PATH/$LXC_NAME"
|
|
log_info "Container removed"
|
|
fi
|
|
|
|
uci_set main.enabled '0'
|
|
|
|
log_info "Streamlit Platform uninstalled"
|
|
log_info "Apps preserved in: $(uci_get main.data_path)/apps"
|
|
}
|
|
|
|
cmd_update() {
|
|
require_root
|
|
load_config
|
|
|
|
if ! lxc_exists; then
|
|
log_error "Container not installed. Run: streamlitctl install"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Updating Streamlit in container..."
|
|
|
|
# Remove installed marker to force reinstall
|
|
rm -f "$LXC_ROOTFS/opt/.installed"
|
|
|
|
# Restart to trigger update
|
|
if [ "$(uci_get main.enabled)" = "1" ]; then
|
|
/etc/init.d/streamlit restart
|
|
fi
|
|
|
|
log_info "Update will apply on next start"
|
|
}
|
|
|
|
cmd_status() {
|
|
load_config
|
|
|
|
local enabled="$(uci_get main.enabled)"
|
|
local running="false"
|
|
local installed="false"
|
|
local uptime=""
|
|
|
|
if lxc_exists; then
|
|
installed="true"
|
|
fi
|
|
|
|
if lxc_running; then
|
|
running="true"
|
|
uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1 | awk '{print $3}')
|
|
fi
|
|
|
|
# Get LAN IP for URL
|
|
local lan_ip
|
|
lan_ip=$(uci -q get network.lan.ipaddr || echo "192.168.1.1")
|
|
|
|
# Count apps
|
|
local app_count=0
|
|
if [ -d "$APPS_PATH" ]; then
|
|
app_count=$(ls -1 "$APPS_PATH"/*.py 2>/dev/null | wc -l)
|
|
fi
|
|
|
|
cat << EOF
|
|
{
|
|
"enabled": $([ "$enabled" = "1" ] && echo "true" || echo "false"),
|
|
"running": $running,
|
|
"installed": $installed,
|
|
"uptime": "$uptime",
|
|
"http_port": $http_port,
|
|
"data_path": "$data_path",
|
|
"memory_limit": "$memory_limit",
|
|
"active_app": "$active_app",
|
|
"app_count": $app_count,
|
|
"web_url": "http://${lan_ip}:${http_port}",
|
|
"container_name": "$LXC_NAME"
|
|
}
|
|
EOF
|
|
}
|
|
|
|
cmd_status_text() {
|
|
load_config
|
|
|
|
local enabled="$(uci_get main.enabled)"
|
|
local running="false"
|
|
local uptime=""
|
|
|
|
if lxc_running; then
|
|
running="true"
|
|
uptime=$(lxc-info -n "$LXC_NAME" 2>/dev/null | grep -i "cpu use" | head -1)
|
|
fi
|
|
|
|
cat << EOF
|
|
Streamlit Platform Status
|
|
==========================
|
|
Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no")
|
|
Running: $([ "$running" = "true" ] && echo "yes" || echo "no")
|
|
HTTP Port: $http_port
|
|
Data Path: $data_path
|
|
Memory: $memory_limit
|
|
Active App: $active_app
|
|
|
|
Container: $LXC_NAME
|
|
Rootfs: $LXC_ROOTFS
|
|
Config: $LXC_CONFIG
|
|
|
|
EOF
|
|
|
|
if [ "$running" = "true" ]; then
|
|
echo "Web interface: http://$(uci -q get network.lan.ipaddr || echo "localhost"):$http_port"
|
|
fi
|
|
}
|
|
|
|
cmd_logs() {
|
|
load_config
|
|
|
|
local lines="${1:-100}"
|
|
|
|
if [ -d "$data_path/logs" ]; then
|
|
if [ -n "$(ls -A "$data_path/logs" 2>/dev/null)" ]; then
|
|
tail -n "$lines" "$data_path/logs"/*.log 2>/dev/null || \
|
|
cat "$data_path/logs"/*.log 2>/dev/null || \
|
|
echo "No logs found"
|
|
else
|
|
echo "No logs yet"
|
|
fi
|
|
else
|
|
echo "Log directory not found"
|
|
fi
|
|
|
|
# Also check install logs
|
|
for logfile in /var/log/streamlit-install.log /var/log/streamlit-update.log; do
|
|
if [ -f "$logfile" ]; then
|
|
echo ""
|
|
echo "=== $logfile ==="
|
|
tail -n 50 "$logfile"
|
|
fi
|
|
done
|
|
}
|
|
|
|
cmd_shell() {
|
|
require_root
|
|
|
|
if ! lxc_running; then
|
|
log_error "Container not running"
|
|
exit 1
|
|
fi
|
|
|
|
lxc-attach -n "$LXC_NAME" -- /bin/sh
|
|
}
|
|
|
|
cmd_service_run() {
|
|
require_root
|
|
load_config
|
|
|
|
lxc_check_prereqs || exit 1
|
|
lxc_run
|
|
}
|
|
|
|
cmd_service_stop() {
|
|
require_root
|
|
lxc_stop
|
|
}
|
|
|
|
# Instance management
|
|
_instance_list_first=1
|
|
_print_instance_json() {
|
|
local section="$1"
|
|
local name app port enabled
|
|
config_get name "$section" name "$section"
|
|
config_get app "$section" app ""
|
|
config_get port "$section" port ""
|
|
config_get enabled "$section" enabled "0"
|
|
|
|
[ "$_instance_list_first" -eq 0 ] && echo ","
|
|
_instance_list_first=0
|
|
printf ' {"id": "%s", "name": "%s", "app": "%s", "port": "%s", "enabled": %s}' \
|
|
"$section" "$name" "$app" "$port" "$([ "$enabled" = "1" ] && echo "true" || echo "false")"
|
|
}
|
|
|
|
cmd_instance_list() {
|
|
load_config
|
|
echo "{"
|
|
echo ' "instances": ['
|
|
|
|
_instance_list_first=1
|
|
config_load "$CONFIG"
|
|
config_foreach _print_instance_json instance
|
|
echo ""
|
|
echo " ]"
|
|
echo "}"
|
|
}
|
|
|
|
cmd_instance_add() {
|
|
local name="$1"
|
|
local app="$2"
|
|
local port="$3"
|
|
|
|
if [ -z "$name" ] || [ -z "$app" ] || [ -z "$port" ]; then
|
|
log_error "Usage: streamlitctl instance add <name> <app.py> <port>"
|
|
return 1
|
|
fi
|
|
|
|
# Validate port is numeric
|
|
if ! echo "$port" | grep -qE '^[0-9]+$'; then
|
|
log_error "Port must be a number"
|
|
return 1
|
|
fi
|
|
|
|
uci set "${CONFIG}.${name}=instance"
|
|
uci set "${CONFIG}.${name}.name=$name"
|
|
uci set "${CONFIG}.${name}.app=$app"
|
|
uci set "${CONFIG}.${name}.port=$port"
|
|
uci set "${CONFIG}.${name}.enabled=1"
|
|
uci commit "$CONFIG"
|
|
|
|
log_info "Instance '$name' added (app: $app, port: $port)"
|
|
echo '{"success": true, "message": "Instance added", "name": "'"$name"'", "port": '"$port"'}'
|
|
}
|
|
|
|
cmd_instance_remove() {
|
|
local name="$1"
|
|
|
|
if [ -z "$name" ]; then
|
|
log_error "Usage: streamlitctl instance remove <name>"
|
|
return 1
|
|
fi
|
|
|
|
uci -q delete "${CONFIG}.${name}" 2>/dev/null || {
|
|
log_error "Instance not found: $name"
|
|
return 1
|
|
}
|
|
uci commit "$CONFIG"
|
|
|
|
log_info "Instance '$name' removed"
|
|
echo '{"success": true, "message": "Instance removed", "name": "'"$name"'"}'
|
|
}
|
|
|
|
cmd_instance_enable() {
|
|
local name="$1"
|
|
|
|
if [ -z "$name" ]; then
|
|
log_error "Usage: streamlitctl instance enable <name>"
|
|
return 1
|
|
fi
|
|
|
|
uci set "${CONFIG}.${name}.enabled=1" && uci commit "$CONFIG" || {
|
|
log_error "Instance not found: $name"
|
|
return 1
|
|
}
|
|
|
|
log_info "Instance '$name' enabled"
|
|
echo '{"success": true, "message": "Instance enabled", "name": "'"$name"'"}'
|
|
}
|
|
|
|
cmd_instance_disable() {
|
|
local name="$1"
|
|
|
|
if [ -z "$name" ]; then
|
|
log_error "Usage: streamlitctl instance disable <name>"
|
|
return 1
|
|
fi
|
|
|
|
uci set "${CONFIG}.${name}.enabled=0" && uci commit "$CONFIG" || {
|
|
log_error "Instance not found: $name"
|
|
return 1
|
|
}
|
|
|
|
log_info "Instance '$name' disabled"
|
|
echo '{"success": true, "message": "Instance disabled", "name": "'"$name"'"}'
|
|
}
|
|
|
|
# Main
|
|
case "${1:-}" in
|
|
install) shift; cmd_install "$@" ;;
|
|
uninstall) shift; cmd_uninstall "$@" ;;
|
|
update) shift; cmd_update "$@" ;;
|
|
status)
|
|
shift
|
|
if [ "${1:-}" = "--json" ] || [ -t 0 ]; then
|
|
cmd_status "$@"
|
|
else
|
|
cmd_status_text "$@"
|
|
fi
|
|
;;
|
|
logs) shift; cmd_logs "$@" ;;
|
|
shell) shift; cmd_shell "$@" ;;
|
|
app)
|
|
shift
|
|
case "${1:-}" in
|
|
list) shift; cmd_app_list "$@" ;;
|
|
add) shift; cmd_app_add "$@" ;;
|
|
remove) shift; cmd_app_remove "$@" ;;
|
|
*) echo "Usage: streamlitctl app {list|add|remove}"; exit 1 ;;
|
|
esac
|
|
;;
|
|
instance)
|
|
shift
|
|
case "${1:-}" in
|
|
list) shift; cmd_instance_list "$@" ;;
|
|
add) shift; cmd_instance_add "$@" ;;
|
|
remove) shift; cmd_instance_remove "$@" ;;
|
|
enable) shift; cmd_instance_enable "$@" ;;
|
|
disable) shift; cmd_instance_disable "$@" ;;
|
|
*) echo "Usage: streamlitctl instance {list|add|remove|enable|disable}"; exit 1 ;;
|
|
esac
|
|
;;
|
|
service-run) shift; cmd_service_run "$@" ;;
|
|
service-stop) shift; cmd_service_stop "$@" ;;
|
|
*) usage ;;
|
|
esac
|