Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@
}
}
]
}
}

23 changes: 23 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{ pkgs ? import <nixpkgs> {} }:

pkgs.mkShell {
buildInputs = [
pkgs.gcc
pkgs.python311Full
pkgs.python311Packages.virtualenv
pkgs.python311Packages.pyudev
pkgs.python311Packages.inotify-simple
pkgs.python311Packages.psutil
pkgs.python311Packages.pyudev
];

shellHook = ''
if [ ! -d .venv ]; then
virtualenv .venv
source .venv/bin/activate
else
source .venv/bin/activate
fi
echo "Welcome to your Python development environment."
'';
}
50 changes: 50 additions & 0 deletions testdata/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"usb": {
"hotplug_rules": {
"denylist": {
"0xbadb" : ["0xdada"],
"~0xbabb" : ["0xcaca"]
},

"allowlist" : {
"0x0b95:0x1790" : ["net-vm"]
},

"classlist" : {
"0x01:*:*" : ["audio-vm"],
"0x03:*:0x01" : ["gui-vm"],
"0x03:*:0x02" : ["gui-vm"],
"0x08:0x06:*" : ["gui-vm"],
"0x0b:*:*" : ["gui-vm"],
"0x11:*:*" : ["gui-vm"],
"0xe0:0x01:0x01" : ["gui-vm"],
"0x02:06:*" : ["net-vm"],
"0x0e:*:*" : ["chrome-vm"]
}
},
"static_devices" : [
{
"hostbus":null,
"hostport":null,
"name":"crazyradio1",
"productId":"0101",
"vendorId":"1915",
"vmUdevExtraRule":null,
"vms":["gui-vm"]
},
{
"hostbus":null,
"hostport":null,
"name":"crazyflie0",
"productId": "5740",
"vendorId":"0483",
"vmUdevExtraRule":null,
"vms":["test-vm"]
}
]
},
"eventDevices": {
"targetVM": "gui-vm",
"pcieBusPrefix":"rp"
}
}
23 changes: 23 additions & 0 deletions vhotplug/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,30 @@
import fcntl
import struct
import psutil
import os
import sys
import time
from vhotplug.qemulink import *

EVIOCGRAB = 0x40044590
EVIOCGNAME = 0x82004506

logger = logging.getLogger("vhotplug")

def wait_target_vm(qmp_socket, timeout=30, interval=3):
start = time.time()
while True:
if os.path.exists(qmp_socket):
logger.debug(f" Found qmpSocket {qmp_socket} ...")
break
if time.time() - start > timeout:
logger.debug(f"Timeout! qmpSocket {qmp_socket} not found.")
return True
logger.debug(f"Waiting for qmpSocket {qmp_socket} ...")
time.sleep(interval)
return False


def log_device(device, level=logging.DEBUG):
try:
logger.log(level, f"Device path: {device.device_path}")
Expand Down Expand Up @@ -116,6 +133,9 @@ async def attach_usb_device(context, config, device, use_vid_pid):
vm_name = vm.get("name")
qmp_socket = vm.get("qmpSocket")
logger.info(f"Attaching to {vm_name} ({qmp_socket})")
if wait_target_vm(qmp_socket):
logger.warning(f"VM:{vm_name} timeout! Couldn't retrieve {qmp_socket}")
return
if is_boot_device(context, device):
logger.info(f"USB drive {device.device_node} is used as a boot device, skipping")
return
Expand Down Expand Up @@ -143,6 +163,9 @@ async def remove_usb_device(config, device):
async def attach_evdev_device(vm, busprefix, pcieport, device):
vm_name = vm.get("name")
qmp_socket = vm.get("qmpSocket")
if wait_target_vm(qmp_socket):
logger.warning(f"VM:{vm_name} timeout! Couldn't retrieve {qmp_socket}")
return
bus = f"{busprefix}{pcieport}"
logger.info(f"Attaching evdev device to {vm_name} ({qmp_socket}) on bus {bus}")
qemu = QEMULink(qmp_socket)
Expand Down
125 changes: 125 additions & 0 deletions vhotplug/ghaf_dynamic_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import subprocess
import logging
import socket
import time
import json
import os

logger = logging.getLogger("vhotplug")

class GhafDynamicPolicy:
def __init__(self, admin_name, admin_addr, admin_port, policy_query, givc_cli, cert = None, key = None, cacert = None):

self.policy_json = None
self.admin_name = admin_name
self.admin_addr = admin_addr
self.admin_port = str(admin_port)
if cert is not None:
if not os.path.exists(cert):
raise FileNotFoundError(f"File {cert} does not exist.")
if not os.path.exists(key):
raise FileNotFoundError(f"File {key} does not exist.")
if not os.path.exists(cacert):
raise FileNotFoundError(f"File {cacert} does not exist.")
if not os.path.exists(givc_cli):
raise FileNotFoundError(f"File {givc_cli} does not exist.")
self.policy_query_cmd = [
givc_cli,
"--cert", cert,
"--key", key,
"--cacert", cacert,
"--name", self.admin_name,
"--addr", self.admin_addr,
"--port", self.admin_port,
"policy-query", f"{policy_query}"
]
else:
self.policy_query_cmd = [
givc_cli,
"--notls",
"--name", self.admin_name,
"--addr", self.admin_addr,
"--port", self.admin_port,
"policy-query", f"{policy_query}"
]


def __remove_comments(self, json_as_string):
result = ""
for line in json_as_string.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith('#'):
continue
# Remove inline comment
code_part = line.split('#', 1)[0].rstrip()
if code_part:
result += code_part + "\n"
return result

def __wait_for_admin(self, timeout=60, interval=2):
logger.info("Waiting for admin vm to become reachable...")

end_time = time.time() + timeout
while time.time() < end_time:
try:
with socket.create_connection((self.admin_addr, self.admin_port), timeout=3):
logger.info(f"Admin vm [{self.admin_addr}:{self.admin_port}] is reachable.")
return True
except (socket.timeout, ConnectionRefusedError, OSError):
logger.info(f"Admin vm [{self.admin_addr}:{self.admin_port}] is still not reachable.")
time.sleep(interval)

logger.error(f"Admin vm [{self.admin_addr}:{self.admin_port}] is not reachable. Timed out after {timeout} seconds!")
return False

def __fetch_hotplug_policy(self):
if self.__wait_for_admin() == None:
return None
try:
result = subprocess.run(
self.policy_query_cmd,
capture_output=True,
text=True,
check=True,
encoding='utf-8'
)
output_string = result.stdout.strip()
logger.debug(f"Raw USB Hotplug Policy received:\n{output_string}")

if not output_string:
logger.error("Error: Policy fetcher command returned empty output.")
return None

try:
outer = json.loads(output_string)
inner = None
if isinstance(outer, str):
inner = json.loads(outer)
else:
inner = outer

if isinstance(inner, dict) and "result" in inner:
self.policy_json = inner["result"]
else:
logger.error("Policy fetcher command returned unexpected output.")
return None
return self.policy_json

except json.JSONDecodeError as e:
logger.error(f"Failed to decode JSON from command output. JSONDecodeError: {e}")
logger.error(f"Raw output was:\n---\n{output_string}\n---")
return None

except subprocess.CalledProcessError as e:
logger.error(f"Command execution failed with exit code {e.returncode}.")
logger.error(f"Stderr:\n---\n{result.stderr}\n---")
return None

def get_policy(self):
if self.policy_json == None:
return self.__fetch_hotplug_policy()
else:
return self.policy_json

def reload_policy(self):
self.policy_json = None
Loading