#!/bin/sh # SecuBox Lyrion manager - Multi-runtime support (Docker/LXC) # Copyright (C) 2024 CyberMind.fr CONFIG="lyrion" CONTAINER_NAME="secbx-lyrion" LXC_NAME="lyrion" OPKG_UPDATED=0 # Paths LXC_PATH="/srv/lxc" LXC_ROOTFS="$LXC_PATH/$LXC_NAME/rootfs" LXC_CONFIG="$LXC_PATH/$LXC_NAME/config" LYRION_ROOTFS_SCRIPT="/usr/share/lyrion/create-lxc-rootfs.sh" usage() { cat <<'EOF' Usage: lyrionctl Commands: install Install prerequisites, prep folders, pull/create container check Run prerequisite checks (storage, runtime) update Update container image and restart service destroy Remove container and rootfs (for reinstall) status Show container status logs Show container logs (use -f to follow) shell Open shell in container service-run Internal: run container under procd service-stop Stop container runtime Show detected/configured runtime Runtime Selection: The runtime can be configured in /etc/config/lyrion: option runtime 'auto' # auto-detect (LXC preferred if available) option runtime 'docker' # Force Docker option runtime 'lxc' # Force LXC 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}.main.$1; } uci_set() { uci set ${CONFIG}.main.$1="$2" && uci commit ${CONFIG}; } # Load configuration with defaults load_config() { runtime="$(uci_get runtime || echo auto)" image="$(uci_get image || echo ghcr.io/lms-community/lyrionmusicserver:stable)" data_path="$(uci_get data_path || echo /srv/lyrion)" media_path="$(uci_get media_path || echo /srv/media)" port="$(uci_get port || echo 9000)" timezone="$(uci_get timezone || cat /etc/TZ 2>/dev/null || echo UTC)" memory_limit="$(uci_get memory_limit || echo 256M)" lxc_rootfs_url="$(uci_get lxc_rootfs_url || echo '')" } ensure_dir() { [ -d "$1" ] || mkdir -p "$1"; } # Check if a runtime is available has_docker() { command -v docker >/dev/null 2>&1 && \ command -v dockerd >/dev/null 2>&1 && \ [ -S /var/run/docker.sock ] } has_lxc() { command -v lxc-start >/dev/null 2>&1 && \ command -v lxc-stop >/dev/null 2>&1 } # Detect best available runtime detect_runtime() { load_config case "$runtime" in docker) if has_docker; then echo "docker" else log_error "Docker requested but not available" return 1 fi ;; lxc) if has_lxc; then echo "lxc" else log_error "LXC requested but not available" return 1 fi ;; auto|*) # Prefer LXC if available (lighter weight) if has_lxc; then echo "lxc" elif has_docker; then echo "docker" else log_error "No container runtime available (install lxc or docker)" return 1 fi ;; esac } # 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 } # ============================================================================= # Docker Runtime Functions # ============================================================================= docker_check_prereqs() { log_info "Checking Docker prerequisites..." ensure_packages dockerd docker containerd || return 1 # Enable and start Docker /etc/init.d/dockerd enable >/dev/null 2>&1 if ! /etc/init.d/dockerd status >/dev/null 2>&1; then /etc/init.d/dockerd start || return 1 sleep 3 fi # Wait for Docker socket local retry=0 while [ ! -S /var/run/docker.sock ] && [ $retry -lt 30 ]; do sleep 1 retry=$((retry + 1)) done [ -S /var/run/docker.sock ] || { log_error "Docker socket not available"; return 1; } log_info "Docker ready" } docker_pull() { load_config log_info "Pulling Docker image: $image" docker pull "$image" } docker_stop() { docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true } docker_run() { load_config docker_stop log_info "Starting Lyrion Docker container..." exec docker run --rm \ --name "$CONTAINER_NAME" \ -p "${port}:9000" \ -p "3483:3483" \ -p "3483:3483/udp" \ -v "$data_path:/config" \ -v "$media_path:/music:ro" \ -e TZ="$timezone" \ --memory="$memory_limit" \ "$image" } docker_status() { docker ps -a --filter "name=$CONTAINER_NAME" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" } docker_logs() { docker logs "$@" "$CONTAINER_NAME" } docker_shell() { docker exec -it "$CONTAINER_NAME" /bin/sh } # ============================================================================= # LXC Runtime Functions # ============================================================================= lxc_check_prereqs() { log_info "Checking LXC prerequisites..." ensure_packages lxc lxc-common lxc-attach lxc-start lxc-stop lxc-destroy || return 1 # Check cgroups 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 # Check for COMPLETE installation (Lyrion installed, not just Alpine) if [ -d "$LXC_ROOTFS" ] && [ -f "$LXC_ROOTFS/opt/lyrion/slimserver.pl" ] && [ -f "$LXC_CONFIG" ]; then log_info "LXC rootfs already exists with Lyrion installed" return 0 fi # Check for incomplete installation (Alpine exists but Lyrion not installed) if [ -d "$LXC_ROOTFS" ] && [ -f "$LXC_ROOTFS/etc/alpine-release" ] && [ ! -f "$LXC_ROOTFS/opt/lyrion/slimserver.pl" ]; then log_warn "Incomplete installation detected (Alpine downloaded but Lyrion not installed)" log_info "Cleaning up incomplete rootfs..." rm -rf "$LXC_PATH/$LXC_NAME" fi log_info "Creating LXC rootfs for Lyrion..." ensure_dir "$LXC_PATH/$LXC_NAME" # Use external script if available if [ -x "$LYRION_ROOTFS_SCRIPT" ]; then "$LYRION_ROOTFS_SCRIPT" "$LXC_ROOTFS" || return 1 else # Inline rootfs creation lxc_create_alpine_rootfs || return 1 fi # Verify Lyrion was actually installed if [ ! -f "$LXC_ROOTFS/opt/lyrion/slimserver.pl" ]; then log_error "Lyrion installation failed - slimserver.pl not found" log_error "Check network connectivity and try again" return 1 fi # Create LXC config lxc_create_config || return 1 log_info "LXC rootfs created successfully" } lxc_create_alpine_rootfs() { local arch="aarch64" local alpine_version="3.19" local mirror="https://dl-cdn.alpinelinux.org/alpine" local rootfs="$LXC_ROOTFS" # Detect architecture case "$(uname -m)" in x86_64) arch="x86_64" ;; aarch64) arch="aarch64" ;; armv7l) arch="armv7" ;; *) arch="x86_64" ;; esac log_info "Downloading Alpine Linux $alpine_version ($arch)..." ensure_dir "$rootfs" cd "$rootfs" || return 1 # Download Alpine minirootfs local rootfs_url="$mirror/v$alpine_version/releases/$arch/alpine-minirootfs-$alpine_version.0-$arch.tar.gz" wget -q -O /tmp/alpine-rootfs.tar.gz "$rootfs_url" || { log_error "Failed to download Alpine rootfs" return 1 } # Extract rootfs tar xzf /tmp/alpine-rootfs.tar.gz -C "$rootfs" || return 1 rm -f /tmp/alpine-rootfs.tar.gz # Configure Alpine echo "nameserver 8.8.8.8" > "$rootfs/etc/resolv.conf" # Install Lyrion in the container cat > "$rootfs/tmp/setup-lyrion.sh" << 'SETUP' #!/bin/sh set -e # Update and install dependencies apk update apk add --no-cache \ perl \ perl-io-socket-ssl \ perl-encode \ perl-xml-parser \ perl-xml-simple \ perl-dbi \ perl-dbd-sqlite \ perl-json-xs \ perl-yaml-libyaml \ perl-crypt-openssl-rsa \ perl-ev \ perl-anyevent \ perl-gd \ perl-digest-sha1 \ perl-sub-name \ perl-html-parser \ perl-template-toolkit \ perl-file-slurp \ imagemagick \ flac \ faad2 \ sox \ lame \ curl \ wget # Download and install Lyrion cd /tmp # Detect architecture for appropriate tarball LYRION_ARCH="" case "$(uname -m)" in aarch64|arm*) LYRION_ARCH="arm-linux" ;; esac # Try ARM-specific tarball first (smaller ~60MB), then fall back to multi-arch (~126MB) echo "Downloading Lyrion Music Server..." LYRION_URL="" if [ -n "$LYRION_ARCH" ]; then LYRION_URL="https://downloads.lms-community.org/LyrionMusicServer_v9.0.3/lyrionmusicserver-9.0.3-${LYRION_ARCH}.tgz" else LYRION_URL="https://downloads.lms-community.org/LyrionMusicServer_v9.0.3/lyrionmusicserver-9.0.3.tgz" fi echo "URL: $LYRION_URL" wget -O lyrion.tar.gz "$LYRION_URL" || { echo "Primary download failed, trying multi-arch tarball..." LYRION_URL="https://downloads.lms-community.org/LyrionMusicServer_v9.0.3/lyrionmusicserver-9.0.3.tgz" wget -O lyrion.tar.gz "$LYRION_URL" || { echo "ERROR: All download attempts failed" exit 1 } } # Verify download succeeded if [ ! -f lyrion.tar.gz ] || [ ! -s lyrion.tar.gz ]; then echo "ERROR: Failed to download Lyrion tarball" exit 1 fi mkdir -p /opt/lyrion tar xzf lyrion.tar.gz -C /opt/lyrion --strip-components=1 || { echo "ERROR: Failed to extract Lyrion tarball" exit 1 } rm -f lyrion.tar.gz # Remove conflicting bundled CPAN modules (use system modules instead) rm -rf /opt/lyrion/CPAN/arch rm -rf /opt/lyrion/CPAN/XML/Parser* rm -rf /opt/lyrion/CPAN/Image rm -rf /opt/lyrion/CPAN/DBD rm -rf /opt/lyrion/CPAN/DBI /opt/lyrion/CPAN/DBI.pm rm -rf /opt/lyrion/CPAN/YAML rm -rf /opt/lyrion/CPAN/Template /opt/lyrion/CPAN/Template.pm # Create stub Image::Scale module (artwork resizing - optional) mkdir -p /opt/lyrion/CPAN/Image cat > /opt/lyrion/CPAN/Image/Scale.pm << 'STUB' package Image::Scale; our $VERSION = '0.08'; sub new { return bless {}, shift } sub resize { return 1 } sub width { return 0 } sub height { return 0 } 1; STUB # Create stub Devel::Peek module (runtime inspection - optional) mkdir -p /opt/lyrion/CPAN/Devel cat > /opt/lyrion/CPAN/Devel/Peek.pm << 'STUB' package Devel::Peek; use strict; our $VERSION = '1.32'; our $ANON_GV; { no strict 'refs'; $ANON_GV = \*{'Devel::Peek::ANON'}; } sub Dump { } sub DumpArray { } sub SvREFCNT { return 1 } sub DeadCode { } sub mstat { } sub fill_mstats { } sub SvROK { return 0 } sub CvGV { return $ANON_GV } 1; STUB # Create directories with proper permissions for nobody user mkdir -p /config/prefs/plugin /config/cache /music /var/log/lyrion chown -R nobody:nobody /config /var/log/lyrion # Create startup script that runs as nobody user cat > /opt/lyrion/start.sh << 'START' #!/bin/sh cd /opt/lyrion # Ensure directories exist with proper permissions mkdir -p /config/prefs/plugin /config/cache /var/log/lyrion chown -R nobody:nobody /config /var/log/lyrion /opt/lyrion 2>/dev/null || true # Run Lyrion as nobody user to avoid permission issues exec su -s /bin/sh nobody -c "cd /opt/lyrion && exec perl slimserver.pl \ --prefsdir /config/prefs \ --cachedir /config/cache \ --logdir /var/log/lyrion \ --httpport 9000 \ --cliport 9090" START chmod +x /opt/lyrion/start.sh echo "Lyrion installed successfully" SETUP chmod +x "$rootfs/tmp/setup-lyrion.sh" # Run setup in chroot log_info "Installing Lyrion in container (this may take a while)..." chroot "$rootfs" /tmp/setup-lyrion.sh || { log_error "Failed to install Lyrion in container" return 1 } rm -f "$rootfs/tmp/setup-lyrion.sh" } lxc_create_config() { load_config cat > "$LXC_CONFIG" << EOF # Lyrion LXC Configuration lxc.uts.name = $LXC_NAME # Root filesystem lxc.rootfs.path = dir:$LXC_ROOTFS # Network - use host network for simplicity lxc.net.0.type = none # Mounts lxc.mount.auto = proc:mixed sys:ro cgroup:mixed lxc.mount.entry = $data_path config none bind,create=dir 0 0 lxc.mount.entry = $media_path music none bind,ro,create=dir 0 0 # Capabilities lxc.cap.drop = sys_admin sys_module mac_admin mac_override # cgroups limits lxc.cgroup.memory.limit_in_bytes = $memory_limit # Init lxc.init.cmd = /opt/lyrion/start.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 'lyrionctl install' first." return 1 fi # Ensure mount points exist ensure_dir "$data_path" ensure_dir "$media_path" log_info "Starting Lyrion LXC container..." exec lxc-start -n "$LXC_NAME" -F -f "$LXC_CONFIG" } lxc_status() { 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 } lxc_logs() { load_config local logfile="$LXC_ROOTFS/var/log/lyrion/server.log" # Also check container logs via lxc-attach if container is running if lxc-info -n "$LXC_NAME" -s 2>/dev/null | grep -q "RUNNING"; then if [ "$1" = "-f" ]; then lxc-attach -n "$LXC_NAME" -- tail -f /var/log/lyrion/server.log else lxc-attach -n "$LXC_NAME" -- tail -100 /var/log/lyrion/server.log fi elif [ -f "$logfile" ]; then if [ "$1" = "-f" ]; then tail -f "$logfile" else tail -100 "$logfile" fi else log_warn "Container not running and no log file found" log_info "Start the service with: /etc/init.d/lyrion start" 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 } # ============================================================================= # Main Commands # ============================================================================= cmd_install() { require_root load_config local rt=$(detect_runtime) || exit 1 log_info "Using runtime: $rt" # Save detected runtime if auto [ "$runtime" = "auto" ] && uci_set detected_runtime "$rt" # Create directories ensure_dir "$data_path" ensure_dir "$media_path" case "$rt" in docker) docker_check_prereqs || exit 1 docker_pull || exit 1 ;; lxc) lxc_check_prereqs || exit 1 lxc_create_rootfs || exit 1 ;; esac uci_set enabled '1' /etc/init.d/lyrion enable log_info "Lyrion installed. Start with: /etc/init.d/lyrion start" log_info "Web interface will be at: http://:$port" } cmd_check() { load_config log_info "Checking prerequisites..." log_info "Configured runtime: $runtime" local rt=$(detect_runtime) if [ -n "$rt" ]; then log_info "Detected runtime: $rt" case "$rt" in docker) docker_check_prereqs ;; lxc) lxc_check_prereqs ;; esac fi } cmd_update() { require_root load_config local rt=$(detect_runtime) || exit 1 case "$rt" in docker) docker_pull || exit 1 ;; lxc) log_info "Updating LXC rootfs..." lxc_destroy lxc_create_rootfs || exit 1 ;; esac if /etc/init.d/lyrion enabled >/dev/null 2>&1; then /etc/init.d/lyrion restart else log_info "Update complete. Restart manually to apply." fi } cmd_destroy() { require_root load_config local rt=$(detect_runtime 2>/dev/null) case "$rt" in docker) docker_stop log_info "Docker container stopped. Image kept for reinstall." log_info "To remove image: docker rmi $image" ;; lxc) lxc_destroy ;; *) # No runtime detected, but try to clean up LXC anyway if [ -d "$LXC_PATH/$LXC_NAME" ]; then log_info "Removing LXC rootfs..." rm -rf "$LXC_PATH/$LXC_NAME" log_info "LXC container destroyed" else log_info "No container found to destroy" fi ;; esac log_info "To reinstall: lyrionctl install" } cmd_status() { local rt=$(detect_runtime 2>/dev/null) case "$rt" in docker) docker_status ;; lxc) lxc_status ;; *) echo "No runtime detected" ;; esac } cmd_logs() { local rt=$(detect_runtime 2>/dev/null) case "$rt" in docker) docker_logs "$@" ;; lxc) lxc_logs "$@" ;; *) echo "No runtime detected" ;; esac } cmd_shell() { local rt=$(detect_runtime 2>/dev/null) case "$rt" in docker) docker_shell ;; lxc) lxc_shell ;; *) echo "No runtime detected" ;; esac } cmd_service_run() { require_root load_config local rt=$(detect_runtime) || exit 1 case "$rt" in docker) docker_check_prereqs || exit 1 docker_run ;; lxc) lxc_check_prereqs || exit 1 lxc_run ;; esac } cmd_service_stop() { require_root local rt=$(detect_runtime 2>/dev/null) case "$rt" in docker) docker_stop ;; lxc) lxc_stop ;; esac } cmd_runtime() { load_config echo "Configured: $runtime" local detected=$(detect_runtime 2>/dev/null) if [ -n "$detected" ]; then echo "Detected: $detected" else echo "Detected: none" fi echo "" echo "Available runtimes:" has_docker && echo " - docker" || echo " - docker (not installed)" has_lxc && echo " - lxc" || echo " - lxc (not installed)" } # ============================================================================= # Main Entry Point # ============================================================================= case "${1:-}" in install) shift; cmd_install "$@" ;; check) shift; cmd_check "$@" ;; update) shift; cmd_update "$@" ;; destroy) shift; cmd_destroy "$@" ;; status) shift; cmd_status "$@" ;; logs) shift; cmd_logs "$@" ;; shell) shift; cmd_shell "$@" ;; service-run) shift; cmd_service_run "$@" ;; service-stop) shift; cmd_service_stop "$@" ;; runtime) shift; cmd_runtime "$@" ;; help|--help|-h|'') usage ;; *) echo "Unknown command: $1" >&2; usage >&2; exit 1 ;; esac