Skip to content

Commit 4e2fe41

Browse files
author
Paul C
committed
feat: WolfUSB integration — USB over IP device sharing
New node-level page for managing USB devices shared across the network via WolfUSB. Includes install, config (bind/port/auth key), device listing, and assignment to Docker containers, LXC containers, and VMs. Backend: wolfusb module with config persistence, service management, device enumeration (wolfusb list + lsusb fallback), assign/unassign with LXC cgroup passthrough and Docker --device guidance. Frontend: USB Sharing page with install button, config form, device table with target picker dropdown (grouped by Docker/LXC/VM), active assignments table. Node menu sorted alphabetically. API: GET/POST /api/wolfusb/config, GET /api/wolfusb/status, POST /api/wolfusb/install, GET /api/wolfusb/devices, POST /api/wolfusb/assign, POST /api/wolfusb/unassign.
1 parent 966b731 commit 4e2fe41

4 files changed

Lines changed: 882 additions & 38 deletions

File tree

src/api/mod.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9696,6 +9696,116 @@ pub async fn wolfflow_all_containers_exec(req: HttpRequest, state: web::Data<App
96969696
}
96979697
}
96989698

9699+
// ─── WolfUSB API ───
9700+
9701+
/// GET /api/wolfusb/status — installation status, service status, config
9702+
pub async fn wolfusb_status(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
9703+
if let Err(resp) = require_auth(&req, &state) { return resp; }
9704+
let config = crate::wolfusb::WolfUsbConfig::load();
9705+
HttpResponse::Ok().json(serde_json::json!({
9706+
"installed": crate::wolfusb::is_installed(),
9707+
"version": crate::wolfusb::installed_version(),
9708+
"running": crate::wolfusb::is_running(),
9709+
"config": {
9710+
"enabled": config.enabled,
9711+
"bind_address": config.bind_address,
9712+
"port": config.port,
9713+
"has_auth_key": !config.auth_key.is_empty(),
9714+
},
9715+
"assignment_count": config.assignments.len(),
9716+
}))
9717+
}
9718+
9719+
/// POST /api/wolfusb/install — install WolfUSB from setup script
9720+
pub async fn wolfusb_install(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
9721+
if let Err(resp) = require_auth(&req, &state) { return resp; }
9722+
match crate::wolfusb::install().await {
9723+
Ok(output) => HttpResponse::Ok().json(serde_json::json!({ "ok": true, "output": output })),
9724+
Err(e) => HttpResponse::Ok().json(serde_json::json!({ "ok": false, "error": e })),
9725+
}
9726+
}
9727+
9728+
/// GET /api/wolfusb/config — get full config
9729+
pub async fn wolfusb_get_config(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
9730+
if let Err(resp) = require_auth(&req, &state) { return resp; }
9731+
let config = crate::wolfusb::WolfUsbConfig::load();
9732+
HttpResponse::Ok().json(&config)
9733+
}
9734+
9735+
/// POST /api/wolfusb/config — save config and optionally start/stop service
9736+
pub async fn wolfusb_save_config(req: HttpRequest, state: web::Data<AppState>, body: web::Json<serde_json::Value>) -> HttpResponse {
9737+
if let Err(resp) = require_auth(&req, &state) { return resp; }
9738+
let mut config = crate::wolfusb::WolfUsbConfig::load();
9739+
9740+
if let Some(v) = body.get("enabled").and_then(|v| v.as_bool()) { config.enabled = v; }
9741+
if let Some(v) = body.get("bind_address").and_then(|v| v.as_str()) { config.bind_address = v.to_string(); }
9742+
if let Some(v) = body.get("port").and_then(|v| v.as_u64()) { config.port = v as u16; }
9743+
if let Some(v) = body.get("auth_key").and_then(|v| v.as_str()) {
9744+
if !v.contains("••••") { config.auth_key = v.to_string(); }
9745+
}
9746+
9747+
if let Err(e) = config.save() {
9748+
return HttpResponse::InternalServerError().json(serde_json::json!({ "error": e }));
9749+
}
9750+
9751+
// Start or stop service based on enabled flag
9752+
if config.enabled && crate::wolfusb::is_installed() {
9753+
if let Err(e) = crate::wolfusb::start_service(&config) {
9754+
return HttpResponse::Ok().json(serde_json::json!({ "ok": true, "warning": format!("Config saved but failed to start service: {}", e) }));
9755+
}
9756+
} else if !config.enabled {
9757+
let _ = crate::wolfusb::stop_service();
9758+
}
9759+
9760+
HttpResponse::Ok().json(serde_json::json!({ "ok": true }))
9761+
}
9762+
9763+
/// GET /api/wolfusb/devices — list USB devices on this node
9764+
pub async fn wolfusb_devices(req: HttpRequest, state: web::Data<AppState>) -> HttpResponse {
9765+
if let Err(resp) = require_auth(&req, &state) { return resp; }
9766+
let config = crate::wolfusb::WolfUsbConfig::load();
9767+
let devices = crate::wolfusb::list_local_devices(&config);
9768+
HttpResponse::Ok().json(serde_json::json!({
9769+
"devices": devices,
9770+
"assignments": config.assignments,
9771+
}))
9772+
}
9773+
9774+
/// POST /api/wolfusb/assign — assign a USB device to a container/VM
9775+
pub async fn wolfusb_assign(req: HttpRequest, state: web::Data<AppState>, body: web::Json<serde_json::Value>) -> HttpResponse {
9776+
if let Err(resp) = require_auth(&req, &state) { return resp; }
9777+
let mut config = crate::wolfusb::WolfUsbConfig::load();
9778+
9779+
let bus = body.get("bus").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
9780+
let address = body.get("address").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
9781+
let label = body.get("label").and_then(|v| v.as_str()).unwrap_or("");
9782+
let target_type = body.get("target_type").and_then(|v| v.as_str()).unwrap_or("");
9783+
let target_name = body.get("target_name").and_then(|v| v.as_str()).unwrap_or("");
9784+
9785+
if bus == 0 || target_type.is_empty() || target_name.is_empty() {
9786+
return HttpResponse::BadRequest().json(serde_json::json!({ "error": "bus, target_type, and target_name are required" }));
9787+
}
9788+
9789+
match crate::wolfusb::assign_device(&mut config, bus, address, label, target_type, target_name) {
9790+
Ok(msg) => HttpResponse::Ok().json(serde_json::json!({ "ok": true, "message": msg })),
9791+
Err(e) => HttpResponse::Ok().json(serde_json::json!({ "ok": false, "error": e })),
9792+
}
9793+
}
9794+
9795+
/// POST /api/wolfusb/unassign — remove a USB device assignment
9796+
pub async fn wolfusb_unassign(req: HttpRequest, state: web::Data<AppState>, body: web::Json<serde_json::Value>) -> HttpResponse {
9797+
if let Err(resp) = require_auth(&req, &state) { return resp; }
9798+
let mut config = crate::wolfusb::WolfUsbConfig::load();
9799+
9800+
let bus = body.get("bus").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
9801+
let address = body.get("address").and_then(|v| v.as_u64()).unwrap_or(0) as u8;
9802+
9803+
match crate::wolfusb::unassign_device(&mut config, bus, address) {
9804+
Ok(msg) => HttpResponse::Ok().json(serde_json::json!({ "ok": true, "message": msg })),
9805+
Err(e) => HttpResponse::Ok().json(serde_json::json!({ "ok": false, "error": e })),
9806+
}
9807+
}
9808+
96999809
// ─── MySQL Database Editor API ───
97009810

97019811
/// GET /api/mysql/detect — check if MySQL is installed on this node
@@ -16092,6 +16202,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
1609216202
.route("/api/wolfflow/container-exec", web::post().to(wolfflow_container_exec))
1609316203
.route("/api/wolfflow/all-containers-exec", web::post().to(wolfflow_all_containers_exec))
1609416204
.route("/api/wolfflow/infrastructure", web::get().to(wolfflow_infrastructure))
16205+
// WolfUSB — USB over IP
16206+
.route("/api/wolfusb/status", web::get().to(wolfusb_status))
16207+
.route("/api/wolfusb/install", web::post().to(wolfusb_install))
16208+
.route("/api/wolfusb/config", web::get().to(wolfusb_get_config))
16209+
.route("/api/wolfusb/config", web::post().to(wolfusb_save_config))
16210+
.route("/api/wolfusb/devices", web::get().to(wolfusb_devices))
16211+
.route("/api/wolfusb/assign", web::post().to(wolfusb_assign))
16212+
.route("/api/wolfusb/unassign", web::post().to(wolfusb_unassign))
1609516213
// VR Terminal
1609616214
.route("/api/vr-terminal/create", web::post().to(crate::vr_terminal::vr_term_create))
1609716215
.route("/api/vr-terminal/{id}/output", web::get().to(crate::vr_terminal::vr_term_output))

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mod icons;
3838
mod tui;
3939
mod wolfflow;
4040
mod wolfnote;
41+
mod wolfusb;
4142
mod paths;
4243
mod compat;
4344
mod plugins;

0 commit comments

Comments
 (0)