Flask 1.1.2 requires specific old versions of dependencies: - Jinja2==2.11.3 (escape moved in 3.1) - markupsafe==1.1.1 - itsdangerous==1.1.0 (json removed in 2.x) - Werkzeug==1.0.1 - click==7.1.2 - pybeerxml<2.0.0 (Parser import changed in 2.x) - marshmallow<4.0.0 Also: - Use pip-installed package instead of git repo mount - Simplify LXC mounts to just data directories Tested and working on OpenWrt 24.10.5. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
446 lines
11 KiB
Bash
446 lines
11 KiB
Bash
#!/bin/sh
|
|
# SecuBox PicoBrew Server Controller
|
|
# Copyright (C) 2025 CyberMind.fr
|
|
#
|
|
# Manages PicoBrew Server in LXC container
|
|
|
|
CONFIG="picobrew"
|
|
LXC_NAME="picobrew"
|
|
|
|
# Paths
|
|
LXC_PATH="/srv/lxc"
|
|
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs"
|
|
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
|
|
REPO_URL="https://github.com/CyberMind-FR/picobrew-server.git"
|
|
REPO_PATH="/srv/picobrew/app"
|
|
|
|
# Logging
|
|
log_info() { echo "[INFO] $*"; logger -t picobrew "$*"; }
|
|
log_error() { echo "[ERROR] $*" >&2; logger -t picobrew -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="8080"
|
|
http_host="$(uci_get main.http_host)" || http_host="0.0.0.0"
|
|
data_path="$(uci_get main.data_path)" || data_path="/srv/picobrew"
|
|
memory_limit="$(uci_get main.memory_limit)" || memory_limit="512M"
|
|
log_level="$(uci_get main.log_level)" || log_level="INFO"
|
|
dns_name="$(uci_get server.dns_name)" || dns_name=""
|
|
https_enabled="$(uci_get server.https_enabled)" || https_enabled="0"
|
|
units="$(uci_get brewing.units)" || units="metric"
|
|
|
|
ensure_dir "$data_path"
|
|
ensure_dir "$data_path/recipes"
|
|
ensure_dir "$data_path/sessions"
|
|
ensure_dir "$data_path/logs"
|
|
}
|
|
|
|
# Usage
|
|
usage() {
|
|
cat <<EOF
|
|
SecuBox PicoBrew Server Controller
|
|
|
|
Usage: $(basename $0) <command> [options]
|
|
|
|
Commands:
|
|
install Download and install PicoBrew Server
|
|
uninstall Remove PicoBrew Server container
|
|
update Update PicoBrew Server to latest version
|
|
status Show service status
|
|
logs Show container logs
|
|
shell Open shell in container
|
|
|
|
service-run Start service (used by init)
|
|
service-stop Stop service (used by init)
|
|
|
|
Configuration:
|
|
/etc/config/picobrew
|
|
|
|
Data directory:
|
|
/srv/picobrew
|
|
|
|
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
|
|
if ! has_git; then
|
|
log_error "Git not installed. Install with: opkg install git"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Clone or update repo
|
|
repo_clone() {
|
|
load_config
|
|
|
|
if [ -d "$REPO_PATH/.git" ]; then
|
|
log_info "Updating PicoBrew Server repository..."
|
|
cd "$REPO_PATH" && git pull
|
|
else
|
|
log_info "Cloning PicoBrew Server repository..."
|
|
ensure_dir "$(dirname "$REPO_PATH")"
|
|
git clone "$REPO_URL" "$REPO_PATH"
|
|
fi
|
|
}
|
|
|
|
# Create Python LXC rootfs from Alpine
|
|
lxc_create_rootfs() {
|
|
local rootfs="$LXC_ROOTFS"
|
|
local arch=$(uname -m)
|
|
|
|
log_info "Creating Alpine rootfs for PicoBrew..."
|
|
|
|
ensure_dir "$rootfs"
|
|
|
|
# Use Alpine mini rootfs
|
|
local alpine_version="3.19"
|
|
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 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
|
|
cat > "$rootfs/opt/start-picobrew.sh" << 'STARTUP'
|
|
#!/bin/sh
|
|
set -e
|
|
|
|
# Install Python and dependencies on first run
|
|
if [ ! -f /opt/.installed ]; then
|
|
echo "Installing Python and dependencies..."
|
|
apk update
|
|
apk add --no-cache python3 py3-pip
|
|
|
|
# Install picobrew_server with compatible dependency versions
|
|
# Flask 1.1.2 requires old versions of Jinja2, markupsafe, itsdangerous, werkzeug
|
|
echo "Installing compatible dependencies..."
|
|
pip3 install --break-system-packages \
|
|
"Jinja2==2.11.3" \
|
|
"markupsafe==1.1.1" \
|
|
"itsdangerous==1.1.0" \
|
|
"Werkzeug==1.0.1" \
|
|
"click==7.1.2" \
|
|
2>/dev/null || \
|
|
pip3 install \
|
|
"Jinja2==2.11.3" \
|
|
"markupsafe==1.1.1" \
|
|
"itsdangerous==1.1.0" \
|
|
"Werkzeug==1.0.1" \
|
|
"click==7.1.2" \
|
|
2>/dev/null || true
|
|
|
|
echo "Installing picobrew_server package..."
|
|
pip3 install --break-system-packages --no-deps picobrew_server 2>/dev/null || \
|
|
pip3 install --no-deps picobrew_server 2>/dev/null || true
|
|
|
|
# Install remaining deps (pybeerxml<2.0 required for Parser import)
|
|
pip3 install --break-system-packages Flask==1.1.2 Flask-Cors==3.0.8 "pybeerxml<2.0.0" webargs==6.0.0 "marshmallow<4.0.0" 2>/dev/null || \
|
|
pip3 install Flask==1.1.2 Flask-Cors==3.0.8 "pybeerxml<2.0.0" webargs==6.0.0 "marshmallow<4.0.0" 2>/dev/null || true
|
|
|
|
touch /opt/.installed
|
|
fi
|
|
|
|
# Start PicoBrew Server (use pip-installed package, not /app)
|
|
cd /
|
|
export FLASK_APP=picobrew_server
|
|
export FLASK_ENV=production
|
|
export PICOBREW_HOST="${PICOBREW_HOST:-0.0.0.0}"
|
|
export PICOBREW_PORT="${PICOBREW_PORT:-8080}"
|
|
export PICOBREW_LOG_LEVEL="${PICOBREW_LOG_LEVEL:-INFO}"
|
|
|
|
echo "Starting PicoBrew Server on ${PICOBREW_HOST}:${PICOBREW_PORT}..."
|
|
exec python3 -m flask run --host="$PICOBREW_HOST" --port="$PICOBREW_PORT"
|
|
STARTUP
|
|
chmod +x "$rootfs/opt/start-picobrew.sh"
|
|
|
|
log_info "Rootfs created successfully"
|
|
return 0
|
|
}
|
|
|
|
# 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
|
|
|
|
cat > "$LXC_CONFIG" << EOF
|
|
# PicoBrew Server LXC Configuration
|
|
lxc.uts.name = $LXC_NAME
|
|
lxc.rootfs.path = dir:$LXC_ROOTFS
|
|
lxc.arch = $(uname -m)
|
|
|
|
# Network: use host network for device discovery
|
|
lxc.net.0.type = none
|
|
|
|
# Mount points
|
|
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed
|
|
lxc.mount.entry = $data_path/recipes srv/recipes none bind,create=dir 0 0
|
|
lxc.mount.entry = $data_path/sessions srv/sessions none bind,create=dir 0 0
|
|
lxc.mount.entry = $data_path/logs srv/logs none bind,create=dir 0 0
|
|
|
|
# Environment
|
|
lxc.environment = PICOBREW_HOST=$http_host
|
|
lxc.environment = PICOBREW_PORT=$http_port
|
|
lxc.environment = PICOBREW_LOG_LEVEL=$log_level
|
|
lxc.environment = PICOBREW_UNITS=$units
|
|
|
|
# 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-picobrew.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 PicoBrew 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: picobrewctl install"
|
|
return 1
|
|
fi
|
|
|
|
# Regenerate config in case settings changed
|
|
lxc_create_config
|
|
|
|
log_info "Starting PicoBrew container..."
|
|
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
|
|
}
|
|
|
|
# Commands
|
|
cmd_install() {
|
|
require_root
|
|
load_config
|
|
|
|
log_info "Installing PicoBrew Server..."
|
|
|
|
lxc_check_prereqs || exit 1
|
|
|
|
# Clone repository
|
|
repo_clone || exit 1
|
|
|
|
# Create container
|
|
if ! lxc_exists; then
|
|
lxc_create_rootfs || exit 1
|
|
fi
|
|
|
|
lxc_create_config || exit 1
|
|
|
|
# Enable service
|
|
uci_set main.enabled '1'
|
|
/etc/init.d/picobrew enable 2>/dev/null || true
|
|
|
|
log_info "Installation complete!"
|
|
log_info ""
|
|
log_info "Start with: /etc/init.d/picobrew start"
|
|
log_info "Web interface: http://<router-ip>:$http_port"
|
|
}
|
|
|
|
cmd_uninstall() {
|
|
require_root
|
|
|
|
log_info "Uninstalling PicoBrew Server..."
|
|
|
|
# Stop service
|
|
/etc/init.d/picobrew stop 2>/dev/null || true
|
|
/etc/init.d/picobrew disable 2>/dev/null || true
|
|
|
|
lxc_stop
|
|
|
|
# Remove container (keep data)
|
|
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 "PicoBrew Server uninstalled"
|
|
log_info "Data preserved in: $(uci_get main.data_path)"
|
|
}
|
|
|
|
cmd_update() {
|
|
require_root
|
|
load_config
|
|
|
|
log_info "Updating PicoBrew Server..."
|
|
|
|
# Update repo
|
|
repo_clone || exit 1
|
|
|
|
# Recreate container to get fresh dependencies
|
|
lxc_stop
|
|
if [ -d "$LXC_ROOTFS" ]; then
|
|
rm -rf "$LXC_ROOTFS"
|
|
fi
|
|
lxc_create_rootfs || exit 1
|
|
|
|
# Restart if was running
|
|
if [ "$(uci_get main.enabled)" = "1" ]; then
|
|
/etc/init.d/picobrew restart
|
|
fi
|
|
|
|
log_info "Update complete!"
|
|
}
|
|
|
|
cmd_status() {
|
|
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
|
|
PicoBrew Server 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
|
|
|
|
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
|
|
|
|
if [ -d "$data_path/logs" ]; then
|
|
if [ -n "$(ls -A "$data_path/logs" 2>/dev/null)" ]; then
|
|
tail -f "$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
|
|
}
|
|
|
|
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 "$@" ;;
|
|
service-run) shift; cmd_service_run "$@" ;;
|
|
service-stop) shift; cmd_service_stop "$@" ;;
|
|
*) usage ;;
|
|
esac
|