diff --git a/ADOPTERS.md b/ADOPTERS.md index 425b13a81..008fa9b14 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -6,4 +6,4 @@ To add your organization to this list, feel free to PR the additions to this pag | **Organization** | **Reference or Description of Usage** | |------------------|---------------------------------------| -| | | +| [Philips](https://www.philips.com) | Using Akri to dynamically discover and manage mice, keyboards, and USB storage devices at the edge. | diff --git a/Cargo.lock b/Cargo.lock index 5c415fa9c..5aaf66af8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -215,7 +215,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "agent" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-debug-echo", "akri-discovery-utils", @@ -279,7 +279,7 @@ dependencies = [ [[package]] name = "akri-debug-echo" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-discovery-utils", "akri-shared", @@ -297,7 +297,7 @@ dependencies = [ [[package]] name = "akri-discovery-utils" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-shared", "anyhow", @@ -319,7 +319,7 @@ dependencies = [ [[package]] name = "akri-onvif" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-discovery-utils", "anyhow", @@ -348,7 +348,7 @@ dependencies = [ [[package]] name = "akri-opcua" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-discovery-utils", "anyhow", @@ -368,7 +368,7 @@ dependencies = [ [[package]] name = "akri-shared" -version = "0.13.22" +version = "0.13.23" dependencies = [ "anyhow", "async-trait", @@ -393,7 +393,7 @@ dependencies = [ [[package]] name = "akri-udev" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-discovery-utils", "anyhow", @@ -912,7 +912,7 @@ dependencies = [ [[package]] name = "controller" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-shared", "anyhow", @@ -1063,7 +1063,7 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "debug-echo-discovery-handler" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-debug-echo", "akri-discovery-utils", @@ -2415,7 +2415,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "onvif-discovery-handler" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-discovery-utils", "akri-onvif", @@ -2464,7 +2464,7 @@ dependencies = [ [[package]] name = "opcua-discovery-handler" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-discovery-utils", "akri-opcua", @@ -3977,7 +3977,7 @@ dependencies = [ [[package]] name = "udev-discovery-handler" -version = "0.13.22" +version = "0.13.23" dependencies = [ "akri-discovery-utils", "akri-udev", @@ -4279,7 +4279,7 @@ dependencies = [ [[package]] name = "webhook-configuration" -version = "0.13.22" +version = "0.13.23" dependencies = [ "actix-rt", "actix-web", diff --git a/Cargo.toml b/Cargo.toml index ced721756..e4c2b0876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.13.22" +version = "0.13.23" edition = "2024" license = "Apache-2.0" homepage = "https://docs.akri.sh/" diff --git a/deployment/helm/Chart.yaml b/deployment/helm/Chart.yaml index 3c13b4c83..2ba6c802d 100644 --- a/deployment/helm/Chart.yaml +++ b/deployment/helm/Chart.yaml @@ -16,9 +16,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.13.22 +version: 0.13.23 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: 0.13.22 +appVersion: 0.13.23 diff --git a/discovery-handlers/udev/src/device_utils.rs b/discovery-handlers/udev/src/device_utils.rs new file mode 100644 index 000000000..9328f4619 --- /dev/null +++ b/discovery-handlers/udev/src/device_utils.rs @@ -0,0 +1,171 @@ +fn transform_resource_name(resource_name: &str) -> String { + resource_name + .chars() + .map(|c| match c { + '.' | '/' => '_', + other => other.to_ascii_uppercase(), + }) + .collect() +} + +pub fn to_usb_resource_env_var(resource_name: &str) -> String { + format!( + "{}_{}", + super::USB_RESOURCE_PREFIX, + transform_resource_name(resource_name) + ) +} + +pub fn to_pci_resource_env_var(resource_name: &str) -> String { + format!( + "{}_{}", + super::PCI_RESOURCE_PREFIX, + transform_resource_name(resource_name) + ) +} + +pub fn extract_usb_address(devnode: &str) -> Option<(String, String)> { + if !devnode.starts_with("/dev/bus/usb/") { + return None; + } + + let parts: Vec<&str> = devnode.split('/').collect(); + if parts.len() >= 2 { + let bus = parts[parts.len() - 2]; + let device = parts[parts.len() - 1]; + + if let (Ok(bus_num), Ok(dev_num)) = (bus.parse::(), device.parse::()) { + return Some((bus_num.to_string(), dev_num.to_string())); + } + } + + None +} + +fn is_pci_address(s: &str) -> bool { + let parts: Vec<&str> = s.splitn(3, ':').collect(); + if parts.len() != 3 { + return false; + } + let slot_func: Vec<&str> = parts[2].splitn(2, '.').collect(); + if slot_func.len() != 2 { + return false; + } + [parts[0], parts[1], slot_func[0], slot_func[1]] + .iter() + .all(|seg| !seg.is_empty() && seg.chars().all(|c| c.is_ascii_hexdigit())) +} + +pub fn extract_pci_address(sysfs_path: &str) -> Option { + sysfs_path + .split('/') + .filter(|s| is_pci_address(s)) + .next_back() + .map(|s| s.to_string()) +} + +pub fn read_iommu_group(devpath: &str) -> Option { + let full_path = format!("/sys{devpath}"); + let iommu_link = std::path::Path::new(&full_path).join("iommu_group"); + std::fs::read_link(&iommu_link).ok().and_then(|target| { + target + .file_name() + .and_then(|n| n.to_str()) + .map(|s| s.to_string()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_usb_resource_env_var() { + assert_eq!( + to_usb_resource_env_var("akri.sh/udev-usb-keyboard"), + "USB_RESOURCE_AKRI_SH_UDEV-USB-KEYBOARD" + ); + assert_eq!( + to_usb_resource_env_var("akri.sh/udev-usb-mouse"), + "USB_RESOURCE_AKRI_SH_UDEV-USB-MOUSE" + ); + assert_eq!( + to_usb_resource_env_var("example.com/my-device"), + "USB_RESOURCE_EXAMPLE_COM_MY-DEVICE" + ); + } + + #[test] + fn test_to_pci_resource_env_var() { + assert_eq!( + to_pci_resource_env_var("example.com/my-gpu"), + "PCI_RESOURCE_EXAMPLE_COM_MY-GPU" + ); + assert_eq!( + to_pci_resource_env_var("akri.sh/udev-gpu-t400e"), + "PCI_RESOURCE_AKRI_SH_UDEV-GPU-T400E" + ); + assert_eq!( + to_pci_resource_env_var("vendor.io/device"), + "PCI_RESOURCE_VENDOR_IO_DEVICE" + ); + } + + #[test] + fn test_extract_pci_address() { + assert_eq!( + extract_pci_address("/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0"), + Some("0000:01:00.0".to_string()) + ); + assert_eq!( + extract_pci_address("/sys/devices/pci0000:00/0000:03:00.0"), + Some("0000:03:00.0".to_string()) + ); + assert_eq!(extract_pci_address("/dev/bus/usb/001/010"), None); + assert_eq!(extract_pci_address("/dev/video0"), None); + } + + #[test] + fn test_extract_usb_address_valid() { + assert_eq!( + extract_usb_address("/dev/bus/usb/001/010"), + Some(("1".to_string(), "10".to_string())) + ); + + assert_eq!( + extract_usb_address("/dev/bus/usb/002/005"), + Some(("2".to_string(), "5".to_string())) + ); + + assert_eq!( + extract_usb_address("/dev/bus/usb/003/127"), + Some(("3".to_string(), "127".to_string())) + ); + } + + #[test] + fn test_extract_usb_address_invalid() { + assert_eq!(extract_usb_address("/dev/video0"), None); + assert_eq!(extract_usb_address("/dev/sda"), None); + assert_eq!(extract_usb_address("/dev/ttyUSB0"), None); + + assert_eq!(extract_usb_address("/dev/bus/usb/"), None); + assert_eq!(extract_usb_address("/dev/bus/usb/abc/def"), None); + + assert_eq!(extract_usb_address(""), None); + assert_eq!(extract_usb_address("/"), None); + } + + #[test] + fn test_extract_usb_address_edge_cases() { + assert_eq!( + extract_usb_address("/dev/bus/usb/1/5"), + Some(("1".to_string(), "5".to_string())) + ); + + assert_eq!( + extract_usb_address("/dev/bus/usb/001/001"), + Some(("1".to_string(), "1".to_string())) + ); + } +} diff --git a/discovery-handlers/udev/src/discovery_handler.rs b/discovery-handlers/udev/src/discovery_handler.rs index be630a204..dc86b0ded 100644 --- a/discovery-handlers/udev/src/discovery_handler.rs +++ b/discovery-handlers/udev/src/discovery_handler.rs @@ -87,6 +87,19 @@ impl DiscoveryHandler for DiscoveryHandlerImpl { let discovery_handler_config: UdevDiscoveryDetails = deserialize_discovery_details(&discover_request.discovery_details) .map_err(|e| tonic::Status::new(tonic::Code::InvalidArgument, format!("{e}")))?; + let device_plugin_resource_name: Option = discover_request + .discovery_properties + .get(super::DEVICE_PLUGIN_RESOURCE_PROPERTY_KEY) + .and_then(|b| b.vec.as_ref()) + .and_then(|v| std::str::from_utf8(v).ok()) + .map(|s| s.trim().to_string()); + let vfio_passthrough: bool = discover_request + .discovery_properties + .get(super::VFIO_PASSTHROUGH_PROPERTY_KEY) + .and_then(|b| b.vec.as_ref()) + .and_then(|v| std::str::from_utf8(v).ok()) + .map(|s| s.trim().eq_ignore_ascii_case("true")) + .unwrap_or(false); let mut previously_discovered_devices: Vec = Vec::new(); tokio::spawn(async move { let udev_rules = discovery_handler_config.udev_rules.clone(); @@ -130,6 +143,16 @@ impl DiscoveryHandler for DiscoveryHandlerImpl { super::UDEV_DEVNODE_LABEL_ID.to_string() + &property_suffix, devnode.clone(), ); + + if let Some(ref resource_name) = device_plugin_resource_name { + if let Some((bus, device)) = super::device_utils::extract_usb_address(&devnode) { + let env_var = super::device_utils::to_usb_resource_env_var(resource_name); + let value = format!("{bus}:{device}"); + trace!("discover - USB resource: {env_var}={value} path={devnode}"); + properties.insert(env_var + &property_suffix, value); + } + } + device_specs.push(DeviceSpec { container_path: devnode.clone(), host_path: devnode, @@ -141,6 +164,36 @@ impl DiscoveryHandler for DiscoveryHandlerImpl { //id is the sysfs path of the most top level device so we only need this one properties.insert(super::UDEV_DEVPATH_LABEL_ID.to_string(), id.clone()); + let is_usb_device = id.split('/').any(|s| s.starts_with("usb")); + if let Some(ref resource_name) = device_plugin_resource_name { + if !is_usb_device { + if let Some(pci_addr) = super::device_utils::extract_pci_address(&id) { + let env_var = super::device_utils::to_pci_resource_env_var(resource_name); + trace!("discover - PCI resource: {env_var}={pci_addr} path={id}"); + properties.insert(env_var, pci_addr); + + if vfio_passthrough { + if let Some(iommu_group) = super::device_utils::read_iommu_group(&id) { + let vfio_group = format!("/dev/vfio/{iommu_group}"); + trace!("discover - PCI VFIO group: {vfio_group} path={id}"); + device_specs.push(DeviceSpec { + container_path: vfio_group.clone(), + host_path: vfio_group, + permissions: "rwm".to_string(), + }); + device_specs.push(DeviceSpec { + container_path: "/dev/vfio/vfio".to_string(), + host_path: "/dev/vfio/vfio".to_string(), + permissions: "rwm".to_string(), + }); + } else { + trace!("discover - PCI device {id} has no IOMMU group (not vfio-pci bound?), skipping vfio DeviceSpec"); + } + } + } + } + } + // TODO: use device spec Device { id, diff --git a/discovery-handlers/udev/src/discovery_impl.rs b/discovery-handlers/udev/src/discovery_impl.rs index 1700221a5..fdf1e2441 100644 --- a/discovery-handlers/udev/src/discovery_impl.rs +++ b/discovery-handlers/udev/src/discovery_impl.rs @@ -153,10 +153,25 @@ fn find_devices( let device_devpaths: Vec = final_devices .into_iter() .map(|device| { - ( - get_devpath(&device).to_str().unwrap().to_string(), - get_devnode(&device).map(|devnode| devnode.to_str().unwrap().to_string()), - ) + if let Some(devnode) = get_devnode(&device) { + ( + get_devpath(&device).to_str().unwrap().to_string(), + Some(devnode.to_str().unwrap().to_string()), + ) + } else { + let devpath = get_devpath(&device).to_str().unwrap().to_string(); + let mut current = device.mockable_parent(); + while let Some(parent) = current { + if let Some(node) = parent.mockable_devnode() { + return ( + parent.mockable_devpath().to_str().unwrap().to_string(), + Some(node.to_str().unwrap().to_string()), + ); + } + current = parent.mockable_parent(); + } + (devpath, None) + } }) .collect(); diff --git a/discovery-handlers/udev/src/lib.rs b/discovery-handlers/udev/src/lib.rs index 433ca4fd8..21feac49f 100644 --- a/discovery-handlers/udev/src/lib.rs +++ b/discovery-handlers/udev/src/lib.rs @@ -5,6 +5,7 @@ extern crate udev; #[macro_use] extern crate serde_derive; +pub mod device_utils; pub mod discovery_handler; mod discovery_impl; mod wrappers; @@ -15,6 +16,14 @@ pub const UDEV_DEVNODE_LABEL_ID: &str = "UDEV_DEVNODE"; /// Name of environment variable that is set in udev brokers. Contains devpath for udev device /// the broker should connect to. pub const UDEV_DEVPATH_LABEL_ID: &str = "UDEV_DEVPATH"; +/// Prefix for USB resource ENV variable (e.g. USB_RESOURCE_AKRI_SH_UDEV_USB_GENERIC) +pub const USB_RESOURCE_PREFIX: &str = "USB_RESOURCE"; +/// Prefix for PCI resource ENV variable (e.g. PCI_RESOURCE_AKRI_SH_UDEV_GPU_T400E) +pub const PCI_RESOURCE_PREFIX: &str = "PCI_RESOURCE"; +/// Key used to pass the Kubernetes Device Plugin resource name +pub const DEVICE_PLUGIN_RESOURCE_PROPERTY_KEY: &str = "devicePluginResourceName"; +/// Key used to enable VFIO PCI passthrough DeviceSpec injection. +pub const VFIO_PASSTHROUGH_PROPERTY_KEY: &str = "vfioPassthrough"; /// Name that udev discovery handlers use when registering with the Agent pub const DISCOVERY_HANDLER_NAME: &str = "udev"; /// Defines whether this discovery handler discovers local devices on nodes rather than ones visible to multiple nodes diff --git a/version.sh b/version.sh index fc204888d..2c63995e9 100755 --- a/version.sh +++ b/version.sh @@ -129,7 +129,7 @@ if [ "$CHECK" == "1" ]; then check_file_version "$BASEDIR/Cargo.toml" "$TOML_VERSION_PATTERN" "$TOML_VERSION" if [ "$?" -eq "1" ]; then exit 1; fi - CARGO_LOCK_PROJECTS="controller akri-shared agent controller webhook-configuration udev-video-broker akri-discovery-utils akri-debug-echo akri-udev akri-onvif akri-opcua debug-echo-discovery-handler onvif-discovery-handler udev-discovery-handler opcua-discovery-handler" + CARGO_LOCK_PROJECTS="controller akri-shared agent controller webhook-configuration akri-discovery-utils akri-debug-echo akri-udev akri-onvif akri-opcua debug-echo-discovery-handler onvif-discovery-handler udev-discovery-handler opcua-discovery-handler" CARGO_LOCK_VERSION="\"$(echo $VERSION)\"" for CARGO_LOCK_PROJECT in $CARGO_LOCK_PROJECTS do diff --git a/version.txt b/version.txt index 51252bbc2..a703836a4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.13.22 +0.13.23