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:
parent
4ef549b981
commit
609eccae4a
2 changed files with 212 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue