1use std::collections::HashMap;
2use std::fs::{self, OpenOptions};
3use std::io::{BufRead, BufReader, Write};
4use std::path::{Path, PathBuf};
5use std::sync::LazyLock;
6
7use nix::unistd::Pid;
8use oci_spec::runtime::LinuxIntelRdt;
9use pathrs::flags::OpenFlags;
10use pathrs::procfs::{ProcfsBase, ProcfsHandle};
11use procfs::process::MountInfo;
12use regex::Regex;
13
14#[derive(Debug, thiserror::Error)]
15pub enum IntelRdtError {
16 #[error(transparent)]
17 ProcError(#[from] procfs::ProcError),
18 #[error("failed to find resctrl mount point")]
19 ResctrlMountPointNotFound,
20 #[error("failed to find ID for resctrl")]
21 ResctrlIdNotFound,
22 #[error("existing schemata found but data did not match")]
23 ExistingSchemataMismatch,
24 #[error("failed to read existing schemata")]
25 ReadSchemata(#[source] std::io::Error),
26 #[error("failed to write schemata")]
27 WriteSchemata(#[source] std::io::Error),
28 #[error("failed to open schemata file")]
29 OpenSchemata(#[source] std::io::Error),
30 #[error(transparent)]
31 ParseLine(#[from] ParseLineError),
32 #[error("no resctrl subdirectory found for container id")]
33 NoResctrlSubdirectory,
34 #[error("failed to remove subdirectory")]
35 RemoveSubdirectory(#[source] std::io::Error),
36 #[error("no parent for resctrl subdirectory")]
37 NoResctrlSubdirectoryParent,
38 #[error("invalid resctrl directory")]
39 InvalidResctrlDirectory,
40 #[error("resctrl closID directory didn't exist")]
41 NoClosIDDirectory,
42 #[error("failed to write to resctrl closID directory")]
43 WriteClosIDDirectory(#[source] std::io::Error),
44 #[error("failed to open resctrl closID directory")]
45 OpenClosIDDirectory(#[source] std::io::Error),
46 #[error("failed to create resctrl closID directory")]
47 CreateClosIDDirectory(#[source] std::io::Error),
48 #[error("failed to canonicalize path")]
49 Canonicalize(#[source] std::io::Error),
50 #[error(transparent)]
51 Pathrs(#[from] pathrs::error::Error),
52 #[error(transparent)]
53 Io(#[from] std::io::Error),
54}
55
56#[derive(Debug, thiserror::Error)]
57pub enum ParseLineError {
58 #[error("MB line doesn't match validation")]
59 MBLine,
60 #[error("MB token has wrong number of fields")]
61 MBToken,
62 #[error("L3 line doesn't match validation")]
63 L3Line,
64 #[error("L3 token has wrong number of fields")]
65 L3Token,
66}
67
68type Result<T> = std::result::Result<T, IntelRdtError>;
69
70pub fn delete_resctrl_subdirectory(id: &str) -> Result<()> {
71 let dir = find_resctrl_mount_point().map_err(|err| {
72 tracing::error!("failed to find resctrl mount point: {}", err);
73 err
74 })?;
75 let container_resctrl_path = dir.join(id).canonicalize().map_err(|err| {
76 tracing::error!(?dir, ?id, "failed to canonicalize path: {}", err);
77 IntelRdtError::Canonicalize(err)
78 })?;
79 match container_resctrl_path.parent() {
80 Some(parent) => {
83 if parent == dir && container_resctrl_path.exists() {
84 fs::remove_dir(&container_resctrl_path).map_err(|err| {
85 tracing::error!(path = ?container_resctrl_path, "failed to remove resctrl subdirectory: {}", err);
86 IntelRdtError::RemoveSubdirectory(err)
87 })?;
88 } else {
89 return Err(IntelRdtError::NoResctrlSubdirectory);
90 }
91 }
92 None => return Err(IntelRdtError::NoResctrlSubdirectoryParent),
93 }
94 Ok(())
95}
96
97pub fn find_resctrl_mount_point() -> Result<PathBuf> {
99 let reader = BufReader::new(ProcfsHandle::new()?.open(
100 ProcfsBase::ProcSelf,
101 "mountinfo",
102 OpenFlags::O_RDONLY | OpenFlags::O_CLOEXEC,
103 )?);
104
105 for lr in reader.lines() {
106 let s = lr.map_err(IntelRdtError::from)?;
107 let mi = MountInfo::from_line(&s).map_err(IntelRdtError::from)?;
108
109 if mi.fs_type == "resctrl" {
110 let path = mi
111 .mount_point
112 .canonicalize()
113 .map_err(IntelRdtError::Canonicalize)?;
114 return Ok(path);
115 }
116 }
117
118 Err(IntelRdtError::ResctrlMountPointNotFound)
119}
120
121fn write_container_pid_to_resctrl_tasks(
125 path: &Path,
126 id: &str,
127 init_pid: Pid,
128 only_clos_id_set: bool,
129) -> Result<bool> {
130 let tasks = path.to_owned().join(id).join("tasks");
131 let dir = tasks.parent();
132 match dir {
133 None => Err(IntelRdtError::InvalidResctrlDirectory),
134 Some(resctrl_container_dir) => {
135 let mut created_dir = false;
136 if !resctrl_container_dir.exists() {
137 if only_clos_id_set {
138 return Err(IntelRdtError::NoClosIDDirectory);
140 }
141 fs::create_dir_all(resctrl_container_dir).map_err(|err| {
142 tracing::error!("failed to create resctrl subdirectory: {}", err);
143 IntelRdtError::CreateClosIDDirectory(err)
144 })?;
145 created_dir = true;
146 }
147 let mut file = OpenOptions::new()
151 .create(true)
152 .append(true)
153 .open(tasks)
154 .map_err(|err| {
155 tracing::error!("failed to open resctrl tasks file: {}", err);
156 IntelRdtError::OpenClosIDDirectory(err)
157 })?;
158 write!(file, "{init_pid}").map_err(|err| {
159 tracing::error!("failed to write to resctrl tasks file: {}", err);
160 IntelRdtError::WriteClosIDDirectory(err)
161 })?;
162 Ok(created_dir)
163 }
164 }
165}
166
167fn combine_l3_cache_and_mem_bw_schemas(
170 l3_cache_schema: &Option<String>,
171 mem_bw_schema: &Option<String>,
172) -> Option<String> {
173 match (l3_cache_schema, mem_bw_schema) {
174 (Some(real_l3_cache_schema), Some(real_mem_bw_schema)) => {
175 let mut output: Vec<&str> = vec![];
177
178 for line in real_l3_cache_schema.lines() {
179 if line.starts_with("MB:") {
180 continue;
181 }
182 output.push(line);
183 }
184 output.push(real_mem_bw_schema);
185 Some(output.join("\n"))
186 }
187 (Some(_), None) => {
188 l3_cache_schema.to_owned()
190 }
191 (None, Some(_)) => mem_bw_schema.to_owned(),
192 (None, None) => None,
193 }
194}
195
196#[derive(PartialEq)]
197enum LineType {
198 L3Line,
199 L3DataLine,
200 L3CodeLine,
201 MbLine,
202 Unknown,
203}
204
205#[derive(PartialEq)]
206struct ParsedLine {
207 line_type: LineType,
208 tokens: HashMap<String, String>,
209}
210
211fn parse_mb_line(line: &str) -> std::result::Result<HashMap<String, String>, ParseLineError> {
213 let mut token_map = HashMap::new();
214
215 static MB_VALIDATE_RE: LazyLock<Regex> = LazyLock::new(|| {
216 Regex::new(r"^MB:(?:\s|;)*(?:\w+\s*=\s*\w+)?(?:(?:\s*;+\s*)+\w+\s*=\s*\w+)*(?:\s|;)*$")
217 .unwrap()
218 });
219 static MB_CAPTURE_RE: LazyLock<Regex> =
220 LazyLock::new(|| Regex::new(r"(\w+)\s*=\s*(\w+)").unwrap());
221
222 if !MB_VALIDATE_RE.is_match(line) {
223 return Err(ParseLineError::MBLine);
224 }
225
226 for token in MB_CAPTURE_RE.captures_iter(line) {
227 match (token.get(1), token.get(2)) {
228 (Some(key), Some(value)) => {
229 token_map.insert(key.as_str().to_string(), value.as_str().to_string());
230 }
231 _ => return Err(ParseLineError::MBToken),
232 }
233 }
234
235 Ok(token_map)
236}
237
238fn parse_l3_line(line: &str) -> std::result::Result<HashMap<String, String>, ParseLineError> {
240 let mut token_map = HashMap::new();
241
242 static L3_VALIDATE_RE: LazyLock<Regex> = LazyLock::new(|| {
243 Regex::new(r"^(?:L3|L3DATA|L3CODE):(?:\s|;)*(?:\w+\s*=\s*[[:xdigit:]]+)?(?:(?:\s*;+\s*)+\w+\s*=\s*[[:xdigit:]]+)*(?:\s|;)*$").unwrap()
244 });
245 static L3_CAPTURE_RE: LazyLock<Regex> =
246 LazyLock::new(|| Regex::new(r"(\w+)\s*=\s*0*([[:xdigit:]]+)").unwrap());
247 if !L3_VALIDATE_RE.is_match(line) {
253 return Err(ParseLineError::L3Line);
254 }
255
256 for token in L3_CAPTURE_RE.captures_iter(line) {
257 match (token.get(1), token.get(2)) {
258 (Some(key), Some(value)) => {
259 token_map.insert(key.as_str().to_string(), value.as_str().to_string());
260 }
261 _ => return Err(ParseLineError::L3Token),
262 }
263 }
264
265 Ok(token_map)
266}
267
268fn get_line_type(line: &str) -> LineType {
270 if line.starts_with("L3:") {
271 return LineType::L3Line;
272 }
273 if line.starts_with("L3CODE:") {
274 return LineType::L3CodeLine;
275 }
276 if line.starts_with("L3DATA:") {
277 return LineType::L3DataLine;
278 }
279 if line.starts_with("MB:") {
280 return LineType::MbLine;
281 }
282
283 LineType::Unknown
285}
286
287fn parse_line(line: &str) -> Option<std::result::Result<ParsedLine, ParseLineError>> {
289 let line_type = get_line_type(line);
290
291 let maybe_tokens = match line_type {
292 LineType::L3Line => parse_l3_line(line).map(Some),
293 LineType::L3DataLine => parse_l3_line(line).map(Some),
294 LineType::L3CodeLine => parse_l3_line(line).map(Some),
295 LineType::MbLine => parse_mb_line(line).map(Some),
296 LineType::Unknown => Ok(None),
297 };
298
299 match maybe_tokens {
300 Err(err) => Some(Err(err)),
301 Ok(None) => None,
302 Ok(Some(tokens)) => Some(Ok(ParsedLine { line_type, tokens })),
303 }
304}
305
306fn compare_lines(first_lines: &[ParsedLine], second_lines: &[ParsedLine]) -> bool {
309 first_lines.iter().all(|line| second_lines.contains(line))
310 && second_lines.iter().all(|line| first_lines.contains(line))
311}
312
313fn is_same_schema(combined_schema: &str, existing_schema: &str) -> Result<bool> {
316 let combined = combined_schema
319 .lines()
320 .filter_map(parse_line)
321 .collect::<std::result::Result<Vec<ParsedLine>, _>>()?;
322 let existing = existing_schema
323 .lines()
324 .filter_map(parse_line)
325 .collect::<std::result::Result<Vec<ParsedLine>, _>>()?;
326
327 Ok(compare_lines(&combined, &existing))
329}
330
331fn write_resctrl_schemata(
337 path: &Path,
338 id: &str,
339 l3_cache_schema: &Option<String>,
340 mem_bw_schema: &Option<String>,
341 clos_id_was_set: bool,
342 created_dir: bool,
343) -> Result<()> {
344 let schemata = path.to_owned().join(id).join("schemata");
345 let maybe_combined_schema = combine_l3_cache_and_mem_bw_schemas(l3_cache_schema, mem_bw_schema);
346
347 if let Some(combined_schema) = maybe_combined_schema {
348 if clos_id_was_set && !created_dir {
349 let data = fs::read_to_string(&schemata).map_err(IntelRdtError::ReadSchemata)?;
351 if !is_same_schema(&combined_schema, &data)? {
352 Err(IntelRdtError::ExistingSchemataMismatch)?;
353 }
354 } else {
355 let mut file = OpenOptions::new()
360 .create(true)
361 .truncate(true)
362 .write(true)
363 .open(schemata)
364 .map_err(IntelRdtError::OpenSchemata)?;
365 let schema_with_newline = combined_schema + "\n";
367 write!(file, "{schema_with_newline}").map_err(IntelRdtError::WriteSchemata)?;
368 }
369 }
370
371 Ok(())
372}
373
374pub fn setup_intel_rdt(
378 maybe_container_id: Option<&str>,
379 init_pid: &Pid,
380 intel_rdt: &LinuxIntelRdt,
381) -> Result<bool> {
382 let path = find_resctrl_mount_point().inspect_err(|_err| {
384 tracing::error!("failed to find a mounted resctrl file system");
385 })?;
386 let clos_id_set = intel_rdt.clos_id().is_some();
387 let only_clos_id_set =
388 clos_id_set && intel_rdt.l3_cache_schema().is_none() && intel_rdt.mem_bw_schema().is_none();
389 let id = match (intel_rdt.clos_id(), maybe_container_id) {
390 (Some(clos_id), _) => clos_id,
391 (None, Some(container_id)) => container_id,
392 (None, None) => Err(IntelRdtError::ResctrlIdNotFound)?,
393 };
394
395 let created_dir = write_container_pid_to_resctrl_tasks(&path, id, *init_pid, only_clos_id_set)
396 .inspect_err(|_err| {
397 tracing::error!("failed to write container pid to resctrl tasks file");
398 })?;
399 write_resctrl_schemata(
400 &path,
401 id,
402 intel_rdt.l3_cache_schema(),
403 intel_rdt.mem_bw_schema(),
404 clos_id_set,
405 created_dir,
406 )
407 .inspect_err(|_err| {
408 tracing::error!("failed to write schemata to resctrl schemata file");
409 })?;
410
411 let need_to_delete_directory = !clos_id_set && created_dir;
414
415 Ok(need_to_delete_directory)
416}
417
418#[cfg(test)]
419mod test {
420 use anyhow::Result;
421
422 use super::*;
423
424 #[test]
425 fn test_combine_schemas() -> Result<()> {
426 let res = combine_l3_cache_and_mem_bw_schemas(&None, &None);
427 assert!(res.is_none());
428
429 let l3_1 = "L3:0=f;1=f0";
430 let bw_1 = "MB:0=70;1=20";
431
432 let res = combine_l3_cache_and_mem_bw_schemas(&Some(l3_1.to_owned()), &None);
433 assert!(res.is_some());
434 assert!(res.unwrap() == "L3:0=f;1=f0");
435
436 let res = combine_l3_cache_and_mem_bw_schemas(&None, &Some(bw_1.to_owned()));
437 assert!(res.is_some());
438 assert!(res.unwrap() == "MB:0=70;1=20");
439
440 let res =
441 combine_l3_cache_and_mem_bw_schemas(&Some(l3_1.to_owned()), &Some(bw_1.to_owned()));
442 assert!(res.is_some());
443 let val = res.unwrap();
444 assert!(val.lines().any(|line| line == "MB:0=70;1=20"));
445 assert!(val.lines().any(|line| line == "L3:0=f;1=f0"));
446
447 let l3_2 = "L3:0=f;1=f0\nL3:2=f\n;MB:0=20;1=70";
448 let res =
449 combine_l3_cache_and_mem_bw_schemas(&Some(l3_2.to_owned()), &Some(bw_1.to_owned()));
450 assert!(res.is_some());
451 let val = res.unwrap();
452 assert!(val.lines().any(|line| line == "MB:0=70;1=20"));
453 assert!(val.lines().any(|line| line == "L3:0=f;1=f0"));
454 assert!(val.lines().any(|line| line == "L3:2=f"));
455 assert!(!val.lines().any(|line| line == "MB:0=20;1=70"));
456
457 Ok(())
458 }
459
460 #[test]
461 fn test_is_same_schema() -> Result<()> {
462 assert!(is_same_schema("L3:0=f;1=f0", "L3:0=f;1=f0")?);
464 assert!(is_same_schema("L3DATA:0=f;1=f0", "L3DATA:0=f;1=f0")?);
465 assert!(is_same_schema("L3CODE:0=f;1=f0", "L3CODE:0=f;1=f0")?);
466 assert!(is_same_schema("MB:0=bar;1=f0", "MB:0=bar;1=f0")?);
467 assert!(is_same_schema("L3:", "L3:")?);
468 assert!(is_same_schema("MB:", "MB:")?);
469
470 assert!(!is_same_schema("L3:0=f;1=f0", "L3:2=f")?);
472 assert!(!is_same_schema("MB:0=bar;1=f0", "MB:0=foo;1=f0")?);
473 assert!(!is_same_schema("L3DATA:0=f;1=f0", "L3CODE:2=f")?);
474 assert!(!is_same_schema("L3DATA:0=f;1=f0", "L3CODE:2=f")?);
475 assert!(!is_same_schema("L3DATA:0=f", "L3CODE:0=f")?);
476 assert!(!is_same_schema("L3:0=f", "L3DATA:0=f")?);
477 assert!(!is_same_schema("L3CODE:0=f", "L3:0=f")?);
478 assert!(!is_same_schema("MB:0=f", "L3:0=f")?);
479
480 assert!(is_same_schema(
482 "L3:0=f;1=f0\nL3:2=f",
483 "L3:0=f;1=f0\nL3:2=f"
484 )?);
485
486 assert!(is_same_schema(
488 "L3:0=f;1=f0\nL3:2=f\nBAR:foo",
489 "L3:0=f;1=f0\nL3:2=f"
490 )?);
491
492 assert!(!is_same_schema(
494 "L3:0=f;1=f0\nL3:2=f\nL3:3=f",
495 "L3:0=f;1=f0\nL3:2=f"
496 )?);
497
498 assert!(!is_same_schema(
500 "L3:0=f;1=f0\nL3:2=f\nL3:3=f",
501 "L3:0=f;1=f0\nL3:2=f"
502 )?);
503 assert!(!is_same_schema(
504 "L3:0=f;1=f0\nL3:2=f",
505 "L3:0=f;1=f0\nL3:2=f\nL3:3=f"
506 )?);
507
508 assert!(is_same_schema("L3:1=f0;0=0", "L3:0=0;1=f0")?);
510
511 assert!(is_same_schema("L3:;; 0 = f; ; 1=f0", "L3:0=f;1 = f0;;")?);
513
514 assert!(is_same_schema("L3:0=000f", "L3:0=0f")?);
516 assert!(is_same_schema("L3:0=000f", "L3:0=0f")?);
517 assert!(is_same_schema("L3:0=f", "L3:0=0f")?);
518 assert!(is_same_schema("L3:0=0", "L3:0=0000")?);
519
520 assert!(is_same_schema("L3:1=;0=f", "L3:1=;0=f").is_err());
522 assert!(is_same_schema("L3:=0;0=f", "L3:=0;0=f").is_err());
523 assert!(is_same_schema("L3:1=0=3;0=f", "L3:1=0=3;0=f").is_err());
524 assert!(is_same_schema("L3:1=bar", "L3:1=bar").is_err());
525 assert!(is_same_schema("MB:1=;0=f", "MB:1=;0=f").is_err());
526 assert!(is_same_schema("MB:=0;0=f", "MB:=0;0=f").is_err());
527 assert!(is_same_schema("MB:1=0=3;0=f", "MB:1=0=3;0=f").is_err());
528
529 Ok(())
530 }
531
532 #[test]
533 fn test_write_pid_to_resctrl_tasks() -> Result<()> {
534 let tmp = tempfile::tempdir().unwrap();
535
536 let res =
538 write_container_pid_to_resctrl_tasks(tmp.path(), "foo", Pid::from_raw(1000), false);
539 assert!(res.unwrap()); let res = fs::read_to_string(tmp.path().join("foo").join("tasks"));
541 assert!(res.unwrap() == "1000");
542
543 let res =
545 write_container_pid_to_resctrl_tasks(tmp.path(), "foo", Pid::from_raw(1500), false);
546 assert!(!res.unwrap()); let res =
550 write_container_pid_to_resctrl_tasks(tmp.path(), "foobar", Pid::from_raw(2000), true);
551 assert!(res.is_err());
552
553 let res =
555 write_container_pid_to_resctrl_tasks(tmp.path(), "foo", Pid::from_raw(2500), true);
556 assert!(!res.unwrap()); Ok(())
559 }
560
561 #[test]
562 fn test_write_resctrl_schemata() -> Result<()> {
563 let tmp = tempfile::tempdir().unwrap();
564
565 let res =
566 write_container_pid_to_resctrl_tasks(tmp.path(), "foobar", Pid::from_raw(1000), false);
567 assert!(res.unwrap()); let res = write_resctrl_schemata(tmp.path(), "foobar", &None, &None, false, true);
571 assert!(res.is_ok());
572 let res = fs::read_to_string(tmp.path().join("foobar").join("schemata"));
573 assert!(res.is_err()); let l3_1 = "L3:0=f;1=f0\nL3:2=f\nMB:0=20;1=70";
576 let bw_1 = "MB:0=70;1=20";
577 let res = write_resctrl_schemata(
578 tmp.path(),
579 "foobar",
580 &Some(l3_1.to_owned()),
581 &Some(bw_1.to_owned()),
582 false,
583 true,
584 );
585 assert!(res.is_ok());
586
587 let res = fs::read_to_string(tmp.path().join("foobar").join("schemata"));
588 assert!(res.is_ok());
589 assert!(is_same_schema(
590 "L3:0=f;1=f0\nL3:2=f\nMB:0=70;1=20\n",
591 &res.unwrap()
592 )?);
593
594 let res = write_resctrl_schemata(
598 tmp.path(),
599 "foobar",
600 &Some(l3_1.to_owned()),
601 &Some(bw_1.to_owned()),
602 true,
603 false,
604 );
605 assert!(res.is_ok());
606
607 let l3_2 = "L3:0=f;1=f0\nMB:0=20;1=70";
609 let bw_2 = "MB:0=70;1=20";
610 let res = write_resctrl_schemata(
611 tmp.path(),
612 "foobar",
613 &Some(l3_2.to_owned()),
614 &Some(bw_2.to_owned()),
615 true,
616 false,
617 );
618 assert!(res.is_err());
619
620 Ok(())
621 }
622}