#!/bin/sh # SecuBox Glances manager - LXC container support # Copyright (C) 2025-2026 CyberMind.fr CONFIG="glances" LXC_NAME="glances" OPKG_UPDATED=0 # Paths LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" usage() { cat <<'EOF' Usage: glancesctl Commands: install Install prerequisites and create LXC container check Run prerequisite checks update Update Glances in container status Show container status logs Show Glances logs (use -f to follow) shell Open shell in container service-run Internal: run container under procd service-stop Stop container Web Interface: http://:61208 API Endpoint: http://:61209 EOF } require_root() { [ "$(id -u)" -eq 0 ] || { echo "Root required" >&2; exit 1; }; } log_info() { echo "[INFO] $*"; } log_warn() { echo "[WARN] $*" >&2; } log_error() { echo "[ERROR] $*" >&2; } uci_get() { uci -q get ${CONFIG}.$1; } uci_set() { uci set ${CONFIG}.$1="$2" && uci commit ${CONFIG}; } # Load configuration with defaults load_config() { web_port="$(uci_get main.web_port || echo 61208)" api_port="$(uci_get main.api_port || echo 61209)" web_host="$(uci_get main.web_host || echo 0.0.0.0)" refresh_rate="$(uci_get main.refresh_rate || echo 3)" memory_limit="$(uci_get main.memory_limit || echo 128M)" # Monitoring options monitor_docker="$(uci_get monitoring.monitor_docker || echo 1)" monitor_network="$(uci_get monitoring.monitor_network || echo 1)" monitor_diskio="$(uci_get monitoring.monitor_diskio || echo 1)" monitor_sensors="$(uci_get monitoring.monitor_sensors || echo 1)" # Alert thresholds cpu_warning="$(uci_get alerts.cpu_warning || echo 70)" cpu_critical="$(uci_get alerts.cpu_critical || echo 90)" mem_warning="$(uci_get alerts.mem_warning || echo 70)" mem_critical="$(uci_get alerts.mem_critical || echo 90)" } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } has_lxc() { command -v lxc-start >/dev/null 2>&1 && \ command -v lxc-stop >/dev/null 2>&1 } # 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 # ============================================================================= lxc_check_prereqs() { log_info "Checking LXC prerequisites..." ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1 if [ ! -d /sys/fs/cgroup ]; then log_error "cgroups not mounted at /sys/fs/cgroup" return 1 fi log_info "LXC ready" } lxc_create_rootfs() { load_config if [ -d "$LXC_ROOTFS" ] && [ -x "$LXC_ROOTFS/usr/local/bin/glances" ]; then log_info "LXC rootfs already exists with Glances" return 0 fi log_info "Creating LXC rootfs for Glances..." ensure_dir "$LXC_PATH/$LXC_NAME" lxc_create_docker_rootfs || return 1 lxc_create_config || return 1 log_info "LXC rootfs created successfully" } lxc_create_docker_rootfs() { local rootfs="$LXC_ROOTFS" local image="nicolargo/glances" local tag="latest-full" local registry="registry-1.docker.io" local arch # Detect architecture for Docker manifest case "$(uname -m)" in x86_64) arch="amd64" ;; aarch64) arch="arm64" ;; armv7l) arch="arm" ;; *) arch="amd64" ;; esac log_info "Extracting Glances Docker image ($arch)..." ensure_dir "$rootfs" # Get Docker Hub token local token=$(wget -q -O - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:$image:pull" | jsonfilter -e '@.token') [ -z "$token" ] && { log_error "Failed to get Docker Hub token"; return 1; } # Get manifest list local manifest=$(wget -q -O - --header="Authorization: Bearer $token" \ --header="Accept: application/vnd.docker.distribution.manifest.list.v2+json" \ "https://$registry/v2/$image/manifests/$tag") # Find digest for our architecture local digest=$(echo "$manifest" | jsonfilter -e "@.manifests[@.platform.architecture='$arch'].digest") [ -z "$digest" ] && { log_error "No manifest found for $arch"; return 1; } # Get image manifest local img_manifest=$(wget -q -O - --header="Authorization: Bearer $token" \ --header="Accept: application/vnd.docker.distribution.manifest.v2+json" \ "https://$registry/v2/$image/manifests/$digest") # Extract layers and download them log_info "Downloading and extracting layers..." local layers=$(echo "$img_manifest" | jsonfilter -e '@.layers[*].digest') for layer_digest in $layers; do log_info " Layer: ${layer_digest:7:12}..." wget -q -O - --header="Authorization: Bearer $token" \ "https://$registry/v2/$image/blobs/$layer_digest" | \ tar xzf - -C "$rootfs" 2>&1 | grep -v "Cannot change ownership" || true done # Configure container echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf" mkdir -p "$rootfs/var/log/glances" "$rootfs/etc/glances" "$rootfs/tmp" mkdir -p "$rootfs/host" "$rootfs/rom" "$rootfs/overlay" "$rootfs/boot" "$rootfs/srv" mkdir -p "$rootfs/run" touch "$rootfs/run/docker.sock" # Ensure /bin/sh exists if [ ! -x "$rootfs/bin/sh" ]; then if [ -x "$rootfs/bin/dash" ]; then ln -sf dash "$rootfs/bin/sh" elif [ -x "$rootfs/bin/bash" ]; then ln -sf bash "$rootfs/bin/sh" elif [ -x "$rootfs/usr/bin/dash" ]; then mkdir -p "$rootfs/bin" ln -sf /usr/bin/dash "$rootfs/bin/sh" fi fi # Patch Glances: disable @exit_after on fs plugin (multiprocessing # fails inside LXC containers, causing all disk_usage calls to return None) local fs_init="$rootfs/app/glances/plugins/fs/__init__.py" if [ -f "$fs_init" ]; then sed -i 's/^@exit_after(/#@exit_after(/' "$fs_init" log_info "Patched Glances fs plugin for LXC compatibility" fi # Generate /etc/mtab from host mounts (psutil reads this for disk_partitions) grep -E '^(/dev/|overlayfs:)' /proc/mounts | \ grep -v '/srv/docker/overlay' > "$rootfs/etc/mtab" 2>/dev/null # Create startup script (written via file, not heredoc, to preserve shebang) printf '%s\n' '#!/bin/sh' \ 'export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"' \ 'export PYTHONPATH="/app:$PYTHONPATH"' \ 'cd /' \ '' \ '# Set hostname from host kernel (proc:mixed exposes this)' \ 'REAL_HOSTNAME=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo glances)' \ 'hostname "$REAL_HOSTNAME" 2>/dev/null' \ 'printf "127.0.0.1\tlocalhost %s glances\n::1\t\tlocalhost ip6-localhost\n" "$REAL_HOSTNAME" > /etc/hosts' \ '' \ '# Copy host os-release so Glances reports OpenWrt, not Alpine' \ 'if [ -f /host/openwrt_release ]; then' \ " RELEASE=\$(grep DISTRIB_RELEASE /host/openwrt_release 2>/dev/null | cut -d\"'\" -f2)" \ ' printf '"'"'NAME="OpenWrt"\nVERSION="%s"\nID=openwrt\nPRETTY_NAME="OpenWrt %s"\n'"'"' "$RELEASE" "$RELEASE" > /etc/os-release' \ 'fi' \ '' \ 'WEB_PORT="${GLANCES_WEB_PORT:-61208}"' \ 'WEB_HOST="${GLANCES_WEB_HOST:-0.0.0.0}"' \ 'REFRESH="${GLANCES_REFRESH:-3}"' \ '' \ 'ARGS="-w -B $WEB_HOST -p $WEB_PORT -t $REFRESH --disable-autodiscover --disable-check-update"' \ '[ "$GLANCES_NO_DOCKER" = "1" ] && ARGS="$ARGS --disable-plugin docker"' \ '[ "$GLANCES_NO_SENSORS" = "1" ] && ARGS="$ARGS --disable-plugin sensors"' \ '' \ 'echo "Starting Glances on ${WEB_HOST}:${WEB_PORT} host=${REAL_HOSTNAME}..."' \ 'exec /venv/bin/python -m glances $ARGS' \ > "$rootfs/opt/start-glances.sh" chmod +x "$rootfs/opt/start-glances.sh" log_info "Glances Docker image extracted successfully" } lxc_create_config() { load_config local hostname=$(cat /proc/sys/kernel/hostname 2>/dev/null || echo glances) local mem_bytes=$(echo "$memory_limit" | sed 's/M/000000/') cat > "$LXC_CONFIG" << EOF # Glances LXC Configuration — full host visibility lxc.uts.name = $hostname lxc.rootfs.path = dir:$LXC_ROOTFS # Network - host network for full visibility lxc.net.0.type = none # Auto-mounts lxc.mount.auto = proc:mixed sys:ro cgroup:mixed # Host filesystem bind mounts (read-only) for disk monitoring lxc.mount.entry = /rom rom none bind,ro,create=dir 0 0 lxc.mount.entry = /overlay overlay none bind,ro,create=dir 0 0 lxc.mount.entry = /boot boot none bind,ro,create=dir 0 0 lxc.mount.entry = /srv srv none bind,ro,create=dir 0 0 # Host info for OS identification lxc.mount.entry = /etc/openwrt_release host/openwrt_release none bind,ro,create=file 0 0 lxc.mount.entry = /etc/openwrt_version host/openwrt_version none bind,ro,create=file 0 0 # Docker socket for container monitoring (mount at /run, not /var/run which is a symlink) lxc.mount.entry = /var/run/docker.sock run/docker.sock none bind,create=file 0 0 # Environment variables lxc.environment = GLANCES_WEB_PORT=$web_port lxc.environment = GLANCES_WEB_HOST=$web_host lxc.environment = GLANCES_REFRESH=$refresh_rate lxc.environment = GLANCES_NO_DOCKER=$([ "$monitor_docker" = "0" ] && echo 1 || echo 0) lxc.environment = GLANCES_NO_SENSORS=$([ "$monitor_sensors" = "0" ] && echo 1 || echo 0) # Capabilities lxc.cap.drop = sys_module mac_admin mac_override sys_rawio # cgroups limits lxc.cgroup2.memory.max = $mem_bytes # Init lxc.init.cmd = /opt/start-glances.sh # Console lxc.console.size = 1024 lxc.pty.max = 1024 EOF log_info "LXC config created at $LXC_CONFIG" } lxc_stop() { 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 lxc_stop if [ ! -f "$LXC_CONFIG" ]; then log_error "LXC not configured. Run 'glancesctl install' first." return 1 fi # Regenerate config to pick up any UCI changes lxc_create_config # Regenerate /etc/mtab from host mounts (psutil reads this) grep -E '^(/dev/|overlayfs:)' /proc/mounts | \ grep -v '/srv/docker/overlay' > "$LXC_ROOTFS/etc/mtab" 2>/dev/null log_info "Starting Glances LXC container..." log_info "Web interface: http://0.0.0.0:$web_port" exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" } lxc_status() { load_config echo "=== Glances Status ===" echo "" if lxc-info -n "$LXC_NAME" >/dev/null 2>&1; then lxc-info -n "$LXC_NAME" else echo "LXC container '$LXC_NAME' not found or not configured" fi echo "" echo "=== Configuration ===" echo "Web port: $web_port" echo "Refresh rate: ${refresh_rate}s" echo "Memory limit: $memory_limit" } lxc_logs() { if [ "$1" = "-f" ]; then logread -f -e glances else logread -e glances | tail -100 fi } lxc_shell() { lxc-attach -n "$LXC_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 } # ============================================================================= # COMMANDS # ============================================================================= cmd_install() { require_root load_config if ! has_lxc; then log_error "LXC not available. Install lxc packages first." exit 1 fi log_info "Installing Glances..." lxc_check_prereqs || exit 1 lxc_create_rootfs || exit 1 uci_set main.enabled '1' /etc/init.d/glances enable log_info "Glances installed." log_info "Start with: /etc/init.d/glances start" log_info "Web interface: http://:$web_port" } cmd_check() { load_config log_info "Checking prerequisites..." if has_lxc; then log_info "LXC: available" lxc_check_prereqs else log_warn "LXC: not available" fi } cmd_update() { require_root load_config log_info "Updating Glances..." lxc_destroy lxc_create_rootfs || exit 1 if /etc/init.d/glances enabled >/dev/null 2>&1; then /etc/init.d/glances restart else log_info "Update complete. Restart manually to apply." fi } cmd_status() { lxc_status } cmd_logs() { lxc_logs "$@" } cmd_shell() { lxc_shell } cmd_service_run() { require_root load_config if ! has_lxc; then log_error "LXC not available" exit 1 fi lxc_check_prereqs || exit 1 lxc_run } cmd_service_stop() { require_root lxc_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 "$@" ;; 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