Skip to main content

libcontainer/process/
intel_rdt.rs

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        // Make sure the container_id really exists and the directory
81        // is inside the resctrl fs.
82        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
97/// Finds the resctrl mount path by looking at the process mountinfo data.
98pub 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
121/// Adds container PID to the tasks file in the correct resctrl
122/// pseudo-filesystem subdirectory.  Creates the directory if needed based on
123/// the rules in Linux OCI runtime config spec.
124fn 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                    // Directory doesn't exist and only clos_id is set: error out.
139                    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            // TODO(ipuustin): File doesn't need to be created, but it's easier
148            // to test this way. Fix the tests so that the fake resctrl
149            // filesystem is pre-populated.
150            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
167/// Merges the two schemas together, removing lines starting with "MB:" from
168/// l3_cache_schema if mem_bw_schema is also specified.
169fn 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            // Combine the results. Filter out "MB:"-lines from l3_cache_schema
176            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            // Apprarently the "MB:"-lines don't need to be removed in this case?
189            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
211/// Parse tokens ("1=7000") from a "MB:" line.
212fn 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
238/// Parse tokens ("0=ffff") from a L3{,CODE,DATA} line.
239fn 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    //                                        ^
248    //                          +-------------+
249    //                          |
250    // The capture regexp also removes leading zeros from mask values.
251
252    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
268/// Get the resctrl line type. We only support L3{,CODE,DATA} and MB.
269fn 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    // Empty or unknown line.
284    LineType::Unknown
285}
286
287/// Parse a resctrl line.
288fn 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
306/// Compare two sets of parsed lines. Do this both ways because of possible
307/// duplicate lines, meaning that the vector lengths may be different.
308fn 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
313/// Compares that two strings have the same set of lines (even if the lines are
314/// in different order).
315fn is_same_schema(combined_schema: &str, existing_schema: &str) -> Result<bool> {
316    // Parse the strings first to lines and then to structs. Also filter
317    // out lines that are non-L3{DATA,CODE} and non-MB.
318    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    // Compare the two sets of parsed lines.
328    Ok(compare_lines(&combined, &existing))
329}
330
331/// Combines the l3_cache_schema and mem_bw_schema values together with the
332/// rules given in Linux OCI runtime config spec. If clos_id_was_set parameter
333/// is true and the directory wasn't created, the rules say that the schemas
334/// need to be compared with the existing value and an error must be generated
335/// if they don't match.
336fn 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            // Compare existing schema and error out if no match.
350            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            // Write the combined schema to the schemata file.
356            // TODO(ipuustin): File doesn't need to be created, but it's easier
357            // to test this way. Fix the tests so that the fake resctrl
358            // filesystem is pre-populated.
359            let mut file = OpenOptions::new()
360                .create(true)
361                .truncate(true)
362                .write(true)
363                .open(schemata)
364                .map_err(IntelRdtError::OpenSchemata)?;
365            // Prevent write!() from writing the newline with a separate call.
366            let schema_with_newline = combined_schema + "\n";
367            write!(file, "{schema_with_newline}").map_err(IntelRdtError::WriteSchemata)?;
368        }
369    }
370
371    Ok(())
372}
373
374/// Sets up Intel RDT configuration for the container process based on the
375/// OCI config. The result bool tells whether or not we need to clean up
376/// the created subdirectory.
377pub fn setup_intel_rdt(
378    maybe_container_id: Option<&str>,
379    init_pid: &Pid,
380    intel_rdt: &LinuxIntelRdt,
381) -> Result<bool> {
382    // Find mounted resctrl filesystem, error out if it can't be found.
383    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    // If closID is not set and the runtime has created the sub-directory,
412    // the runtime MUST remove the sub-directory when the container is deleted.
413    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        // Exact same schemas.
463        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        // Different schemas.
471        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        // Exact same multi-line schema.
481        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        // Unknown line type is ignored.
487        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        // Different multi-line schema.
493        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        // Different lines (two ways).
499        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        // Same schema, different token order.
509        assert!(is_same_schema("L3:1=f0;0=0", "L3:0=0;1=f0")?);
510
511        // Same schema, different whitespace and semicolons.
512        assert!(is_same_schema("L3:;;  0 = f; ;  1=f0", "L3:0=f;1  = f0;;")?);
513
514        // Same schema, different leading zeros in masks.
515        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        // Invalid schemas.
521        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        // Create the directory for id "foo".
537        let res =
538            write_container_pid_to_resctrl_tasks(tmp.path(), "foo", Pid::from_raw(1000), false);
539        assert!(res.unwrap()); // new directory created
540        let res = fs::read_to_string(tmp.path().join("foo").join("tasks"));
541        assert!(res.unwrap() == "1000");
542
543        // Create the same directory the second time.
544        let res =
545            write_container_pid_to_resctrl_tasks(tmp.path(), "foo", Pid::from_raw(1500), false);
546        assert!(!res.unwrap()); // no new directory created
547
548        // If just clos_id then throw an error.
549        let res =
550            write_container_pid_to_resctrl_tasks(tmp.path(), "foobar", Pid::from_raw(2000), true);
551        assert!(res.is_err());
552
553        // If the directory already exists then it's fine to have just clos_id.
554        let res =
555            write_container_pid_to_resctrl_tasks(tmp.path(), "foo", Pid::from_raw(2500), true);
556        assert!(!res.unwrap()); // no new directory created
557
558        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()); // new directory created
568
569        // No schemes, clos_id was not set, directory created (with container id).
570        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()); // File not found because no schemes.
574
575        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        // Try the verification case. If the directory existed (was not created
595        // by us) and the clos_id was set, it needs to contain the same data as
596        // we are trying to set. This is the same data:
597        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        // And this different data:
608        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}