container_device_interface/
cache.rs1use std::{
2 cell::RefCell,
3 collections::{HashMap, HashSet},
4 error::Error,
5 fmt,
6 sync::{Arc, Mutex},
7};
8
9use anyhow::Result;
10
11use oci_spec::runtime as oci;
12
13use crate::{
14 container_edits::ContainerEdits,
16 device::Device,
17 spec::Spec,
18 spec_dirs::{convert_errors, scan_spec_dirs, with_spec_dirs, SpecError, DEFAULT_SPEC_DIRS},
19};
20
21#[derive(Debug)]
23struct ConflictError {
24 name: String,
25 dev_path: String,
26 old_path: String,
27}
28
29impl ConflictError {
30 fn new(name: &str, dev_path: &str, old_path: &str) -> Self {
31 Self {
32 name: name.to_owned(),
33 dev_path: dev_path.to_owned(),
34 old_path: old_path.to_owned(),
35 }
36 }
37}
38
39impl fmt::Display for ConflictError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 write!(
42 f,
43 "conflicting device {} (specs {}, {})",
44 self.name, self.dev_path, self.old_path
45 )
46 }
47}
48
49impl Error for ConflictError {}
50
51pub type CdiOption = Box<dyn FnOnce(&mut Cache)>;
55
56pub fn with_auto_refresh(auto_refresh: bool) -> CdiOption {
62 Box::new(move |c: &mut Cache| {
63 c.auto_refresh = auto_refresh;
64 })
65}
66
67#[allow(dead_code)]
68#[derive(Default)]
69pub struct Cache {
70 pub spec_dirs: Vec<String>,
71 pub specs: HashMap<String, Vec<Spec>>,
72 pub devices: HashMap<String, Device>,
73 pub errors: HashMap<String, Vec<Box<dyn std::error::Error + Send + Sync + 'static>>>,
74 pub dir_errors: HashMap<String, Box<dyn std::error::Error + Send + Sync + 'static>>,
75
76 pub auto_refresh: bool,
77 }
79
80pub fn new_cache(options: Vec<CdiOption>) -> Arc<Mutex<Cache>> {
81 let cache = Arc::new(Mutex::new(Cache::default()));
82
83 {
84 let mut c = cache.lock().unwrap();
85
86 with_spec_dirs(&DEFAULT_SPEC_DIRS)(&mut c);
87 c.configure(options);
88 let _ = c.refresh();
89 } cache
92}
93
94impl Cache {
95 pub fn new(
96 spec_dirs: Vec<String>,
97 specs: HashMap<String, Vec<Spec>>,
98 devices: HashMap<String, Device>,
99 ) -> Self {
100 Self {
101 spec_dirs,
102 specs,
103 devices,
104 errors: HashMap::new(),
105 dir_errors: HashMap::new(),
106 auto_refresh: false,
107 }
109 }
110
111 pub fn configure(&mut self, options: Vec<CdiOption>) {
112 for option in options {
113 option(self);
114 }
115 }
116
117 pub fn get_device(&mut self, dev_name: &str) -> Option<&Device> {
118 let _ = self.refresh_if_required(false);
119
120 self.devices.get(dev_name)
121 }
122
123 pub fn list_devices(&mut self) -> Vec<String> {
124 let _ = self.refresh_if_required(false);
125
126 let mut devices: Vec<String> = self.devices.keys().cloned().collect();
127 devices.sort();
128 devices
129 }
130
131 pub fn list_vendors(&mut self) -> Vec<String> {
132 let mut vendors: Vec<String> = Vec::new();
133
134 let _ = self.refresh_if_required(false);
135
136 for vendor in self.specs.keys() {
137 vendors.push(vendor.clone());
138 }
139 vendors.sort();
140 vendors
141 }
142
143 pub fn get_vendor_specs(&mut self, vendor: &str) -> Vec<Spec> {
144 let _ = self.refresh_if_required(false);
145
146 match self.specs.get(vendor) {
147 Some(specs) => specs.clone(),
148 None => Vec::new(),
149 }
150 }
151
152 pub fn refresh(&mut self) -> Result<(), Box<dyn Error>> {
154 let mut specs: HashMap<String, Vec<Spec>> = HashMap::new();
155 let mut devices: HashMap<String, Device> = HashMap::new();
156 let mut conflicts: HashSet<String> = HashSet::new();
157 let mut spec_errors: HashMap<String, Vec<Box<dyn Error>>> = HashMap::new();
158
159 let collect_error = RefCell::new(|err: Box<dyn Error>, paths: Vec<String>| {
161 let err_string = err.to_string();
162 for path in paths {
163 spec_errors
164 .entry(path.to_string())
165 .or_default()
166 .push(Box::new(SpecError::new(&err_string.to_string())));
167 }
168 });
169
170 let resolve_conflict = RefCell::new(|name: &str, dev: &Device, old: &Device| -> bool {
171 let dev_spec = dev.get_spec();
172 let old_spec = old.get_spec();
173 let dev_prio = dev_spec.get_priority();
174 let old_prio = old_spec.get_priority();
175
176 match dev_prio.cmp(&old_prio) {
177 std::cmp::Ordering::Greater => false,
178 std::cmp::Ordering::Equal => {
179 let dev_path = dev_spec.get_path();
180 let old_path = old_spec.get_path();
181 collect_error.borrow_mut()(
182 Box::new(ConflictError::new(name, &dev_path, &old_path)),
183 vec![dev_path.clone(), old_path.clone()],
184 );
185 conflicts.insert(name.to_owned());
186 true
187 }
188 std::cmp::Ordering::Less => true,
189 }
190 });
191
192 let mut scan_spec_fn = |s: Spec| -> Result<(), Box<dyn Error>> {
193 let vendor = s.get_vendor().to_owned();
194 specs.entry(vendor.clone()).or_default().push(s.clone());
195 let spec_devices = s.get_devices();
196 for dev in spec_devices.values() {
197 let qualified = dev.get_qualified_name();
198 if let Some(other) = devices.get(&qualified) {
199 if resolve_conflict.borrow_mut()(&qualified, dev, other) {
200 continue;
201 }
202 }
203 devices.insert(qualified, dev.clone());
204 }
205
206 Ok(())
207 };
208
209 let scaned_specs: Vec<Spec> = scan_spec_dirs(&self.spec_dirs)?;
210 for spec in scaned_specs {
211 scan_spec_fn(spec)?
212 }
213
214 for conflict in conflicts.iter() {
215 self.devices.remove(conflict);
216 }
217
218 self.specs = specs;
219 self.devices = devices;
220 self.errors = convert_errors(&spec_errors);
221
222 let errs: Vec<String> = spec_errors
223 .values()
224 .flat_map(|errors| errors.iter().map(|err| err.to_string()))
225 .collect();
226
227 if !errs.is_empty() {
228 Err(errs.join(", ").into())
229 } else {
230 Ok(())
231 }
232 }
233
234 fn refresh_if_required(&mut self, force: bool) -> Result<bool, Box<dyn std::error::Error>> {
235 if force || (self.auto_refresh) {
241 self.refresh()?;
242 return Ok(true);
243 }
244
245 Ok(false)
246 }
247
248 pub fn inject_devices(
249 &mut self,
250 oci_spec: Option<&mut oci::Spec>,
251 devices: Vec<String>,
252 ) -> Result<Vec<String>, Box<dyn Error + Send + Sync + 'static>> {
253 let mut unresolved = Vec::new();
254
255 let oci_spec = match oci_spec {
256 Some(spec) => spec,
257 None => return Err("can't inject devices, OCI Spec is empty".into()),
258 };
259
260 let _ = self.refresh_if_required(false);
261
262 let edits = &mut ContainerEdits::new();
263 let mut specs: HashSet<Spec> = HashSet::new();
264
265 for device in devices {
266 if let Some(dev) = self.devices.get(&device) {
267 let mut spec = dev.get_spec();
268 if specs.insert(spec.clone()) {
269 if let Some(ce) = spec.edits() {
272 edits.append(ce)?
273 }
274 }
275 edits.append(dev.edits())?;
276 } else {
277 unresolved.push(device);
278 }
279 }
280
281 if !unresolved.is_empty() {
282 return Err(format!("unresolvable CDI devices {}", unresolved.join(", ")).into());
283 }
284
285 if let Err(err) = edits.apply(oci_spec) {
286 return Err(format!("failed to inject devices: {}", err).into());
287 }
288
289 Ok(Vec::new())
290 }
291
292 pub fn get_errors(&self) -> HashMap<String, Vec<anyhow::Error>> {
293 HashMap::new()
295 }
296}
297
298#[cfg(test)]
299mod tests {
300 use super::*;
301 use crate::spec_dirs::with_spec_dirs;
302 use crate::{
303 spec::new_spec,
304 specs::config::{
305 ContainerEdits as CDIContainerEdits, Device as CDIDevice, DeviceNode, IntelRdt,
306 Spec as CDISpec,
307 },
308 };
309 use oci_spec::runtime::Spec as OCISpec;
310 use std::{collections::HashMap, fs, path::PathBuf};
311
312 fn spec_yaml(kind: &str, env: &str) -> String {
313 format!(
314 r#"cdiVersion: "0.6.0"
315kind: "{kind}"
316devices:
317 - name: "gpu0"
318 containerEdits:
319 env:
320 - "{env}"
321"#
322 )
323 }
324
325 fn dir_cache(dirs: &[&str]) -> Cache {
326 let mut cache = Cache::default();
327 with_spec_dirs(dirs)(&mut cache);
328 cache
329 }
330
331 #[test]
332 fn refresh_scans_dirs_and_answers_queries() {
333 let dir = tempfile::tempdir().unwrap();
334 fs::write(
335 dir.path().join("vendor.yaml"),
336 spec_yaml("vendor.com/device", "VENDOR=1"),
337 )
338 .unwrap();
339 let mut cache = dir_cache(&[dir.path().to_str().unwrap()]);
340
341 cache.refresh().unwrap();
342
343 assert_eq!(cache.list_devices(), vec!["vendor.com/device=gpu0"]);
344 assert_eq!(cache.list_vendors(), vec!["vendor.com"]);
345 assert_eq!(cache.get_vendor_specs("vendor.com").len(), 1);
346 assert!(cache.get_vendor_specs("other.com").is_empty());
347 assert!(cache.get_device("vendor.com/device=gpu0").is_some());
348 assert!(cache.get_device("vendor.com/device=missing").is_none());
349 }
350
351 #[test]
352 fn auto_refresh_picks_up_new_specs_without_manual_refresh() {
353 let dir = tempfile::tempdir().unwrap();
354 let mut cache = dir_cache(&[dir.path().to_str().unwrap()]);
355 with_auto_refresh(true)(&mut cache);
356 assert!(cache.list_devices().is_empty());
357
358 fs::write(
359 dir.path().join("vendor.yaml"),
360 spec_yaml("vendor.com/device", "VENDOR=1"),
361 )
362 .unwrap();
363
364 assert_eq!(cache.list_devices(), vec!["vendor.com/device=gpu0"]);
366 }
367
368 #[test]
369 fn later_dir_wins_on_conflicting_device_names() {
370 let low = tempfile::tempdir().unwrap();
371 let high = tempfile::tempdir().unwrap();
372 fs::write(
373 low.path().join("a.yaml"),
374 spec_yaml("vendor.com/device", "FROM=low"),
375 )
376 .unwrap();
377 fs::write(
378 high.path().join("b.yaml"),
379 spec_yaml("vendor.com/device", "FROM=high"),
380 )
381 .unwrap();
382 let mut cache = dir_cache(&[low.path().to_str().unwrap(), high.path().to_str().unwrap()]);
383
384 cache.refresh().unwrap();
385
386 let dev = cache.get_device("vendor.com/device=gpu0").unwrap();
387 assert_eq!(dev.get_spec().get_priority(), 1);
388 }
389
390 #[test]
391 fn same_priority_conflicts_are_reported() {
392 let dir = tempfile::tempdir().unwrap();
393 fs::write(
394 dir.path().join("a.yaml"),
395 spec_yaml("vendor.com/device", "FROM=a"),
396 )
397 .unwrap();
398 fs::write(
399 dir.path().join("b.yaml"),
400 spec_yaml("vendor.com/device", "FROM=b"),
401 )
402 .unwrap();
403 let mut cache = dir_cache(&[dir.path().to_str().unwrap()]);
404
405 let err = cache.refresh().unwrap_err();
406
407 assert!(err.to_string().contains("conflicting device"));
408 assert!(!cache.errors.is_empty());
409 }
410
411 #[test]
412 fn inject_devices_requires_an_oci_spec() {
413 let mut cache = Cache::default();
414 let err = cache.inject_devices(None, vec![]).unwrap_err();
415 assert!(err.to_string().contains("OCI Spec is empty"));
416 }
417
418 #[test]
419 fn inject_devices_reports_unresolvable_devices() {
420 let mut cache = Cache::default();
421 let mut oci_spec = OCISpec::default();
422 let err = cache
423 .inject_devices(Some(&mut oci_spec), vec!["vendor.com/device=nope".into()])
424 .unwrap_err();
425 assert!(err.to_string().contains("unresolvable CDI devices"));
426 assert!(err.to_string().contains("vendor.com/device=nope"));
427 }
428
429 #[test]
430 fn inject_devices_preserves_spec_level_intel_rdt_with_device_edits() {
431 let raw = CDISpec {
432 version: "1.1.0".to_string(),
433 kind: "vendor.com/device".to_string(),
434 container_edits: Some(CDIContainerEdits {
435 intel_rdt: Some(IntelRdt {
436 clos_id: Some("global-class".to_string()),
437 ..Default::default()
438 }),
439 ..Default::default()
440 }),
441 devices: vec![CDIDevice {
442 name: "gpu0".to_string(),
443 container_edits: CDIContainerEdits {
444 device_nodes: Some(vec![DeviceNode {
445 path: "/dev/null".to_string(),
446 r#type: Some("c".to_string()),
447 major: Some(1),
448 minor: Some(3),
449 ..Default::default()
450 }]),
451 ..Default::default()
452 },
453 ..Default::default()
454 }],
455 ..Default::default()
456 };
457 let spec = new_spec(&raw, &PathBuf::from("/tmp/vendor-device.yaml"), 0).unwrap();
458 let device = spec.get_device("gpu0").unwrap().clone();
459 let mut devices = HashMap::new();
460 devices.insert(device.get_qualified_name(), device);
461 let mut cache = Cache::new(Vec::new(), HashMap::new(), devices);
462 let mut oci_spec = OCISpec::default();
463
464 cache
465 .inject_devices(
466 Some(&mut oci_spec),
467 vec!["vendor.com/device=gpu0".to_string()],
468 )
469 .unwrap();
470
471 let intel_rdt = oci_spec
472 .linux()
473 .as_ref()
474 .unwrap()
475 .intel_rdt()
476 .as_ref()
477 .unwrap();
478 assert_eq!(
479 Some(&"global-class".to_string()),
480 intel_rdt.clos_id().as_ref()
481 );
482 }
483}