feat(localai,ollama): Switch LocalAI to Docker and add Ollama package

LocalAI changes:
- Rewrite localaictl to use Docker/Podman instead of standalone binary
- Use localai/localai:v2.25.0-ffmpeg image with all backends included
- Fix llama-cpp backend not found issue
- Auto-detect podman or docker runtime
- Update UCI config with Docker settings

New Ollama package:
- Add secubox-app-ollama as lighter alternative to LocalAI
- Native ARM64 support with backends included
- Simple CLI: ollamactl pull/run/list
- Docker image ~1GB vs 2-4GB for LocalAI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-21 17:56:40 +01:00
parent 63c0bb3e5a
commit b245fdb3e7
8 changed files with 886 additions and 290 deletions

View File

@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-localai PKG_NAME:=luci-app-localai
PKG_VERSION:=0.1.0 PKG_VERSION:=0.1.0
PKG_RELEASE:=12 PKG_RELEASE:=13
PKG_ARCH:=all PKG_ARCH:=all
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0

View File

@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-localai PKG_NAME:=secubox-app-localai
PKG_RELEASE:=5 PKG_RELEASE:=6
PKG_VERSION:=0.1.0 PKG_VERSION:=0.1.0
PKG_ARCH:=all PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr> PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
@ -14,8 +14,8 @@ define Package/secubox-app-localai
CATEGORY:=Utilities CATEGORY:=Utilities
PKGARCH:=all PKGARCH:=all
SUBMENU:=SecuBox Apps SUBMENU:=SecuBox Apps
TITLE:=SecuBox LocalAI - Self-hosted LLM (LXC) TITLE:=SecuBox LocalAI - Self-hosted LLM (Docker)
DEPENDS:=+uci +libuci +jsonfilter DEPENDS:=+uci +libuci +jsonfilter +wget-ssl
endef endef
define Package/secubox-app-localai/description define Package/secubox-app-localai/description
@ -25,10 +25,10 @@ Features:
- OpenAI-compatible API (drop-in replacement) - OpenAI-compatible API (drop-in replacement)
- No cloud dependency - all processing on-device - No cloud dependency - all processing on-device
- Support for various models (LLaMA, Mistral, Phi, etc.) - Support for various models (LLaMA, Mistral, Phi, etc.)
- All backends included (llama-cpp, whisper, etc.)
- Text generation, embeddings, transcription - Text generation, embeddings, transcription
- Image generation (optional)
Runs in LXC container for isolation. Runs in Docker/Podman container with all backends.
Configure in /etc/config/localai. Configure in /etc/config/localai.
endef endef
@ -54,17 +54,20 @@ define Package/secubox-app-localai/postinst
#!/bin/sh #!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || { [ -n "$${IPKG_INSTROOT}" ] || {
echo "" echo ""
echo "LocalAI installed." echo "LocalAI installed (Docker/Podman version)."
echo ""
echo "Prerequisites: Install podman or docker first"
echo " opkg install podman"
echo "" echo ""
echo "To install and start LocalAI:" echo "To install and start LocalAI:"
echo " localaictl install" echo " localaictl install # Pull Docker image (~2-4GB)"
echo " /etc/init.d/localai start" echo " /etc/init.d/localai start"
echo "" echo ""
echo "API endpoint: http://<router-ip>:8080/v1" echo "API endpoint: http://<router-ip>:8080/v1"
echo "Web UI: http://<router-ip>:8080" echo "Web UI: http://<router-ip>:8080"
echo "" echo ""
echo "Download models with:" echo "Download models with:"
echo " localaictl model-install <model-name>" echo " localaictl model-install tinyllama"
echo "" echo ""
} }
exit 0 exit 0

View File

@ -4,24 +4,30 @@ config main 'main'
option api_host '0.0.0.0' option api_host '0.0.0.0'
option data_path '/srv/localai' option data_path '/srv/localai'
option models_path '/srv/localai/models' option models_path '/srv/localai/models'
option memory_limit '2G' option memory_limit '2g'
option threads '4' option threads '4'
option context_size '2048' option context_size '2048'
option debug '0' option debug '0'
option cors '1' option cors '1'
# GPU settings (experimental on ARM64) # Docker/Podman settings
config gpu 'gpu' config docker 'docker'
option enabled '0' option image 'localai/localai:v2.25.0-ffmpeg'
option backend 'vulkan'
# Default model to load on startup # Default model to load on startup
config model 'default' config model 'default'
option enabled '1' option enabled '1'
option name 'phi-2' option name 'tinyllama'
option backend 'llama-cpp' option backend 'llama-cpp'
# Model presets # Model presets - GGUF format for llama-cpp backend
config preset 'tinyllama'
option name 'tinyllama'
option url 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf'
option size '669M'
option type 'text-generation'
option description 'TinyLlama 1.1B - Ultra-lightweight'
config preset 'phi2' config preset 'phi2'
option name 'phi-2' option name 'phi-2'
option url 'https://huggingface.co/TheBloke/phi-2-GGUF/resolve/main/phi-2.Q4_K_M.gguf' option url 'https://huggingface.co/TheBloke/phi-2-GGUF/resolve/main/phi-2.Q4_K_M.gguf'
@ -36,13 +42,6 @@ config preset 'mistral'
option type 'text-generation' option type 'text-generation'
option description 'Mistral 7B Instruct - High quality assistant' option description 'Mistral 7B Instruct - High quality assistant'
config preset 'tinyllama'
option name 'tinyllama'
option url 'https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf'
option size '669M'
option type 'text-generation'
option description 'TinyLlama 1.1B - Ultra-lightweight'
config preset 'gte_small' config preset 'gte_small'
option name 'gte-small' option name 'gte-small'
option url 'https://huggingface.co/Supabase/gte-small/resolve/main/model.onnx' option url 'https://huggingface.co/Supabase/gte-small/resolve/main/model.onnx'

View File

@ -1,25 +1,27 @@
#!/bin/sh #!/bin/sh
# SecuBox LocalAI manager - LXC container support # SecuBox LocalAI manager - Docker/Podman container support
# Copyright (C) 2025 CyberMind.fr # Copyright (C) 2025 CyberMind.fr
#
# Uses LocalAI Docker image with all backends included (llama-cpp, etc.)
CONFIG="localai" CONFIG="localai"
LXC_NAME="localai" CONTAINER_NAME="localai"
OPKG_UPDATED=0 LOCALAI_VERSION="v2.25.0"
LOCALAI_VERSION="v3.10.0" # Docker image with all backends included
LOCALAI_IMAGE="localai/localai:${LOCALAI_VERSION}-ffmpeg"
# Paths # Paths
LXC_PATH="/srv/lxc" DATA_PATH="/srv/localai"
LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" MODELS_PATH="/srv/localai/models"
LXC_CONFIG="$LXC_PATH/$LXC_NAME/config"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: localaictl <command> Usage: localaictl <command>
Commands: Commands:
install Install prerequisites and create LXC container install Pull Docker image and setup LocalAI
check Run prerequisite checks check Run prerequisite checks
update Update LocalAI in container update Update LocalAI Docker image
status Show container and service status status Show container and service status
logs Show LocalAI logs (use -f to follow) logs Show LocalAI logs (use -f to follow)
shell Open shell in container shell Open shell in container
@ -28,8 +30,6 @@ Model Management:
models List installed models models List installed models
model-install <n> Install model from preset or URL model-install <n> Install model from preset or URL
model-remove <n> Remove installed model model-remove <n> Remove installed model
model-load <n> Load model into memory
model-unload <n> Unload model from memory
Service Control: Service Control:
service-run Internal: run container under procd service-run Internal: run container under procd
@ -48,13 +48,12 @@ EOF
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; } require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
log_info() { echo "[INFO] $*"; } log_info() { echo "[INFO] $*"; logger -t localai "$*"; }
log_warn() { echo "[WARN] $*" >&2; } log_warn() { echo "[WARN] $*" >&2; logger -t localai -p warning "$*"; }
log_error() { echo "[ERROR] $*" >&2; } log_error() { echo "[ERROR] $*" >&2; logger -t localai -p err "$*"; }
uci_get() { uci -q get ${CONFIG}.$1; } uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
uci_get_list() { uci -q get ${CONFIG}.$1 2>/dev/null; }
# Load configuration with defaults # Load configuration with defaults
load_config() { load_config() {
@ -62,235 +61,133 @@ load_config() {
api_host="$(uci_get main.api_host || echo 0.0.0.0)" api_host="$(uci_get main.api_host || echo 0.0.0.0)"
data_path="$(uci_get main.data_path || echo /srv/localai)" data_path="$(uci_get main.data_path || echo /srv/localai)"
models_path="$(uci_get main.models_path || echo /srv/localai/models)" models_path="$(uci_get main.models_path || echo /srv/localai/models)"
memory_limit="$(uci_get main.memory_limit || echo 2G)" memory_limit="$(uci_get main.memory_limit || echo 2g)"
threads="$(uci_get main.threads || echo 4)" threads="$(uci_get main.threads || echo 4)"
context_size="$(uci_get main.context_size || echo 2048)" context_size="$(uci_get main.context_size || echo 2048)"
debug="$(uci_get main.debug || echo 0)" debug="$(uci_get main.debug || echo 0)"
cors="$(uci_get main.cors || echo 1)" cors="$(uci_get main.cors || echo 1)"
gpu_enabled="$(uci_get gpu.enabled || echo 0)"
gpu_backend="$(uci_get gpu.backend || echo vulkan)"
}
ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } # Docker settings
docker_image="$(uci_get docker.image || echo $LOCALAI_IMAGE)"
has_lxc() { # Ensure paths exist
command -v lxc-start >/dev/null 2>&1 && \ [ -d "$data_path" ] || mkdir -p "$data_path"
command -v lxc-stop >/dev/null 2>&1 [ -d "$models_path" ] || mkdir -p "$models_path"
}
# Ensure required packages are installed
ensure_packages() {
require_root
for pkg in "$@"; do
if ! opkg list-installed | grep -q "^$pkg "; then
if [ "$OPKG_UPDATED" -eq 0 ]; then
opkg update || return 1
OPKG_UPDATED=1
fi
opkg install "$pkg" || return 1
fi
done
} }
# ============================================================================= # =============================================================================
# LXC CONTAINER FUNCTIONS # CONTAINER RUNTIME DETECTION
# ============================================================================= # =============================================================================
lxc_check_prereqs() { # Detect available container runtime (podman preferred, then docker)
log_info "Checking LXC prerequisites..." detect_runtime() {
ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1 if command -v podman >/dev/null 2>&1; then
RUNTIME="podman"
if [ ! -d /sys/fs/cgroup ]; then elif command -v docker >/dev/null 2>&1; then
log_error "cgroups not mounted at /sys/fs/cgroup" RUNTIME="docker"
return 1
fi
log_info "LXC ready"
}
lxc_create_rootfs() {
load_config
if [ -d "$LXC_ROOTFS" ] && [ -x "$LXC_ROOTFS/usr/bin/local-ai" ]; then
log_info "LXC rootfs already exists with LocalAI"
return 0
fi
log_info "Creating LXC rootfs for LocalAI..."
ensure_dir "$LXC_PATH/$LXC_NAME"
lxc_download_binary || return 1
lxc_create_config || return 1
log_info "LXC rootfs created successfully"
}
lxc_download_binary() {
local rootfs="$LXC_ROOTFS"
local arch
# Detect architecture - LocalAI uses lowercase format: local-ai-vX.X.X-linux-arm64
case "$(uname -m)" in
x86_64) arch="linux-x86_64" ;;
aarch64) arch="linux-arm64" ;;
armv7l) arch="linux-arm" ;;
*) arch="linux-x86_64" ;;
esac
log_info "Downloading LocalAI $LOCALAI_VERSION for $arch..."
ensure_dir "$rootfs/usr/bin"
ensure_dir "$rootfs/data"
ensure_dir "$rootfs/models"
ensure_dir "$rootfs/tmp"
ensure_dir "$rootfs/etc"
# Download LocalAI binary - format: local-ai-v3.10.0-linux-arm64
local binary_url="https://github.com/mudler/LocalAI/releases/download/${LOCALAI_VERSION}/local-ai-${LOCALAI_VERSION}-${arch}"
log_info "Downloading from: $binary_url"
if wget -q --show-progress -O "$rootfs/usr/bin/local-ai" "$binary_url"; then
chmod +x "$rootfs/usr/bin/local-ai"
log_info "LocalAI binary downloaded successfully ($(ls -sh "$rootfs/usr/bin/local-ai" | cut -d' ' -f1))"
else else
log_error "Failed to download LocalAI binary" RUNTIME=""
log_error "URL: $binary_url" fi
echo "$RUNTIME"
}
has_runtime() {
[ -n "$(detect_runtime)" ]
}
run_container() {
local runtime=$(detect_runtime)
[ -z "$runtime" ] && { log_error "No container runtime found"; return 1; }
$runtime "$@"
}
# =============================================================================
# CONTAINER MANAGEMENT
# =============================================================================
container_exists() {
run_container ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
}
container_running() {
run_container ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
}
container_stop() {
if container_running; then
log_info "Stopping container..."
run_container stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if container_exists; then
run_container rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
}
container_pull() {
load_config
log_info "Pulling LocalAI Docker image: $docker_image"
log_info "This may take several minutes (image is ~2-4GB)..."
if run_container pull "$docker_image"; then
log_info "Image pulled successfully"
return 0
else
log_error "Failed to pull image"
return 1 return 1
fi fi
# Create minimal rootfs structure
mkdir -p "$rootfs/bin" "$rootfs/lib" "$rootfs/proc" "$rootfs/sys" "$rootfs/dev"
# Create resolv.conf
echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf"
# Create startup script
cat > "$rootfs/usr/bin/start-localai.sh" << 'START'
#!/bin/sh
export PATH="/usr/bin:/bin:$PATH"
cd /data
# Read environment variables
API_PORT="${LOCALAI_API_PORT:-8080}"
API_HOST="${LOCALAI_API_HOST:-0.0.0.0}"
THREADS="${LOCALAI_THREADS:-4}"
CONTEXT_SIZE="${LOCALAI_CONTEXT_SIZE:-2048}"
DEBUG="${LOCALAI_DEBUG:-0}"
CORS="${LOCALAI_CORS:-1}"
GPU_ENABLED="${LOCALAI_GPU_ENABLED:-0}"
# Build args
ARGS="--address ${API_HOST}:${API_PORT}"
ARGS="$ARGS --models-path /models"
ARGS="$ARGS --threads $THREADS"
ARGS="$ARGS --context-size $CONTEXT_SIZE"
[ "$DEBUG" = "1" ] && ARGS="$ARGS --debug"
[ "$CORS" = "1" ] && ARGS="$ARGS --cors"
echo "Starting LocalAI..."
echo "API: http://${API_HOST}:${API_PORT}"
echo "Models path: /models"
echo "Threads: $THREADS, Context: $CONTEXT_SIZE"
exec /usr/bin/local-ai $ARGS
START
chmod +x "$rootfs/usr/bin/start-localai.sh"
log_info "LocalAI binary and startup script installed"
} }
lxc_create_config() { container_run() {
load_config load_config
container_stop
# Build command line flags log_info "Starting LocalAI container..."
local cors_flag="" log_info "Image: $docker_image"
local debug_flag="" log_info "API: http://${api_host}:${api_port}"
[ "$cors" = "1" ] && cors_flag=" --cors" log_info "Models: $models_path"
[ "$debug" = "1" ] && debug_flag=" --debug"
cat > "$LXC_CONFIG" << EOF # Build environment variables
# LocalAI LXC Configuration local env_args=""
lxc.uts.name = $LXC_NAME env_args="$env_args -e LOCALAI_THREADS=$threads"
env_args="$env_args -e LOCALAI_CONTEXT_SIZE=$context_size"
[ "$debug" = "1" ] && env_args="$env_args -e LOCALAI_DEBUG=true"
[ "$cors" = "1" ] && env_args="$env_args -e LOCALAI_CORS=true"
# Root filesystem # Run container in foreground (for procd)
lxc.rootfs.path = dir:$LXC_ROOTFS exec run_container run --rm \
--name "$CONTAINER_NAME" \
# Network - use host network for simplicity -p "${api_port}:8080" \
lxc.net.0.type = none -v "${models_path}:/models:rw" \
-v "${data_path}:/build:rw" \
# Mounts --memory="$memory_limit" \
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed $env_args \
lxc.mount.entry = $data_path data none bind,create=dir 0 0 "$docker_image"
lxc.mount.entry = $models_path models none bind,create=dir 0 0
# Environment variables for configuration
lxc.environment = LOCALAI_API_PORT=$api_port
lxc.environment = LOCALAI_API_HOST=$api_host
lxc.environment = LOCALAI_THREADS=$threads
lxc.environment = LOCALAI_CONTEXT_SIZE=$context_size
lxc.environment = LOCALAI_DEBUG=$debug
lxc.environment = LOCALAI_CORS=$cors
lxc.environment = LOCALAI_GPU_ENABLED=$gpu_enabled
# Capabilities
lxc.cap.drop = sys_admin sys_module mac_admin mac_override
# cgroups limits
lxc.cgroup.memory.limit_in_bytes = $memory_limit
# Run binary directly (no shell needed in minimal rootfs)
lxc.init.cmd = /usr/bin/local-ai --address ${api_host}:${api_port} --models-path /models --threads $threads --context-size $context_size${cors_flag}${debug_flag}
# Console
lxc.console.size = 4096
lxc.pty.max = 1024
EOF
log_info "LXC config created at $LXC_CONFIG"
} }
lxc_stop() { container_status() {
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then
lxc-stop -n "$LXC_NAME" -k >/dev/null 2>&1 || true
fi
}
lxc_run() {
load_config load_config
lxc_stop local runtime=$(detect_runtime)
if [ ! -f "$LXC_CONFIG" ]; then
log_error "LXC not configured. Run 'localaictl install' first."
return 1
fi
# Regenerate config to pick up any UCI changes
lxc_create_config
# Ensure mount points exist
ensure_dir "$data_path"
ensure_dir "$models_path"
log_info "Starting LocalAI LXC container..."
log_info "API endpoint: http://0.0.0.0:$api_port/v1"
log_info "Web UI: http://0.0.0.0:$api_port"
log_info "Models path: $models_path"
exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG"
}
lxc_status() {
load_config
echo "=== LocalAI Status ===" echo "=== LocalAI Status ==="
echo "" echo ""
echo "Container Runtime: ${runtime:-NOT FOUND}"
echo ""
if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then if [ -n "$runtime" ]; then
lxc-info -n "$LXC_NAME" if container_running; then
else echo "Container Status: RUNNING"
echo "LXC container '$LXC_NAME' not found or not configured" echo ""
run_container ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
elif container_exists; then
echo "Container Status: STOPPED"
else
echo "Container Status: NOT CREATED"
fi
fi fi
echo "" echo ""
echo "=== Configuration ===" echo "=== Configuration ==="
echo "Image: $docker_image"
echo "API port: $api_port" echo "API port: $api_port"
echo "Data path: $data_path" echo "Data path: $data_path"
echo "Models path: $models_path" echo "Models path: $models_path"
@ -302,29 +199,31 @@ lxc_status() {
# Check API health # Check API health
if wget -q -O - "http://127.0.0.1:$api_port/readyz" 2>/dev/null | grep -q "ok"; then if wget -q -O - "http://127.0.0.1:$api_port/readyz" 2>/dev/null | grep -q "ok"; then
echo "API Status: HEALTHY" echo "API Status: HEALTHY"
# List loaded models via API
echo ""
echo "=== Loaded Models (via API) ==="
local models=$(wget -q -O - "http://127.0.0.1:$api_port/v1/models" 2>/dev/null)
if [ -n "$models" ]; then
echo "$models" | jsonfilter -e '@.data[*].id' 2>/dev/null | while read model; do
echo " - $model"
done
fi
else else
echo "API Status: NOT RESPONDING" echo "API Status: NOT RESPONDING"
fi fi
} }
lxc_logs() { container_logs() {
if [ "$1" = "-f" ]; then if [ "$1" = "-f" ]; then
logread -f -e localai run_container logs -f "$CONTAINER_NAME"
else else
logread -e localai | tail -100 run_container logs --tail 100 "$CONTAINER_NAME"
fi fi
} }
lxc_shell() { container_shell() {
lxc-attach -n "$LXC_NAME" -- /bin/sh run_container exec -it "$CONTAINER_NAME" /bin/sh
}
lxc_destroy() {
lxc_stop
if [ -d "$LXC_PATH/$LXC_NAME" ]; then
rm -rf "$LXC_PATH/$LXC_NAME"
log_info "LXC container destroyed"
fi
} }
# ============================================================================= # =============================================================================
@ -346,12 +245,19 @@ cmd_models() {
echo " $count. $name ($size)" echo " $count. $name ($size)"
done done
# Also check for yaml configs (gallery models)
for yaml in "$models_path"/*.yaml; do
[ -f "$yaml" ] || continue
local name=$(basename "$yaml" .yaml)
echo " - $name (config)"
done
if [ "$count" -eq 0 ]; then if [ "$count" -eq 0 ]; then
echo " No models installed" echo " No models installed"
echo "" echo ""
echo "Install a model with:" echo "Install a model with:"
echo " localaictl model-install phi2"
echo " localaictl model-install tinyllama" echo " localaictl model-install tinyllama"
echo " localaictl model-install phi2"
fi fi
else else
echo " Models directory not found: $models_path" echo " Models directory not found: $models_path"
@ -362,7 +268,7 @@ cmd_models() {
echo "" echo ""
# List presets from UCI config # List presets from UCI config
uci show localai 2>/dev/null | grep "preset\[" | while read line; do uci show localai 2>/dev/null | grep "=preset" | while read line; do
local section=$(echo "$line" | cut -d. -f2 | cut -d= -f1) local section=$(echo "$line" | cut -d. -f2 | cut -d= -f1)
local name=$(uci_get "$section.name") local name=$(uci_get "$section.name")
local desc=$(uci_get "$section.description") local desc=$(uci_get "$section.description")
@ -378,13 +284,13 @@ cmd_model_install() {
local model_name="$1" local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: localaictl model-install <preset-name|url>"; return 1; } [ -z "$model_name" ] && { echo "Usage: localaictl model-install <preset-name|url>"; return 1; }
ensure_dir "$models_path" mkdir -p "$models_path"
# Check if it's a preset # Check if it's a preset
local preset_url="" local preset_url=""
local preset_file="" local preset_file=""
# Search presets # Search presets in UCI
for section in $(uci show localai 2>/dev/null | grep "=preset" | cut -d. -f2 | cut -d= -f1); do for section in $(uci show localai 2>/dev/null | grep "=preset" | cut -d. -f2 | cut -d= -f1); do
local pname=$(uci_get "$section.name") local pname=$(uci_get "$section.name")
if [ "$pname" = "$model_name" ]; then if [ "$pname" = "$model_name" ]; then
@ -397,11 +303,12 @@ cmd_model_install() {
if [ -n "$preset_url" ]; then if [ -n "$preset_url" ]; then
log_info "Installing preset model: $model_name" log_info "Installing preset model: $model_name"
log_info "URL: $preset_url" log_info "URL: $preset_url"
log_info "This may take a while depending on model size..."
if wget -O "$models_path/$preset_file" "$preset_url"; then if wget --show-progress -O "$models_path/$preset_file" "$preset_url"; then
log_info "Model installed: $models_path/$preset_file" log_info "Model downloaded: $models_path/$preset_file"
# Create model config YAML # Create model config YAML for LocalAI
cat > "$models_path/$model_name.yaml" << EOF cat > "$models_path/$model_name.yaml" << EOF
name: $model_name name: $model_name
backend: llama-cpp backend: llama-cpp
@ -411,6 +318,9 @@ context_size: $context_size
threads: $threads threads: $threads
EOF EOF
log_info "Model config created: $models_path/$model_name.yaml" log_info "Model config created: $models_path/$model_name.yaml"
log_info ""
log_info "Model '$model_name' installed successfully!"
log_info "Restart LocalAI to load: /etc/init.d/localai restart"
else else
log_error "Failed to download model" log_error "Failed to download model"
return 1 return 1
@ -420,7 +330,7 @@ EOF
local filename=$(basename "$model_name") local filename=$(basename "$model_name")
log_info "Downloading model from URL..." log_info "Downloading model from URL..."
if wget -O "$models_path/$filename" "$model_name"; then if wget --show-progress -O "$models_path/$filename" "$model_name"; then
log_info "Model installed: $models_path/$filename" log_info "Model installed: $models_path/$filename"
else else
log_error "Failed to download model" log_error "Failed to download model"
@ -428,13 +338,13 @@ EOF
fi fi
else else
log_error "Unknown model or preset: $model_name" log_error "Unknown model or preset: $model_name"
# List available presets from UCI echo ""
local presets="" echo "Available presets:"
for section in $(uci show localai 2>/dev/null | grep "=preset" | cut -d. -f2 | cut -d= -f1); do uci show localai 2>/dev/null | grep "=preset" | while read line; do
local section=$(echo "$line" | cut -d. -f2 | cut -d= -f1)
local pname=$(uci_get "$section.name") local pname=$(uci_get "$section.name")
[ -n "$pname" ] && presets="$presets $pname" [ -n "$pname" ] && echo " - $pname"
done done
log_info "Available presets:$presets"
return 1 return 1
fi fi
} }
@ -457,7 +367,20 @@ cmd_model_remove() {
fi fi
done done
[ "$found" -eq 0 ] && log_warn "Model not found: $model_name" # Also try to match partial names (model file might have different name)
for file in "$models_path"/*"$model_name"*; do
if [ -f "$file" ]; then
rm -f "$file"
log_info "Removed: $file"
found=1
fi
done
if [ "$found" -eq 0 ]; then
log_warn "Model not found: $model_name"
else
log_info "Restart LocalAI to apply: /etc/init.d/localai restart"
fi
} }
# ============================================================================= # =============================================================================
@ -468,26 +391,34 @@ cmd_install() {
require_root require_root
load_config load_config
if ! has_lxc; then if ! has_runtime; then
log_error "LXC not available. Install lxc packages first." log_error "No container runtime found!"
log_error "Install podman or docker first:"
log_error " opkg update && opkg install podman"
exit 1 exit 1
fi fi
log_info "Installing LocalAI..." local runtime=$(detect_runtime)
log_info "Installing LocalAI using $runtime..."
log_info "Image: $docker_image"
# Create directories # Create directories
ensure_dir "$data_path" mkdir -p "$data_path"
ensure_dir "$models_path" mkdir -p "$models_path"
lxc_check_prereqs || exit 1 # Pull the image
lxc_create_rootfs || exit 1 container_pull || exit 1
# Enable service
uci_set main.enabled '1' uci_set main.enabled '1'
/etc/init.d/localai enable /etc/init.d/localai enable
log_info "LocalAI installed." log_info ""
log_info "LocalAI installed successfully!"
log_info ""
log_info "Start with: /etc/init.d/localai start" log_info "Start with: /etc/init.d/localai start"
log_info "API endpoint: http://<router-ip>:$api_port/v1" log_info "API endpoint: http://<router-ip>:$api_port/v1"
log_info "Web UI: http://<router-ip>:$api_port"
log_info "" log_info ""
log_info "Install a model to get started:" log_info "Install a model to get started:"
log_info " localaictl model-install tinyllama # Lightweight (669MB)" log_info " localaictl model-install tinyllama # Lightweight (669MB)"
@ -497,21 +428,48 @@ cmd_install() {
cmd_check() { cmd_check() {
load_config load_config
log_info "Checking prerequisites..." echo "=== Prerequisite Check ==="
if has_lxc; then echo ""
log_info "LXC: available"
lxc_check_prereqs # Check container runtime
local runtime=$(detect_runtime)
if [ -n "$runtime" ]; then
echo "[OK] Container runtime: $runtime"
$runtime --version 2>/dev/null | head -1
else else
log_warn "LXC: not available" echo "[FAIL] No container runtime found"
echo " Install: opkg install podman"
fi fi
echo ""
# Check storage
local storage_path=$(dirname "$data_path")
local storage_avail=$(df -h "$storage_path" 2>/dev/null | tail -1 | awk '{print $4}')
echo "Storage available: $storage_avail (at $storage_path)"
echo " Note: LocalAI image requires ~2-4GB"
echo " Models require 500MB-8GB each"
echo ""
# Check memory # Check memory
local mem_total=$(grep MemTotal /proc/meminfo | awk '{print $2}') local mem_total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
local mem_gb=$((mem_total / 1024 / 1024)) local mem_gb=$((mem_total / 1024 / 1024))
log_info "System memory: ${mem_gb}GB" echo "System memory: ${mem_gb}GB"
if [ "$mem_gb" -lt 2 ]; then if [ "$mem_gb" -lt 2 ]; then
log_warn "Low memory! LocalAI requires at least 2GB RAM for most models" echo "[WARN] Low memory! LocalAI requires at least 2GB RAM"
else
echo "[OK] Memory sufficient"
fi
echo ""
# Check if image exists
if [ -n "$runtime" ]; then
if $runtime images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "localai"; then
echo "[OK] LocalAI image found"
else
echo "[INFO] LocalAI image not downloaded yet"
echo " Run: localaictl install"
fi
fi fi
} }
@ -520,44 +478,52 @@ cmd_update() {
load_config load_config
log_info "Updating LocalAI..." log_info "Updating LocalAI..."
lxc_destroy
lxc_create_rootfs || exit 1
if /etc/init.d/localai enabled >/dev/null 2>&1; then # Stop if running
container_stop
# Pull latest image
container_pull || exit 1
# Restart if was enabled
if [ "$(uci_get main.enabled)" = "1" ]; then
/etc/init.d/localai restart /etc/init.d/localai restart
else else
log_info "Update complete. Restart manually to apply." log_info "Update complete. Start manually with: /etc/init.d/localai start"
fi fi
} }
cmd_status() { cmd_status() {
lxc_status container_status
} }
cmd_logs() { cmd_logs() {
lxc_logs "$@" container_logs "$@"
} }
cmd_shell() { cmd_shell() {
lxc_shell if ! container_running; then
log_error "Container not running. Start with: /etc/init.d/localai start"
exit 1
fi
container_shell
} }
cmd_service_run() { cmd_service_run() {
require_root require_root
load_config load_config
if ! has_lxc; then if ! has_runtime; then
log_error "LXC not available" log_error "No container runtime found"
exit 1 exit 1
fi fi
lxc_check_prereqs || exit 1 container_run
lxc_run
} }
cmd_service_stop() { cmd_service_stop() {
require_root require_root
lxc_stop container_stop
} }
# Main Entry Point # Main Entry Point

View File

@ -0,0 +1,76 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-ollama
PKG_RELEASE:=1
PKG_VERSION:=0.1.0
PKG_ARCH:=all
PKG_MAINTAINER:=CyberMind Studio <contact@cybermind.fr>
PKG_LICENSE:=MIT
include $(INCLUDE_DIR)/package.mk
define Package/secubox-app-ollama
SECTION:=utils
CATEGORY:=Utilities
PKGARCH:=all
SUBMENU:=SecuBox Apps
TITLE:=SecuBox Ollama - Local LLM Runtime
DEPENDS:=+uci +libuci +jsonfilter +wget-ssl
endef
define Package/secubox-app-ollama/description
Ollama - Simple local LLM runtime for SecuBox-powered OpenWrt systems.
Features:
- Easy model management (ollama pull, ollama run)
- OpenAI-compatible API
- Native ARM64 support with backends included
- Lightweight compared to LocalAI
- Support for LLaMA, Mistral, Phi, Gemma models
Runs in Docker/Podman container.
Configure in /etc/config/ollama.
endef
define Package/secubox-app-ollama/conffiles
/etc/config/ollama
endef
define Build/Compile
endef
define Package/secubox-app-ollama/install
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./files/etc/config/ollama $(1)/etc/config/ollama
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./files/etc/init.d/ollama $(1)/etc/init.d/ollama
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) ./files/usr/sbin/ollamactl $(1)/usr/sbin/ollamactl
endef
define Package/secubox-app-ollama/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
echo ""
echo "Ollama installed."
echo ""
echo "Prerequisites: Install podman or docker first"
echo " opkg install podman"
echo ""
echo "To install and start Ollama:"
echo " ollamactl install # Pull Docker image (~1GB)"
echo " /etc/init.d/ollama start"
echo ""
echo "API endpoint: http://<router-ip>:11434/api"
echo ""
echo "Download and run models:"
echo " ollamactl pull tinyllama"
echo " ollamactl run tinyllama"
echo ""
}
exit 0
endef
$(eval $(call BuildPackage,secubox-app-ollama))

View File

@ -0,0 +1,50 @@
config main 'main'
option enabled '0'
option api_port '11434'
option api_host '0.0.0.0'
option data_path '/srv/ollama'
option memory_limit '2g'
# Docker/Podman settings
config docker 'docker'
option image 'ollama/ollama:latest'
# Default model to pull on install
config model 'default'
option enabled '1'
option name 'tinyllama'
# Available models (informational - managed by Ollama)
# Use: ollamactl pull <model> to download
# Lightweight models (< 2GB)
config model_info 'tinyllama'
option name 'tinyllama'
option size '637M'
option description 'TinyLlama 1.1B - Ultra-lightweight, fast responses'
config model_info 'phi'
option name 'phi'
option size '1.6G'
option description 'Microsoft Phi-2 - Small but capable'
config model_info 'gemma'
option name 'gemma:2b'
option size '1.4G'
option description 'Google Gemma 2B - Efficient and modern'
# Medium models (2-5GB)
config model_info 'mistral'
option name 'mistral'
option size '4.1G'
option description 'Mistral 7B - High quality general assistant'
config model_info 'llama2'
option name 'llama2'
option size '3.8G'
option description 'Meta LLaMA 2 7B - Popular general model'
config model_info 'codellama'
option name 'codellama'
option size '3.8G'
option description 'Code LLaMA - Specialized for coding tasks'

View File

@ -0,0 +1,40 @@
#!/bin/sh /etc/rc.common
# SecuBox Ollama - Local LLM runtime
# Copyright (C) 2025 CyberMind.fr
START=95
STOP=10
USE_PROCD=1
PROG=/usr/sbin/ollamactl
start_service() {
local enabled
config_load ollama
config_get enabled main enabled '0'
[ "$enabled" = "1" ] || {
echo "Ollama is disabled. Enable with: uci set ollama.main.enabled=1"
return 0
}
procd_open_instance
procd_set_param command $PROG service-run
procd_set_param respawn 3600 5 5
procd_set_param stdout 1
procd_set_param stderr 1
procd_close_instance
}
stop_service() {
$PROG service-stop
}
service_triggers() {
procd_add_reload_trigger "ollama"
}
reload_service() {
stop
start
}

View File

@ -0,0 +1,462 @@
#!/bin/sh
# SecuBox Ollama manager - Docker/Podman container support
# Copyright (C) 2025 CyberMind.fr
#
# Ollama is a simpler alternative to LocalAI with better ARM64 support
CONFIG="ollama"
CONTAINER_NAME="ollama"
OLLAMA_IMAGE="ollama/ollama:latest"
# Paths
DATA_PATH="/srv/ollama"
usage() {
cat <<'EOF'
Usage: ollamactl <command>
Commands:
install Pull Docker image and setup Ollama
check Run prerequisite checks
update Update Ollama Docker image
status Show container and service status
logs Show Ollama logs (use -f to follow)
shell Open shell in container
Model Management:
list List downloaded models
pull <model> Download a model (e.g., tinyllama, mistral, llama2)
rm <model> Remove a model
run <model> Interactive chat with a model (CLI)
Service Control:
service-run Internal: run container under procd
service-stop Stop container
API Endpoints (default port 11434):
/api/generate - Generate text completion
/api/chat - Chat completion
/api/embeddings - Generate embeddings
/api/tags - List models
Configuration: /etc/config/ollama
EOF
}
require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; }
log_info() { echo "[INFO] $*"; logger -t ollama "$*"; }
log_warn() { echo "[WARN] $*" >&2; logger -t ollama -p warning "$*"; }
log_error() { echo "[ERROR] $*" >&2; logger -t ollama -p err "$*"; }
uci_get() { uci -q get ${CONFIG}.$1; }
uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; }
# Load configuration with defaults
load_config() {
api_port="$(uci_get main.api_port || echo 11434)"
api_host="$(uci_get main.api_host || echo 0.0.0.0)"
data_path="$(uci_get main.data_path || echo /srv/ollama)"
memory_limit="$(uci_get main.memory_limit || echo 2g)"
# Docker settings
docker_image="$(uci_get docker.image || echo $OLLAMA_IMAGE)"
# Ensure paths exist
[ -d "$data_path" ] || mkdir -p "$data_path"
}
# =============================================================================
# CONTAINER RUNTIME DETECTION
# =============================================================================
detect_runtime() {
if command -v podman >/dev/null 2>&1; then
RUNTIME="podman"
elif command -v docker >/dev/null 2>&1; then
RUNTIME="docker"
else
RUNTIME=""
fi
echo "$RUNTIME"
}
has_runtime() {
[ -n "$(detect_runtime)" ]
}
run_container() {
local runtime=$(detect_runtime)
[ -z "$runtime" ] && { log_error "No container runtime found"; return 1; }
$runtime "$@"
}
# =============================================================================
# CONTAINER MANAGEMENT
# =============================================================================
container_exists() {
run_container ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
}
container_running() {
run_container ps --format '{{.Names}}' 2>/dev/null | grep -q "^${CONTAINER_NAME}$"
}
container_stop() {
if container_running; then
log_info "Stopping container..."
run_container stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
if container_exists; then
run_container rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
}
container_pull() {
load_config
log_info "Pulling Ollama Docker image: $docker_image"
log_info "This may take a few minutes (~1GB)..."
if run_container pull "$docker_image"; then
log_info "Image pulled successfully"
return 0
else
log_error "Failed to pull image"
return 1
fi
}
container_run() {
load_config
container_stop
log_info "Starting Ollama container..."
log_info "Image: $docker_image"
log_info "API: http://${api_host}:${api_port}"
log_info "Data: $data_path"
# Run container in foreground (for procd)
exec run_container run --rm \
--name "$CONTAINER_NAME" \
-p "${api_port}:11434" \
-v "${data_path}:/root/.ollama:rw" \
--memory="$memory_limit" \
"$docker_image"
}
container_status() {
load_config
local runtime=$(detect_runtime)
echo "=== Ollama Status ==="
echo ""
echo "Container Runtime: ${runtime:-NOT FOUND}"
echo ""
if [ -n "$runtime" ]; then
if container_running; then
echo "Container Status: RUNNING"
echo ""
run_container ps --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
elif container_exists; then
echo "Container Status: STOPPED"
else
echo "Container Status: NOT CREATED"
fi
fi
echo ""
echo "=== Configuration ==="
echo "Image: $docker_image"
echo "API port: $api_port"
echo "Data path: $data_path"
echo "Memory limit: $memory_limit"
echo ""
# Check API health
if wget -q -O /dev/null "http://127.0.0.1:$api_port" 2>/dev/null; then
echo "API Status: HEALTHY"
# List models via API
echo ""
echo "=== Downloaded Models ==="
local models=$(wget -q -O - "http://127.0.0.1:$api_port/api/tags" 2>/dev/null)
if [ -n "$models" ]; then
echo "$models" | jsonfilter -e '@.models[*].name' 2>/dev/null | while read model; do
echo " - $model"
done
else
echo " No models downloaded yet"
fi
else
echo "API Status: NOT RESPONDING"
fi
}
container_logs() {
if [ "$1" = "-f" ]; then
run_container logs -f "$CONTAINER_NAME"
else
run_container logs --tail 100 "$CONTAINER_NAME"
fi
}
container_shell() {
run_container exec -it "$CONTAINER_NAME" /bin/sh
}
# =============================================================================
# MODEL MANAGEMENT (via Ollama API/CLI)
# =============================================================================
cmd_list() {
load_config
if container_running; then
# Use API to list models
local models=$(wget -q -O - "http://127.0.0.1:$api_port/api/tags" 2>/dev/null)
if [ -n "$models" ]; then
echo "=== Downloaded Models ==="
echo ""
echo "$models" | jsonfilter -e '@.models[*].name' 2>/dev/null | while read model; do
# Get size from same response
local size=$(echo "$models" | jsonfilter -e "@.models[@.name='$model'].size" 2>/dev/null)
if [ -n "$size" ]; then
# Convert to human readable
local size_gb=$(echo "scale=2; $size / 1024 / 1024 / 1024" | bc 2>/dev/null || echo "?")
echo " $model (${size_gb}GB)"
else
echo " $model"
fi
done
else
echo "No models downloaded yet"
echo ""
echo "Download a model with:"
echo " ollamactl pull tinyllama"
fi
else
log_error "Ollama not running. Start with: /etc/init.d/ollama start"
return 1
fi
echo ""
echo "=== Available Models (popular) ==="
echo " tinyllama - 637MB - Ultra-lightweight"
echo " phi - 1.6GB - Microsoft Phi-2"
echo " gemma:2b - 1.4GB - Google Gemma 2B"
echo " mistral - 4.1GB - High quality assistant"
echo " llama2 - 3.8GB - Meta LLaMA 2 7B"
echo " codellama - 3.8GB - Code specialized"
echo ""
echo "More at: https://ollama.com/library"
}
cmd_pull() {
load_config
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: ollamactl pull <model-name>"; return 1; }
if ! container_running; then
log_error "Ollama not running. Start with: /etc/init.d/ollama start"
return 1
fi
log_info "Pulling model: $model_name"
log_info "This may take several minutes depending on model size..."
# Use exec to run ollama pull inside container
run_container exec "$CONTAINER_NAME" ollama pull "$model_name"
}
cmd_rm() {
load_config
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: ollamactl rm <model-name>"; return 1; }
if ! container_running; then
log_error "Ollama not running. Start with: /etc/init.d/ollama start"
return 1
fi
log_info "Removing model: $model_name"
run_container exec "$CONTAINER_NAME" ollama rm "$model_name"
}
cmd_run() {
load_config
local model_name="$1"
[ -z "$model_name" ] && { echo "Usage: ollamactl run <model-name>"; return 1; }
if ! container_running; then
log_error "Ollama not running. Start with: /etc/init.d/ollama start"
return 1
fi
log_info "Starting chat with: $model_name"
log_info "Type your message and press Enter. Use Ctrl+D to exit."
echo ""
# Interactive chat
run_container exec -it "$CONTAINER_NAME" ollama run "$model_name"
}
# =============================================================================
# COMMANDS
# =============================================================================
cmd_install() {
require_root
load_config
if ! has_runtime; then
log_error "No container runtime found!"
log_error "Install podman or docker first:"
log_error " opkg update && opkg install podman"
exit 1
fi
local runtime=$(detect_runtime)
log_info "Installing Ollama using $runtime..."
log_info "Image: $docker_image"
# Create directories
mkdir -p "$data_path"
# Pull the image
container_pull || exit 1
# Enable service
uci_set main.enabled '1'
/etc/init.d/ollama enable
log_info ""
log_info "Ollama installed successfully!"
log_info ""
log_info "Start with: /etc/init.d/ollama start"
log_info "API endpoint: http://<router-ip>:$api_port/api"
log_info ""
log_info "Download and use a model:"
log_info " ollamactl pull tinyllama"
log_info " ollamactl run tinyllama"
}
cmd_check() {
load_config
echo "=== Prerequisite Check ==="
echo ""
# Check container runtime
local runtime=$(detect_runtime)
if [ -n "$runtime" ]; then
echo "[OK] Container runtime: $runtime"
$runtime --version 2>/dev/null | head -1
else
echo "[FAIL] No container runtime found"
echo " Install: opkg install podman"
fi
echo ""
# Check storage
local storage_path=$(dirname "$data_path")
local storage_avail=$(df -h "$storage_path" 2>/dev/null | tail -1 | awk '{print $4}')
echo "Storage available: $storage_avail (at $storage_path)"
echo " Note: Ollama image requires ~1GB"
echo " Models require 500MB-8GB each"
echo ""
# Check memory
local mem_total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
local mem_gb=$((mem_total / 1024 / 1024))
echo "System memory: ${mem_gb}GB"
if [ "$mem_gb" -lt 2 ]; then
echo "[WARN] Low memory! Ollama requires at least 2GB RAM"
else
echo "[OK] Memory sufficient"
fi
echo ""
# Check if image exists
if [ -n "$runtime" ]; then
if $runtime images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "ollama"; then
echo "[OK] Ollama image found"
else
echo "[INFO] Ollama image not downloaded yet"
echo " Run: ollamactl install"
fi
fi
}
cmd_update() {
require_root
load_config
log_info "Updating Ollama..."
# Stop if running
container_stop
# Pull latest image
container_pull || exit 1
# Restart if was enabled
if [ "$(uci_get main.enabled)" = "1" ]; then
/etc/init.d/ollama restart
else
log_info "Update complete. Start manually with: /etc/init.d/ollama start"
fi
}
cmd_status() {
container_status
}
cmd_logs() {
container_logs "$@"
}
cmd_shell() {
if ! container_running; then
log_error "Container not running. Start with: /etc/init.d/ollama start"
exit 1
fi
container_shell
}
cmd_service_run() {
require_root
load_config
if ! has_runtime; then
log_error "No container runtime found"
exit 1
fi
container_run
}
cmd_service_stop() {
require_root
container_stop
}
# Main Entry Point
case "${1:-}" in
install) shift; cmd_install "$@" ;;
check) shift; cmd_check "$@" ;;
update) shift; cmd_update "$@" ;;
status) shift; cmd_status "$@" ;;
logs) shift; cmd_logs "$@" ;;
shell) shift; cmd_shell "$@" ;;
list) shift; cmd_list "$@" ;;
pull) shift; cmd_pull "$@" ;;
rm) shift; cmd_rm "$@" ;;
run) shift; cmd_run "$@" ;;
service-run) shift; cmd_service_run "$@" ;;
service-stop) shift; cmd_service_stop "$@" ;;
help|--help|-h|'') usage ;;
*) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;;
esac