#!/bin/sh # SecuBox Streamlit Platform Controller # Copyright (C) 2025 CyberMind.fr # # Manages Streamlit in LXC container # Supports multi-instance with folder-based apps and Gitea integration # Source OpenWrt UCI functions . /lib/functions.sh CONFIG="streamlit" LXC_NAME="streamlit" # Paths LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" DEFAULT_APP="/usr/share/streamlit/hello.py" # Logging log_info() { echo "[INFO] $*"; logger -t streamlit "$*"; } log_warn() { echo "[WARN] $*" >&2; logger -t streamlit -p warning "$*"; } 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; } has_git() { command -v git >/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" # Gitea config gitea_enabled="$(uci_get gitea.enabled)" || gitea_enabled="0" gitea_url="$(uci_get gitea.url)" || gitea_url="http://192.168.255.1:3000" gitea_user="$(uci_get gitea.user)" || gitea_user="admin" gitea_token="$(uci_get gitea.token)" || gitea_token="" ensure_dir "$data_path" ensure_dir "$data_path/apps" ensure_dir "$data_path/logs" APPS_PATH="$data_path/apps" } # Usage usage() { cat < [options] Container Commands: install Download and setup LXC container uninstall Remove container (preserves apps) update Update Streamlit in container status Show service status logs [app] Show logs App Management (folder-based): app list List all apps app create Create new app folder app delete Delete app folder app deploy Deploy app from path/archive Instance Management: instance list List configured instances instance add Add instance for app on port instance remove Remove instance instance start Start single instance instance stop Stop single instance Gitea Integration: gitea setup Configure git credentials gitea clone Clone app from Gitea repo gitea pull Pull latest from Gitea Service Commands: service-run Start all instances (for init) service-stop Stop all instances Utility: shell Open shell in container exec Execute command in container App Folder Structure: /srv/streamlit/apps// app.py Main Streamlit app (or .py) requirements.txt Python dependencies .streamlit/ Streamlit config (optional) ... Other files Examples: # Create local app streamlitctl app create myapp streamlitctl instance add myapp 8502 /etc/init.d/streamlit restart # Deploy from Gitea (complete workflow) uci set streamlit.gitea.enabled=1 uci set streamlit.gitea.url='http://192.168.255.1:3000' uci set streamlit.gitea.user='admin' uci set streamlit.gitea.token='your-token' uci commit streamlit streamlitctl gitea setup streamlitctl gitea clone yijing CyberMood/yijing-oracle streamlitctl instance add yijing 8505 /etc/init.d/streamlit restart # Access at: http://:8505 # Update app from Gitea streamlitctl gitea pull yijing /etc/init.d/streamlit restart 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" 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" cp /etc/resolv.conf "$rootfs/etc/resolv.conf" 2>/dev/null || \ echo "nameserver 1.1.1.1" > "$rootfs/etc/resolv.conf" log_info "Rootfs created successfully" return 0 } # Create startup script for multi-instance folder-based apps create_startup_script() { load_config local start_script="$LXC_ROOTFS/opt/start-streamlit.sh" cat > "$start_script" << 'STARTUP' #!/bin/sh export PATH=/usr/local/bin:/usr/bin:/bin:$PATH export HOME=/root APPS_BASE="/srv/apps" PIDS_DIR="/var/run/streamlit" LOG_DIR="/var/log/streamlit" INSTANCES_CONF="/srv/apps/instances.conf" mkdir -p "$PIDS_DIR" "$LOG_DIR" # 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 git 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 if [ ! -d "$APPS_BASE/hello" ]; then mkdir -p "$APPS_BASE/hello" cat > "$APPS_BASE/hello/app.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", "Ready", delta="+1") with col3: st.metric("Platform", "SecuBox") st.info("Deploy your Streamlit apps via LuCI dashboard or CLI") HELLO fi # Function to find main app file in folder find_app_file() { local app_dir="$1" local app_name=$(basename "$app_dir") # Priority: app.py > main.py > .py > first .py file for candidate in "$app_dir/app.py" "$app_dir/main.py" "$app_dir/${app_name}.py"; do [ -f "$candidate" ] && { echo "$candidate"; return 0; } done # Fallback to first .py file local first_py=$(ls -1 "$app_dir"/*.py 2>/dev/null | head -1) [ -n "$first_py" ] && { echo "$first_py"; return 0; } return 1 } # Function to install requirements for an app install_requirements() { local app_dir="$1" local app_name=$(basename "$app_dir") # Find requirements file: requirements.txt first, then any requirements*.txt local req_file="" if [ -f "$app_dir/requirements.txt" ]; then req_file="$app_dir/requirements.txt" else req_file=$(ls -1 "$app_dir"/requirements*.txt 2>/dev/null | head -1) fi if [ -n "$req_file" ] && [ -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 from $(basename $req_file) for $app_name..." pip3 install --break-system-packages -r "$req_file" 2>/dev/null || \ pip3 install -r "$req_file" 2>/dev/null || true touch "$REQ_MARKER" fi fi } # Function to start a single instance start_instance() { local app_name="$1" local port="$2" local app_dir="$APPS_BASE/$app_name" local app_file="" local work_dir="" if [ -d "$app_dir" ]; then # Folder-based app (ZIP upload, Gitea clone, or created via CLI) app_file=$(find_app_file "$app_dir") if [ -z "$app_file" ]; then echo "No Python app file found in $app_dir" return 1 fi install_requirements "$app_dir" work_dir="$app_dir" elif [ -f "$APPS_BASE/${app_name}.py" ]; then # Top-level single .py file (direct upload) app_file="$APPS_BASE/${app_name}.py" work_dir="$APPS_BASE" else echo "App not found: $app_name (no folder or .py file)" return 1 fi echo "Starting instance: $app_name on port $port (file: $(basename $app_file))" # Change to app/work directory so relative imports work cd "$work_dir" nohup streamlit run "$(basename $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}" \ > "$LOG_DIR/${app_name}.log" 2>&1 & echo $! > "$PIDS_DIR/${app_name}.pid" echo "Instance '$app_name' started (PID: $!)" } # Function to stop an instance stop_instance() { local app_name="$1" local pidfile="$PIDS_DIR/${app_name}.pid" if [ -f "$pidfile" ]; then local pid=$(cat "$pidfile") if kill -0 "$pid" 2>/dev/null; then kill "$pid" echo "Stopped instance '$app_name' (PID: $pid)" fi rm -f "$pidfile" fi } # Read instances from config file (format: appname:port) if [ -f "$INSTANCES_CONF" ]; then echo "Loading instances from config..." while IFS=: read -r app_name port; do [ -n "$app_name" ] && [ -n "$port" ] && start_instance "$app_name" "$port" done < "$INSTANCES_CONF" else # Fallback: single instance mode if [ -n "$STREAMLIT_APP" ] && [ -n "$STREAMLIT_PORT" ]; then start_instance "$STREAMLIT_APP" "$STREAMLIT_PORT" else # Default: start hello on 8501 start_instance "hello" "8501" fi fi # Keep container running echo "" echo "Streamlit multi-instance manager running." echo "Instances:" ls -1 "$PIDS_DIR"/*.pid 2>/dev/null | while read f; do name=$(basename "$f" .pid) pid=$(cat "$f") echo " - $name (PID: $pid)" done # Monitor loop while true; do sleep 30 if ! pgrep -f "streamlit" >/dev/null; then echo "No streamlit processes running, exiting..." exit 1 fi done STARTUP chmod +x "$start_script" } # Generate instances.conf from UCI generate_instances_conf() { load_config local conf_file="$data_path/apps/instances.conf" > "$conf_file" # Iterate over instance sections _add_instance_to_conf() { 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 echo "${inst_app}:${inst_port}" >> "$conf_file" fi } config_load "$CONFIG" config_foreach _add_instance_to_conf instance # If no instances, add default if [ ! -s "$conf_file" ]; then echo "hello:8501" > "$conf_file" fi log_debug "Generated instances.conf: $(cat $conf_file | tr '\n' ' ')" } # Create LXC config lxc_create_config() { load_config ensure_dir "$(dirname "$LXC_CONFIG")" 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 cat > "$LXC_CONFIG" << EOF # Streamlit Platform LXC Configuration (Multi-Instance) 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 # Terminal lxc.tty.max = 0 lxc.pty.max = 256 # Standard character devices lxc.cgroup2.devices.allow = c 1:* rwm lxc.cgroup2.devices.allow = c 5:* rwm lxc.cgroup2.devices.allow = c 136:* rwm # Security lxc.cap.drop = sys_admin sys_module mac_admin mac_override sys_time sys_rawio # Resource limits lxc.cgroup2.memory.max = $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 lxc_create_config create_startup_script generate_instances_conf log_info "Starting Streamlit container..." exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" } lxc_exec() { if ! lxc_running; then log_error "Container not running. Start with: /etc/init.d/streamlit start" return 1 fi lxc-attach -n "$LXC_NAME" -- env PATH=/usr/local/bin:/usr/bin:/bin "$@" } # App management commands cmd_app_list() { load_config echo "Streamlit Apps:" echo "---------------" if [ -d "$APPS_PATH" ]; then for app_dir in "$APPS_PATH"/*/; do [ -d "$app_dir" ] || continue local name=$(basename "$app_dir") # Find main file local main_file="" for candidate in "$app_dir/app.py" "$app_dir/main.py" "$app_dir/${name}.py"; do [ -f "$candidate" ] && { main_file=$(basename "$candidate"); break; } done [ -z "$main_file" ] && main_file=$(ls -1 "$app_dir"/*.py 2>/dev/null | head -1 | xargs basename 2>/dev/null) # Check for requirements (requirements.txt or any requirements*.txt) local has_req="no" [ -f "$app_dir/requirements.txt" ] && has_req="yes" [ "$has_req" = "no" ] && ls -1 "$app_dir"/requirements*.txt >/dev/null 2>&1 && has_req="yes" # Check for git repo local has_git="no" [ -d "$app_dir/.git" ] && has_git="yes" printf " %-20s main:%-15s req:%-3s git:%-3s\n" "$name" "${main_file:-none}" "$has_req" "$has_git" done fi # Also list legacy single-file apps for app_file in "$APPS_PATH"/*.py; do [ -f "$app_file" ] || continue local name=$(basename "$app_file" .py) # Skip if there's a folder with same name [ -d "$APPS_PATH/$name" ] && continue printf " %-20s (legacy single-file)\n" "$name" done } cmd_app_create() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "App name required"; return 1; } # Validate name echo "$name" | grep -qE '^[a-z][a-z0-9_-]*$' || { log_error "Invalid app name. Use lowercase, numbers, underscore, hyphen." return 1 } local app_dir="$APPS_PATH/$name" if [ -d "$app_dir" ]; then log_error "App '$name' already exists" return 1 fi log_info "Creating app folder: $name" ensure_dir "$app_dir" ensure_dir "$app_dir/.streamlit" # Create template app.py cat > "$app_dir/app.py" << 'APPTEMPLATE' import streamlit as st st.set_page_config( page_title="My Streamlit App", page_icon="🚀", layout="wide" ) st.title("🚀 My Streamlit App") st.write("Edit this file to build your app!") # Example widgets name = st.text_input("Enter your name:") if name: st.success(f"Hello, {name}!") APPTEMPLATE # Create empty requirements.txt cat > "$app_dir/requirements.txt" << 'REQTEMPLATE' # Add your Python dependencies here, one per line # Example: # pandas>=2.0.0 # numpy # plotly REQTEMPLATE # Create .streamlit/config.toml cat > "$app_dir/.streamlit/config.toml" << 'CONFIGTEMPLATE' [theme] base = "dark" primaryColor = "#0ff" [server] headless = true CONFIGTEMPLATE # Register in UCI uci set "${CONFIG}.${name}=app" uci set "${CONFIG}.${name}.name=$name" uci set "${CONFIG}.${name}.enabled=1" uci commit "$CONFIG" log_info "App '$name' created at $app_dir" log_info "Next steps:" log_info " 1. Edit $app_dir/app.py" log_info " 2. Add dependencies to $app_dir/requirements.txt" log_info " 3. Start with: streamlitctl instance add $name " } cmd_app_delete() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "App name required"; return 1; } if [ "$name" = "hello" ]; then log_error "Cannot delete the default hello app" return 1 fi local app_dir="$APPS_PATH/$name" # Stop any running instance cmd_instance_stop "$name" 2>/dev/null # Remove directory if [ -d "$app_dir" ]; then rm -rf "$app_dir" log_info "App folder removed: $app_dir" fi # Remove legacy file if [ -f "$APPS_PATH/${name}.py" ]; then rm -f "$APPS_PATH/${name}.py" fi # Remove from UCI uci -q delete "${CONFIG}.${name}" 2>/dev/null uci commit "$CONFIG" log_info "App '$name' deleted" } cmd_app_deploy() { require_root load_config local name="$1" local source="$2" [ -z "$name" ] || [ -z "$source" ] && { log_error "Usage: streamlitctl app deploy " return 1 } local app_dir="$APPS_PATH/$name" ensure_dir "$app_dir" if [ -d "$source" ]; then # Copy directory contents log_info "Deploying from directory: $source" cp -r "$source"/* "$app_dir/" elif [ -f "$source" ]; then case "$source" in *.tar.gz|*.tgz) log_info "Extracting archive: $source" tar -xzf "$source" -C "$app_dir" ;; *.zip) log_info "Extracting zip: $source" unzip -q "$source" -d "$app_dir" ;; *.py) log_info "Deploying single file: $source" cp "$source" "$app_dir/app.py" ;; *) log_error "Unsupported file type" return 1 ;; esac else log_error "Source not found: $source" return 1 fi # Register in UCI uci set "${CONFIG}.${name}=app" uci set "${CONFIG}.${name}.name=$name" uci set "${CONFIG}.${name}.enabled=1" uci commit "$CONFIG" log_info "App '$name' deployed to $app_dir" # Auto-package for P2P distribution if [ -x /usr/sbin/secubox-content-pkg ]; then log_info "Packaging app for P2P distribution..." /usr/sbin/secubox-content-pkg streamlit "$name" 2>/dev/null && \ log_info "App packaged for mesh distribution" fi } # Instance management cmd_instance_list() { load_config echo "Streamlit Instances:" echo "--------------------" local found=0 _print_instance() { 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" [ -z "$app" ] || [ -z "$port" ] && return found=1 local status="disabled" [ "$enabled" = "1" ] && status="enabled" local running="" if lxc_running && [ -f "$LXC_ROOTFS/var/run/streamlit/${app}.pid" ]; then running=" [RUNNING]" fi printf " %-15s app:%-15s port:%-5s [%s]%s\n" "$section" "$app" "$port" "$status" "$running" } config_load "$CONFIG" config_foreach _print_instance instance [ "$found" = "0" ] && echo " (no instances configured)" } cmd_instance_add() { require_root load_config local app="$1" local port="$2" [ -z "$app" ] || [ -z "$port" ] && { log_error "Usage: streamlitctl instance add " return 1 } # Validate port echo "$port" | grep -qE '^[0-9]+$' || { log_error "Port must be a number" return 1 } # Check app exists if [ ! -d "$APPS_PATH/$app" ] && [ ! -f "$APPS_PATH/${app}.py" ]; then log_error "App not found: $app" log_info "Create with: streamlitctl app create $app" return 1 fi # Use app name as instance name local instance_name="$app" uci set "${CONFIG}.${instance_name}=instance" uci set "${CONFIG}.${instance_name}.name=$instance_name" uci set "${CONFIG}.${instance_name}.app=$app" uci set "${CONFIG}.${instance_name}.port=$port" uci set "${CONFIG}.${instance_name}.enabled=1" uci commit "$CONFIG" log_info "Instance added: $app on port $port" log_info "Restart service to apply: /etc/init.d/streamlit restart" } cmd_instance_remove() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } cmd_instance_stop "$name" 2>/dev/null uci -q delete "${CONFIG}.${name}" 2>/dev/null || { log_error "Instance not found: $name" return 1 } uci commit "$CONFIG" # Regenerate instances.conf generate_instances_conf log_info "Instance '$name' removed" } cmd_instance_start() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } # Get instance config local app=$(uci_get "${name}.app") local port=$(uci_get "${name}.port") [ -z "$app" ] || [ -z "$port" ] && { log_error "Instance '$name' not found or incomplete config" return 1 } if ! lxc_running; then log_error "Container not running. Start with: /etc/init.d/streamlit start" return 1 fi log_info "Starting instance '$name' (app: $app, port: $port)..." lxc_exec sh -c " WORK_DIR='' APP_FILE='' if [ -d /srv/apps/$app ]; then WORK_DIR='/srv/apps/$app' cd \"\$WORK_DIR\" # Find main file in folder for f in app.py main.py ${app}.py; do [ -f \"\$f\" ] && { APP_FILE=\"\$f\"; break; } done [ -z \"\$APP_FILE\" ] && APP_FILE=\$(ls -1 *.py 2>/dev/null | head -1) # Install requirements (requirements.txt or any requirements*.txt) REQ_FILE='' [ -f requirements.txt ] && REQ_FILE='requirements.txt' [ -z \"\$REQ_FILE\" ] && REQ_FILE=\$(ls -1 requirements*.txt 2>/dev/null | head -1) [ -n \"\$REQ_FILE\" ] && pip3 install --break-system-packages -r \"\$REQ_FILE\" 2>/dev/null elif [ -f /srv/apps/${app}.py ]; then WORK_DIR='/srv/apps' APP_FILE='${app}.py' cd \"\$WORK_DIR\" fi [ -z \"\$APP_FILE\" ] && { echo 'No Python file found for $app'; exit 1; } # Kill existing [ -f /var/run/streamlit/${app}.pid ] && kill \$(cat /var/run/streamlit/${app}.pid) 2>/dev/null mkdir -p /var/run/streamlit /var/log/streamlit nohup streamlit run \"\$APP_FILE\" \ --server.address=0.0.0.0 \ --server.port=$port \ --server.headless=true \ > /var/log/streamlit/${app}.log 2>&1 & echo \$! > /var/run/streamlit/${app}.pid echo \"Started \$APP_FILE on port $port (PID: \$!)\" " } cmd_instance_stop() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "Instance name required"; return 1; } local app=$(uci_get "${name}.app") [ -z "$app" ] && app="$name" if ! lxc_running; then return 0 fi log_info "Stopping instance '$name'..." lxc_exec sh -c " if [ -f /var/run/streamlit/${app}.pid ]; then kill \$(cat /var/run/streamlit/${app}.pid) 2>/dev/null rm -f /var/run/streamlit/${app}.pid echo 'Stopped' else echo 'Not running' fi " } # Gitea integration cmd_gitea_setup() { require_root load_config if [ -z "$gitea_token" ]; then log_error "Gitea token not configured" log_info "Set with: uci set streamlit.gitea.token='your-token' && uci commit streamlit" return 1 fi if ! lxc_running; then log_error "Container not running" return 1 fi log_info "Configuring git credentials..." local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||') lxc_exec sh -c " git config --global user.name '$gitea_user' git config --global user.email '${gitea_user}@localhost' git config --global credential.helper store rm -rf ~/.git-credentials cat > ~/.git-credentials << CRED https://${gitea_user}:${gitea_token}@${gitea_host} http://${gitea_user}:${gitea_token}@${gitea_host} CRED chmod 600 ~/.git-credentials " log_info "Git credentials configured" } cmd_gitea_clone() { require_root load_config local name="$1" local repo="$2" [ -z "$name" ] || [ -z "$repo" ] && { log_error "Usage: streamlitctl gitea clone " log_info " repo can be: username/reponame or full URL" return 1 } if [ "$gitea_enabled" != "1" ]; then log_error "Gitea integration not enabled" log_info "Enable with: uci set streamlit.gitea.enabled=1 && uci commit streamlit" return 1 fi if [ -z "$gitea_token" ]; then log_error "Gitea token not configured" return 1 fi local app_dir="$APPS_PATH/$name" # Build clone URL local clone_url if echo "$repo" | grep -q '^http'; then clone_url="$repo" else local gitea_host=$(echo "$gitea_url" | sed 's|^https\?://||' | sed 's|/.*||') clone_url="http://${gitea_user}:${gitea_token}@${gitea_host}/${repo}.git" fi if [ -d "$app_dir/.git" ]; then log_info "Repository already cloned, pulling latest..." cd "$app_dir" && git pull else log_info "Cloning $repo to $name..." ensure_dir "$(dirname "$app_dir")" rm -rf "$app_dir" git clone "$clone_url" "$app_dir" || { log_error "Failed to clone repository" return 1 } fi # Register in UCI uci set "${CONFIG}.${name}=app" uci set "${CONFIG}.${name}.name=$name" uci set "${CONFIG}.${name}.repo=$repo" uci set "${CONFIG}.${name}.enabled=1" uci commit "$CONFIG" log_info "App '$name' cloned from Gitea" log_info "Add instance with: streamlitctl instance add $name " } cmd_gitea_pull() { require_root load_config local name="$1" [ -z "$name" ] && { log_error "App name required"; return 1; } local app_dir="$APPS_PATH/$name" if [ ! -d "$app_dir/.git" ]; then log_error "App '$name' is not a git repository" return 1 fi log_info "Pulling latest for '$name'..." cd "$app_dir" && git pull log_info "Update complete" } # Main commands cmd_install() { require_root load_config log_info "Installing Streamlit Platform..." lxc_check_prereqs || exit 1 if ! lxc_exists; then lxc_create_rootfs || exit 1 fi lxc_create_config || exit 1 create_startup_script # Setup default app ensure_dir "$APPS_PATH/hello" uci_set main.enabled '1' /etc/init.d/streamlit enable 2>/dev/null || true log_info "Installation complete!" log_info "" log_info "Next steps:" log_info " 1. Create app: streamlitctl app create myapp" log_info " 2. Add instance: streamlitctl instance add myapp 8501" log_info " 3. Start: /etc/init.d/streamlit start" } cmd_uninstall() { require_root log_info "Uninstalling Streamlit Platform..." /etc/init.d/streamlit stop 2>/dev/null || true /etc/init.d/streamlit disable 2>/dev/null || true lxc_stop 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" return 1 fi log_info "Updating Streamlit..." rm -f "$LXC_ROOTFS/opt/.installed" 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" lxc_running && running="true" cat << EOF Streamlit Platform Status ========================== Enabled: $([ "$enabled" = "1" ] && echo "yes" || echo "no") Running: $([ "$running" = "true" ] && echo "yes" || echo "no") Data Path: $data_path Memory: $memory_limit Container: $LXC_NAME EOF echo "Apps:" cmd_app_list | tail -n +3 echo "" echo "Instances:" cmd_instance_list | tail -n +3 } cmd_logs() { load_config local app="${1:-}" if [ -n "$app" ]; then local log_file="$data_path/logs/${app}.log" if [ -f "$log_file" ]; then tail -100 "$log_file" else # Try inside container lxc_exec cat /var/log/streamlit/${app}.log 2>/dev/null || echo "No logs for '$app'" fi else # Show all logs for log_file in "$data_path/logs"/*.log; do [ -f "$log_file" ] || continue echo "=== $(basename "$log_file") ===" tail -50 "$log_file" echo "" done fi } 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 } # Main case "${1:-}" in install) shift; cmd_install "$@" ;; uninstall) shift; cmd_uninstall "$@" ;; update) shift; cmd_update "$@" ;; status) shift; cmd_status "$@" ;; logs) shift; cmd_logs "$@" ;; shell) shift; cmd_shell "$@" ;; app) shift case "${1:-}" in list) shift; cmd_app_list "$@" ;; create) shift; cmd_app_create "$@" ;; delete) shift; cmd_app_delete "$@" ;; deploy) shift; cmd_app_deploy "$@" ;; *) echo "Usage: streamlitctl app {list|create|delete|deploy}"; exit 1 ;; esac ;; instance) shift case "${1:-}" in list) shift; cmd_instance_list "$@" ;; add) shift; cmd_instance_add "$@" ;; remove) shift; cmd_instance_remove "$@" ;; start) shift; cmd_instance_start "$@" ;; stop) shift; cmd_instance_stop "$@" ;; *) echo "Usage: streamlitctl instance {list|add|remove|start|stop}"; exit 1 ;; esac ;; gitea) shift case "${1:-}" in setup) shift; cmd_gitea_setup "$@" ;; clone) shift; cmd_gitea_clone "$@" ;; pull) shift; cmd_gitea_pull "$@" ;; *) echo "Usage: streamlitctl gitea {setup|clone|pull}"; exit 1 ;; esac ;; service-run) shift; cmd_service_run "$@" ;; service-stop) shift; cmd_service_stop "$@" ;; *) usage ;; esac