Add quick readme + script to manage server
This commit is contained in:
+54
@@ -0,0 +1,54 @@
|
|||||||
|
#+title: musicfs
|
||||||
|
|
||||||
|
* Incus server VM
|
||||||
|
|
||||||
|
~musicfs-server~ (gRPC) runs inside an Incus VM, live-sharing the host's
|
||||||
|
~~/Music~ as a readonly virtiofs mount. =scripts/vm.sh= builds the binary on the
|
||||||
|
host, bundles its nix ~glibc~ so it runs unmodified in the VM, ships it in, and
|
||||||
|
runs it under systemd.
|
||||||
|
|
||||||
|
** Setup
|
||||||
|
|
||||||
|
#+begin_src bash
|
||||||
|
devenv shell # cargo, incus, patchelf, grpcurl
|
||||||
|
scripts/vm.sh up # create VM, share ~/Music, build, ship, start server
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Options (env): =VM_NAME=, =IMAGE=, =MUSIC_SOURCE=, =LISTEN_PORT=, =RUST_LOG=.
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
|--------------------------+-------------------------------|
|
||||||
|
| =scripts/vm.sh up= | create + build + ship + start |
|
||||||
|
| =scripts/vm.sh redeploy= | rebuild after a code change |
|
||||||
|
| =scripts/vm.sh logs= | tail server logs |
|
||||||
|
| =scripts/vm.sh status= | VM + service status |
|
||||||
|
| =scripts/vm.sh shell= | shell inside the VM |
|
||||||
|
| =scripts/vm.sh down= | stop the VM |
|
||||||
|
| =scripts/vm.sh destroy= | delete the VM |
|
||||||
|
|
||||||
|
** Test it (Nushell)
|
||||||
|
|
||||||
|
#+begin_src nushell
|
||||||
|
let VMIP = (incus list musicfs -f csv -c 4 | lines | first | split row " " | first)
|
||||||
|
let ADDR = $"($VMIP):50051"
|
||||||
|
|
||||||
|
# liveness
|
||||||
|
^nc -z -w 3 $VMIP 50051
|
||||||
|
if $env.LAST_EXIT_CODE == 0 { print "alive" } else { print "dead" }
|
||||||
|
|
||||||
|
# gRPC: stream the manifest, count entries
|
||||||
|
grpcurl -plaintext -import-path proto -proto musicfs.proto -d "{}" $ADDR musicfs.MusicFs/GetManifest | lines | find relPath | length
|
||||||
|
|
||||||
|
# files the server scans
|
||||||
|
incus exec musicfs -- find /music -type f | lines | length
|
||||||
|
|
||||||
|
# play a track straight out of the VM's shared /music
|
||||||
|
incus exec musicfs -- cat "/music/DDT/ДДТ - Творчество в пустоте – 2 - 01 Intro.flac" | ^mpv -
|
||||||
|
#+end_src
|
||||||
|
|
||||||
|
Reach the server at the VM's bridge IP (=scripts/vm.sh status= prints it).
|
||||||
|
Install mpv with =nix profile install nixpkgs#mpv=, or use =^ffplay -= instead.
|
||||||
|
|
||||||
|
In Nushell, always put =| lines= between an external command's stdout and a
|
||||||
|
builtin (=find=, =first=, =length=, ...); external-to-external pipes (=cat |
|
||||||
|
mpv=) are byte-stable and don't need it.
|
||||||
Executable
+290
@@ -0,0 +1,290 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# vm.sh - Incus VM launcher for musicfs-server
|
||||||
|
#
|
||||||
|
# Creates and manages an Incus VM that runs the `musicfs-server` gRPC binary,
|
||||||
|
# live-sharing the host's ~/Music via a virtiofs disk device.
|
||||||
|
#
|
||||||
|
# The server binary is built on the host (cargo) and shipped into the VM as a
|
||||||
|
# *self-contained bundle*: when the host toolchain is nix-based (devenv), the
|
||||||
|
# resulting ELF interpreter points at /nix/store/.../ld-linux-x86-64.so.2 which
|
||||||
|
# does not exist inside a stock Debian/Ubuntu VM. This script detects that,
|
||||||
|
# retargets the interpreter + rpath to an absolute in-VM path via patchelf, and
|
||||||
|
# bundles the few required shared objects alongside the binary. The VM therefore
|
||||||
|
# needs zero package installs to run the server.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# scripts/vm.sh up create VM (if missing), share music, build+ship server, start it
|
||||||
|
# scripts/vm.sh redeploy rebuild + push + restart server (VM kept running)
|
||||||
|
# scripts/vm.sh restart restart the in-VM systemd service (no rebuild)
|
||||||
|
# scripts/vm.sh logs tail server logs (journalctl -f)
|
||||||
|
# scripts/vm.sh status show VM + service status
|
||||||
|
# scripts/vm.sh shell open a shell in the VM
|
||||||
|
# scripts/vm.sh down stop the VM
|
||||||
|
# scripts/vm.sh destroy delete the VM (and its disk) permanently
|
||||||
|
#
|
||||||
|
# Configuration (override via env, e.g. IMAGE=images:ubuntu/24.04 scripts/vm.sh up):
|
||||||
|
# VM_NAME name of the Incus instance (default: musicfs)
|
||||||
|
# IMAGE Incus image alias (default: images:debian/12)
|
||||||
|
# MUSIC_SOURCE host path to the music library (default: $HOME/Music)
|
||||||
|
# MUSIC_MOUNT where it appears inside the VM (default: /music)
|
||||||
|
# LISTEN_PORT gRPC port (host:vm forwarded) (default: 50051)
|
||||||
|
# RUST_LOG tracing filter for the server (default: info)
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ─────────────────────────────── config ────────────────────────────────
|
||||||
|
VM_NAME="${VM_NAME:-musicfs}"
|
||||||
|
IMAGE="${IMAGE:-images:debian/12}"
|
||||||
|
MUSIC_SOURCE="${MUSIC_SOURCE:-$HOME/Music}"
|
||||||
|
MUSIC_MOUNT="${MUSIC_MOUNT:-/music}"
|
||||||
|
LISTEN_PORT="${LISTEN_PORT:-50051}"
|
||||||
|
RUST_LOG="${RUST_LOG:-info}"
|
||||||
|
|
||||||
|
# Fixed inside the VM; the bundle's interpreter+rpath point here.
|
||||||
|
SERVER_DIR="/opt/musicfs"
|
||||||
|
LOG_DIR="/var/log/musicfs"
|
||||||
|
BIN_NAME="musicfs-server"
|
||||||
|
UNIT_NAME="musicfs-server.service"
|
||||||
|
|
||||||
|
# ─────────────────────────────── helpers ───────────────────────────────
|
||||||
|
log() { printf '\033[1;34m[vm]\033[0m %s\n' "$*" >&2; }
|
||||||
|
warn() { printf '\033[1;33m[vm warn]\033[0m %s\n' "$*" >&2; }
|
||||||
|
die() { printf '\033[1;31m[vm error]\033[0m %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
require() { command -v "$1" >/dev/null 2>&1 || die "missing required command: $1 (run inside 'devenv shell')"; }
|
||||||
|
|
||||||
|
vm_exists() { incus list "$VM_NAME" -f csv -c n 2>/dev/null | grep -qx "$VM_NAME"; }
|
||||||
|
vm_running() { incus list "$VM_NAME" -f csv -c n,s 2>/dev/null | grep -q "$VM_NAME,.*RUNNING"; }
|
||||||
|
|
||||||
|
device_exists() { incus config device show "$VM_NAME" 2>/dev/null | grep -q "^$1:"; }
|
||||||
|
|
||||||
|
# incus -c 4 emits "<ip> (<iface>)"; keep only the address.
|
||||||
|
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
|
||||||
|
|
||||||
|
# ──────────────────────────── build + bundle ───────────────────────────
|
||||||
|
# Emits the host path to the prepared bundle dir on stdout.
|
||||||
|
build_bundle() {
|
||||||
|
require cargo
|
||||||
|
require readelf
|
||||||
|
log "building $BIN_NAME (release)..."
|
||||||
|
cargo build --release --bin "$BIN_NAME"
|
||||||
|
|
||||||
|
local bin="target/release/$BIN_NAME"
|
||||||
|
local interp
|
||||||
|
# readelf prints " [Requesting program interpreter: /path]" — capture the path.
|
||||||
|
interp="$(readelf -l "$bin" | sed -n 's/.*interpreter: \(.*\)\]$/\1/p')"
|
||||||
|
[ -n "$interp" ] || die "could not read ELF interpreter from $bin"
|
||||||
|
log "binary interpreter: $interp"
|
||||||
|
|
||||||
|
local bundle="target/vm-bundle"
|
||||||
|
rm -rf "$bundle"; mkdir -p "$bundle/lib"
|
||||||
|
cp "$bin" "$bundle/server"
|
||||||
|
|
||||||
|
case "$interp" in
|
||||||
|
/nix/store/*)
|
||||||
|
log "nix-built binary detected; bundling glibc + libgcc_s for the VM..."
|
||||||
|
require patchelf
|
||||||
|
local libc libm libgcc
|
||||||
|
libc="$(ldd "$bin" | sed -n 's#.*libc.so.6 => \([^ ]*\).*#\1#p')"
|
||||||
|
libm="$(ldd "$bin" | sed -n 's#.*libm.so.6 => \([^ ]*\).*#\1#p')"
|
||||||
|
libgcc="$(ldd "$bin"| sed -n 's#.*libgcc_s.so.1 => \([^ ]*\).*#\1#p')"
|
||||||
|
[ -n "$libc" ] || die "could not resolve libc.so.6"
|
||||||
|
[ -n "$libm" ] || die "could not resolve libm.so.6"
|
||||||
|
[ -n "$libgcc" ] || die "could not resolve libgcc_s.so.1"
|
||||||
|
cp "$interp" "$bundle/lib/ld-linux-x86-64.so.2"
|
||||||
|
cp "$libc" "$bundle/lib/libc.so.6"
|
||||||
|
cp "$libm" "$bundle/lib/libm.so.6"
|
||||||
|
cp "$libgcc" "$bundle/lib/libgcc_s.so.1"
|
||||||
|
patchelf --set-interpreter "$SERVER_DIR/lib/ld-linux-x86-64.so.2" "$bundle/server"
|
||||||
|
patchelf --set-rpath "$SERVER_DIR/lib" "$bundle/server"
|
||||||
|
;;
|
||||||
|
/*)
|
||||||
|
warn "standard interpreter detected; shipping binary as-is."
|
||||||
|
warn " (this only works if the VM's glibc >= the build host's glibc)."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "unexpected interpreter '$interp'; refusing to ship."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
chmod +x "$bundle/server"
|
||||||
|
echo "$bundle"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ───────────────────────────── VM lifecycle ────────────────────────────
|
||||||
|
create_vm() {
|
||||||
|
require incus
|
||||||
|
if vm_exists; then log "VM '$VM_NAME' already exists."; return 0; fi
|
||||||
|
|
||||||
|
[ -d "$MUSIC_SOURCE" ] || die "MUSIC_SOURCE '$MUSIC_SOURCE' is not a directory"
|
||||||
|
log "creating VM '$VM_NAME' from $IMAGE ..."
|
||||||
|
incus init "$IMAGE" "$VM_NAME" --vm
|
||||||
|
|
||||||
|
if ! device_exists music; then
|
||||||
|
log "sharing music: $MUSIC_SOURCE -> $MUSIC_MOUNT (readonly, virtiofs)"
|
||||||
|
incus config device add "$VM_NAME" music disk \
|
||||||
|
source="$MUSIC_SOURCE" path="$MUSIC_MOUNT" readonly=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# No proxy: Incus VM proxies need NAT mode + a *static* instance IP (DHCP is
|
||||||
|
# rejected). Server binds 0.0.0.0 and the VM is on incusbr0 -> reach it at
|
||||||
|
# its bridge IP (shown by `status`).
|
||||||
|
}
|
||||||
|
|
||||||
|
start_vm() {
|
||||||
|
if vm_running; then return 0; fi
|
||||||
|
log "starting VM '$VM_NAME'..."
|
||||||
|
incus start "$VM_NAME"
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_vm() {
|
||||||
|
log "waiting for VM agent to accept commands..."
|
||||||
|
local i
|
||||||
|
for ((i = 0; i < 90; i++)); do
|
||||||
|
if incus exec "$VM_NAME" -- true 2>/dev/null; then return 0; fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
die "VM '$VM_NAME' did not become reachable (incus-agent not up in time)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Push the bundle + install a systemd unit, then (re)start the service.
|
||||||
|
deploy() {
|
||||||
|
local bundle; bundle="$(build_bundle)"
|
||||||
|
log "deploying bundle to $VM_NAME:$SERVER_DIR ..."
|
||||||
|
# Stop first: overwriting a running binary fails with ETXTBSY ("text file busy").
|
||||||
|
if incus exec "$VM_NAME" -- systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
|
||||||
|
log "stopping $UNIT_NAME (binary in use)..."
|
||||||
|
incus exec "$VM_NAME" -- systemctl stop "$UNIT_NAME"
|
||||||
|
fi
|
||||||
|
incus exec "$VM_NAME" -- mkdir -p "$SERVER_DIR/lib" "$LOG_DIR"
|
||||||
|
incus file push "$bundle/server" "$VM_NAME$SERVER_DIR/server" --mode=0755
|
||||||
|
# Ship the bundled libs one by one (robust across incus file-push versions).
|
||||||
|
if [ -n "$(ls -A "$bundle/lib" 2>/dev/null)" ]; then
|
||||||
|
local f
|
||||||
|
for f in "$bundle/lib"/*; do
|
||||||
|
incus file push "$f" "$VM_NAME$SERVER_DIR/lib/"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "installing systemd unit ($UNIT_NAME)..."
|
||||||
|
install_unit
|
||||||
|
|
||||||
|
log "enabling + (re)starting $UNIT_NAME ..."
|
||||||
|
incus exec "$VM_NAME" -- systemctl daemon-reload
|
||||||
|
incus exec "$VM_NAME" -- systemctl enable "$UNIT_NAME" >/dev/null 2>&1 || true
|
||||||
|
incus exec "$VM_NAME" -- systemctl restart "$UNIT_NAME"
|
||||||
|
|
||||||
|
sleep 1
|
||||||
|
if incus exec "$VM_NAME" -- systemctl is-active --quiet "$UNIT_NAME"; then
|
||||||
|
log "musicfs-server is running."
|
||||||
|
else
|
||||||
|
warn "service did not reach active state quickly; check: scripts/vm.sh logs"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_unit() {
|
||||||
|
local unit
|
||||||
|
unit=$(cat <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=musicfs gRPC server
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=$SERVER_DIR
|
||||||
|
ExecStart=$SERVER_DIR/server --source $MUSIC_MOUNT --listen 0.0.0.0:$LISTEN_PORT --log-dir $LOG_DIR
|
||||||
|
Environment=RUST_LOG=$RUST_LOG
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=2
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
printf '%s\n' "$unit" | incus exec "$VM_NAME" -- tee "/etc/systemd/system/$UNIT_NAME" >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# ────────────────────────────── subcommands ────────────────────────────
|
||||||
|
cmd_up() {
|
||||||
|
require incus
|
||||||
|
create_vm
|
||||||
|
start_vm
|
||||||
|
wait_for_vm
|
||||||
|
deploy
|
||||||
|
print_status
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_redeploy() {
|
||||||
|
require incus
|
||||||
|
vm_exists || die "VM '$VM_NAME' does not exist. Run 'up' first."
|
||||||
|
vm_running || die "VM '$VM_NAME' is not running. Run 'up' first."
|
||||||
|
wait_for_vm
|
||||||
|
deploy
|
||||||
|
print_status
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_restart() {
|
||||||
|
require incus
|
||||||
|
vm_running || die "VM '$VM_NAME' is not running."
|
||||||
|
incus exec "$VM_NAME" -- systemctl restart "$UNIT_NAME"
|
||||||
|
log "restarted."
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_logs() { require incus; vm_running || die "VM not running."; incus exec "$VM_NAME" -- journalctl -u "$UNIT_NAME" -f; }
|
||||||
|
|
||||||
|
cmd_shell() { require incus; vm_running || die "VM not running."; exec incus exec "$VM_NAME" -- bash; }
|
||||||
|
|
||||||
|
cmd_down() { require incus; vm_running || { log "already stopped."; return; }; incus stop "$VM_NAME"; log "stopped."; }
|
||||||
|
|
||||||
|
cmd_destroy() {
|
||||||
|
require incus
|
||||||
|
vm_exists || { log "nothing to destroy."; return; }
|
||||||
|
incus delete -f "$VM_NAME"
|
||||||
|
log "VM '$VM_NAME' deleted."
|
||||||
|
}
|
||||||
|
|
||||||
|
print_status() {
|
||||||
|
local ip; ip="$(vm_ip)"
|
||||||
|
local state="missing"; vm_exists && state="$(incus list "$VM_NAME" -f csv -c s 2>/dev/null | head -1)"
|
||||||
|
vm_running && state="RUNNING"
|
||||||
|
echo
|
||||||
|
printf '\033[1;36m== musicfs-server VM ==\033[0m\n'
|
||||||
|
printf ' name: %s\n' "$VM_NAME"
|
||||||
|
printf ' state: %s\n' "$state"
|
||||||
|
printf ' ipv4: %s\n' "${ip:-<none>}"
|
||||||
|
printf ' image: %s\n' "$IMAGE"
|
||||||
|
printf ' listen: 0.0.0.0:%s (reach via VM bridge IP above)\n' "$LISTEN_PORT"
|
||||||
|
printf ' music: %s -> %s\n' "$MUSIC_SOURCE" "$MUSIC_MOUNT"
|
||||||
|
printf ' server: %s/server\n' "$SERVER_DIR"
|
||||||
|
if [ -n "$ip" ]; then
|
||||||
|
printf '\n test: grpcurl -plaintext %s:%s list\n\n' "$ip" "$LISTEN_PORT"
|
||||||
|
else
|
||||||
|
printf '\n test: grpcurl -plaintext <vm-ip>:%s list\n\n' "$LISTEN_PORT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_status() { require incus; print_status; incus exec "$VM_NAME" -- systemctl status "$UNIT_NAME" --no-pager 2>/dev/null || true; }
|
||||||
|
|
||||||
|
# ───────────────────────────────── main ────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
sed -n '3,/^$/p' "$0" | sed 's/^# \?//'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
case "${1:-up}" in
|
||||||
|
up) cmd_up ;;
|
||||||
|
redeploy) cmd_redeploy ;;
|
||||||
|
restart) cmd_restart ;;
|
||||||
|
logs) cmd_logs ;;
|
||||||
|
status) cmd_status ;;
|
||||||
|
shell) cmd_shell ;;
|
||||||
|
down) cmd_down ;;
|
||||||
|
destroy) cmd_destroy ;;
|
||||||
|
-h|--help|help) usage ;;
|
||||||
|
*) die "unknown command '$1'. Try: $0 --help" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user