Add USB helper library for state management and crosvm interaction

Sourced by the vmsilo-usb CLI, systemd oneshot services, and persistent
USB attach service. Provides sysfs enumeration, flock-based state file
management, and crosvm usb attach/detach wrappers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-17 13:03:57 +00:00
parent 4ef549b981
commit 609eccae4a
2 changed files with 212 additions and 0 deletions

View file

@ -972,6 +972,12 @@ in
internal = true;
};
usbHelperLib = lib.mkOption {
type = lib.types.path;
description = "USB helper library script.";
internal = true;
};
# Generated by netvm.nix: maps VM name -> { interfaces, guestConfig }
# Used to inject netvm-derived interfaces and guest config into VMs
# without creating a self-referential cycle on nixosVms.

View file

@ -28,6 +28,210 @@ let
vms = assignVmIds cfg.nixosVms;
# USB helper library — sourced by vmsilo-usb CLI, systemd oneshot services, and persistent attach service
usbHelperLib = pkgs.writeShellScript "vmsilo-usb-lib" ''
# --- constants ---
USB_STATE_FILE=/run/vmsilo/usb-state.json
USB_STATE_LOCK=/run/vmsilo/usb-state.lock
CROSVM=${cfg._internal.crosvm}/bin/crosvm
# --- locking (fd 9) ---
usb_lock() {
exec 9>"''${USB_STATE_LOCK}"
${pkgs.util-linux}/bin/flock 9
}
usb_unlock() {
exec 9>&-
}
# --- state file ---
usb_read_state() {
if [ -f "''${USB_STATE_FILE}" ]; then
${pkgs.coreutils}/bin/cat "''${USB_STATE_FILE}"
else
echo '[]'
fi
}
usb_write_state() {
local json="$1"
local tmp="''${USB_STATE_FILE}.tmp.$$"
printf '%s\n' "''${json}" > "''${tmp}"
${pkgs.coreutils}/bin/mv "''${tmp}" "''${USB_STATE_FILE}"
}
# --- enumerate USB devices from sysfs ---
usb_enumerate() {
local result='[]'
for dev in /sys/bus/usb/devices/[0-9]*-[0-9]*; do
[ -d "''${dev}" ] || continue
# Skip interface entries (e.g. 1-3:1.0)
local base
base=$(${pkgs.coreutils}/bin/basename "''${dev}")
case "''${base}" in
*:*) continue ;;
esac
# Read sysfs attributes
local vid pid serial busnum devnum manufacturer product devpath
vid=$(${pkgs.coreutils}/bin/cat "''${dev}/idVendor" 2>/dev/null || echo "")
pid=$(${pkgs.coreutils}/bin/cat "''${dev}/idProduct" 2>/dev/null || echo "")
serial=$(${pkgs.coreutils}/bin/cat "''${dev}/serial" 2>/dev/null || echo "")
busnum=$(${pkgs.coreutils}/bin/cat "''${dev}/busnum" 2>/dev/null || echo "")
devnum=$(${pkgs.coreutils}/bin/cat "''${dev}/devnum" 2>/dev/null || echo "")
manufacturer=$(${pkgs.coreutils}/bin/cat "''${dev}/manufacturer" 2>/dev/null || echo "")
product=$(${pkgs.coreutils}/bin/cat "''${dev}/product" 2>/dev/null || echo "")
devpath="''${base}"
local dev_file="/dev/bus/usb/$(printf '%03d' "''${busnum}")/$(printf '%03d' "''${devnum}")"
[ -z "''${vid}" ] && continue
result=$(printf '%s\n' "''${result}" | ${pkgs.jq}/bin/jq -c \
--arg devpath "''${devpath}" \
--arg vid "''${vid}" \
--arg pid "''${pid}" \
--arg serial "''${serial}" \
--arg busnum "''${busnum}" \
--arg devnum "''${devnum}" \
--arg manufacturer "''${manufacturer}" \
--arg product "''${product}" \
--arg dev_file "''${dev_file}" \
'. + [{devpath: $devpath, vid: $vid, pid: $pid, serial: $serial, busnum: $busnum, devnum: $devnum, manufacturer: $manufacturer, product: $product, dev_file: $dev_file}]')
done
printf '%s\n' "''${result}"
}
# --- find by VID:PID (optional serial) ---
usb_find_by_vidpid() {
local vid="$1" pid="$2" serial="''${3:-}"
local devices
devices=$(usb_enumerate)
if [ -n "''${serial}" ]; then
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
--arg vid "''${vid}" --arg pid "''${pid}" --arg serial "''${serial}" \
'[.[] | select(.vid == $vid and .pid == $pid and .serial == $serial)]'
else
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
--arg vid "''${vid}" --arg pid "''${pid}" \
'[.[] | select(.vid == $vid and .pid == $pid)]'
fi
}
# --- find by devpath ---
usb_find_by_devpath() {
local devpath="$1"
local devices
devices=$(usb_enumerate)
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
--arg devpath "''${devpath}" \
'[.[] | select(.devpath == $devpath)]'
}
# --- attach (expects lock already held) ---
usb_do_attach() {
local vm_name="$1" devpath="$2" dev_file="$3" vid="$4" pid="$5" serial="$6" busnum="$7" devnum="$8"
local state socket response port
socket="/run/vmsilo/''${vm_name}-crosvm-control.socket"
state=$(usb_read_state)
# Check if already attached somewhere — detach first
local existing
existing=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg devpath "''${devpath}" \
'.[] | select(.devpath == $devpath)')
if [ -n "''${existing}" ]; then
local old_vm old_port old_socket
old_vm=$(printf '%s\n' "''${existing}" | ${pkgs.jq}/bin/jq -r '.vm')
old_port=$(printf '%s\n' "''${existing}" | ${pkgs.jq}/bin/jq -r '.port')
old_socket="/run/vmsilo/''${old_vm}-crosvm-control.socket"
if [ -S "''${old_socket}" ]; then
''${CROSVM} usb detach "''${old_port}" "''${old_socket}" >/dev/null 2>&1 || true
fi
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg devpath "''${devpath}" \
'[.[] | select(.devpath != $devpath)]')
fi
# Attach
response=$(''${CROSVM} usb attach "1:1:0000:0000" "''${dev_file}" "''${socket}" 2>&1) || {
echo "Error: crosvm usb attach failed: ''${response}" >&2
usb_write_state "''${state}"
return 1
}
# Parse "ok <port>"
port=$(printf '%s\n' "''${response}" | ${pkgs.gnused}/bin/sed -n 's/^ok \([0-9]*\)/\1/p')
if [ -z "''${port}" ]; then
echo "Error: unexpected crosvm response: ''${response}" >&2
usb_write_state "''${state}"
return 1
fi
# Update state
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg vm "''${vm_name}" \
--arg devpath "''${devpath}" \
--arg vid "''${vid}" \
--arg pid "''${pid}" \
--arg serial "''${serial}" \
--arg busnum "''${busnum}" \
--arg devnum "''${devnum}" \
--arg port "''${port}" \
'. + [{vm: $vm, devpath: $devpath, vid: $vid, pid: $pid, serial: $serial, busnum: $busnum, devnum: $devnum, port: ($port | tonumber)}]')
usb_write_state "''${state}"
echo "Attached ''${vid}:''${pid} (''${devpath}) to ''${vm_name} on port ''${port}"
}
# --- detach (expects lock already held) ---
usb_do_detach() {
local vm_name="$1" devpath="$2"
local state socket
socket="/run/vmsilo/''${vm_name}-crosvm-control.socket"
state=$(usb_read_state)
local entry
entry=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg vm "''${vm_name}" --arg devpath "''${devpath}" \
'.[] | select(.vm == $vm and .devpath == $devpath)')
if [ -z "''${entry}" ]; then
echo "Error: device ''${devpath} not attached to ''${vm_name}" >&2
return 1
fi
local port
port=$(printf '%s\n' "''${entry}" | ${pkgs.jq}/bin/jq -r '.port')
if [ -S "''${socket}" ]; then
local response
response=$(''${CROSVM} usb detach "''${port}" "''${socket}" 2>&1) || {
echo "Warning: crosvm usb detach failed: ''${response}" >&2
}
fi
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg vm "''${vm_name}" --arg devpath "''${devpath}" \
'[.[] | select(.vm != $vm or .devpath != $devpath)]')
usb_write_state "''${state}"
echo "Detached ''${devpath} from ''${vm_name} (port ''${port})"
}
# --- cleanup all entries for a VM (acquires lock) ---
usb_cleanup_vm() {
local vm_name="$1"
usb_lock
local state
state=$(usb_read_state)
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
--arg vm "''${vm_name}" \
'[.[] | select(.vm != $vm)]')
usb_write_state "''${state}"
usb_unlock
}
'';
# NOTE: getEffectiveInterfaces is intentionally duplicated in networking.nix and services.nix.
# It cannot live in helpers.nix (which has no config access) and the three modules
# don't share a common let-binding scope. Keep the copies in sync.
@ -594,6 +798,8 @@ in
vm-stop = vmStopScript;
vm-shell = vmShellScript;
};
usbHelperLib = usbHelperLib;
};
};
}