1use std::fs::File;
23use std::io::{BufRead, BufReader};
24use std::path::{Path, PathBuf};
25
26#[derive(Debug, PartialEq)]
28pub struct CgroupEntry {
29 subsystems: Vec<String>,
30 root: PathBuf,
31}
32
33impl CgroupEntry {
34 fn from_line(line: String) -> Option<CgroupEntry> {
35 let mut fields = line.split(':');
36 let subsystems = fields
37 .nth(1)?
38 .split(',')
39 .filter(|s| !s.is_empty())
40 .map(|s| s.to_owned())
41 .collect();
42 let root = PathBuf::from(fields.next()?);
43 Some(CgroupEntry { subsystems, root })
44 }
45}
46
47pub fn parse_proc_self_cgroup() -> Option<Vec<CgroupEntry>> {
49 let file = File::open("/proc/self/cgroup").ok()?;
50 let file = BufReader::new(file);
51 Some(
52 file.lines()
53 .map_while(Result::ok)
54 .filter_map(CgroupEntry::from_line)
55 .collect(),
56 )
57}
58
59#[derive(Debug, PartialEq)]
61pub struct Mountinfo {
62 root: PathBuf,
63 mount_point: PathBuf,
64 fs_type: String,
65 super_opts: Vec<String>,
66}
67
68impl Mountinfo {
69 fn from_line(line: String) -> Option<Mountinfo> {
70 let mut split = line.split(" - ");
72
73 let mut mount_fields = split.next()?.split(' ');
74 let root = PathBuf::from(mount_fields.nth(3)?);
75 let mount_point = PathBuf::from(mount_fields.next()?);
76
77 let mut fs_fields = split.next()?.split(' ');
78
79 let fs_type = fs_fields.next()?.split('.').next()?.to_owned();
80 let super_opts: Vec<String> = fs_fields
81 .nth(1)?
82 .split(',')
83 .filter(|s| !s.is_empty())
84 .map(|s| s.to_owned())
85 .collect();
86
87 Some(Mountinfo {
88 root,
89 mount_point,
90 fs_type,
91 super_opts,
92 })
93 }
94}
95
96pub fn parse_proc_self_mountinfo() -> Option<(Vec<Mountinfo>, Vec<Mountinfo>)> {
99 let file = File::open("/proc/self/mountinfo").ok()?;
100 let file = BufReader::new(file);
101 Some(
102 file.lines()
103 .map_while(Result::ok)
104 .filter_map(Mountinfo::from_line)
105 .filter(|mi| mi.fs_type == "cgroup" || mi.fs_type == "cgroup2")
106 .partition(|mi| mi.fs_type == "cgroup2"),
107 )
108}
109
110#[derive(Debug)]
114pub struct MemoryLimit {
115 pub max: Option<usize>,
117 pub swap_max: Option<usize>,
119}
120
121fn read_file_to_usize<P: AsRef<Path>>(path: P) -> Option<usize> {
122 match std::fs::read_to_string(path.as_ref()) {
123 Ok(s) => s.trim().parse::<usize>().ok(),
124 Err(_) => None,
125 }
126}
127
128fn read_v2_memory_limit(cgroups: &[CgroupEntry], mounts: &[Mountinfo]) -> Option<MemoryLimit> {
131 let mount = mounts.first()?;
133 if mount.root != cgroups.first()?.root {
134 return None;
136 }
137 let mount_point = &mount.mount_point;
138 let controllers = std::fs::read_to_string(mount_point.join("cgroup.controllers")).ok()?;
139 let mut controllers = controllers.trim().split(' ');
140 if controllers.any(|c| c == "memory") {
141 let max = read_file_to_usize(mount_point.join("memory.max"));
142 let swap_max = read_file_to_usize(mount_point.join("memory.swap.max"));
144 return Some(MemoryLimit { max, swap_max });
145 }
146 None
147}
148
149fn read_v1_memory_limit(cgroups: &[CgroupEntry], mounts: &[Mountinfo]) -> Option<MemoryLimit> {
152 let memory_cgroup = cgroups
154 .into_iter()
155 .find(|cgroup| cgroup.subsystems.iter().any(|s| s == "memory"))?;
156 let memory_mount = mounts
157 .iter()
158 .find(|mi| mi.root == memory_cgroup.root && mi.super_opts.iter().any(|o| o == "memory"))?;
159 let mount_point = &memory_mount.mount_point;
160 let max = read_file_to_usize(mount_point.join("memory.limit_in_bytes"));
161 let memsw_max = read_file_to_usize(mount_point.join("memory.memsw.limit_in_bytes"));
163 let swap_max = match (max, memsw_max) {
164 (Some(max), Some(memsw_max)) => Some(memsw_max - max),
165 _ => None,
166 };
167 Some(MemoryLimit { max, swap_max })
168}
169pub fn detect_memory_limit() -> Option<MemoryLimit> {
171 let (v2_mounts, v1_mounts) = parse_proc_self_mountinfo()?;
172 let cgroups = parse_proc_self_cgroup()?;
173
174 if !v2_mounts.is_empty() {
175 return read_v2_memory_limit(&cgroups, &v2_mounts);
176 }
177 read_v1_memory_limit(&cgroups, &v1_mounts)
178}
179
180#[derive(Clone, Debug)]
185pub struct CgroupV2 {
186 dir: PathBuf,
187}
188
189impl CgroupV2 {
190 pub fn detect() -> Option<Self> {
195 let (v2_mounts, _v1_mounts) = parse_proc_self_mountinfo()?;
196 let cgroups = parse_proc_self_cgroup()?;
197
198 let mount = v2_mounts.first()?;
200 if mount.root != cgroups.first()?.root {
201 return None;
203 }
204
205 let dir = &mount.mount_point;
206 let controllers = std::fs::read_to_string(dir.join("cgroup.controllers")).ok()?;
207 if !controllers.trim().split(' ').any(|c| c == "memory") {
208 return None;
209 }
210
211 Some(Self { dir: dir.clone() })
212 }
213
214 pub fn path(&self) -> &Path {
220 &self.dir
221 }
222
223 pub fn read_u64(&self, file: &str) -> Option<u64> {
229 let contents = std::fs::read_to_string(self.dir.join(file)).ok()?;
230 contents.trim().parse().ok()
231 }
232
233 pub fn read_keyed_u64(&self, file: &str, key: &str) -> Option<u64> {
235 let contents = std::fs::read_to_string(self.dir.join(file)).ok()?;
236 contents.lines().find_map(|line| {
237 let (name, value) = line.split_once(' ')?;
238 (name == key).then(|| value.trim().parse().ok())?
239 })
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use std::path::PathBuf;
246
247 use super::{CgroupEntry, Mountinfo};
248
249 #[crate::test]
250 fn test_cgroup_from_line() {
251 assert_eq!(
253 CgroupEntry::from_line("0::/".to_owned()),
254 Some(CgroupEntry {
255 subsystems: vec![],
256 root: PathBuf::from("/"),
257 })
258 );
259
260 assert_eq!(
262 CgroupEntry::from_line("6:cpu,cpuacct:/kubepods/pod5b977639-f878-469b-94ee-47a4aa7e597a/dd55abbabd99bcb4d2ce17ffa77d6f811c90e09202f537c273962a8259cac8a0".to_owned()),
263 Some(CgroupEntry {
264 subsystems: vec!["cpu".to_owned(), "cpuacct".to_owned()],
265 root: PathBuf::from("/kubepods/pod5b977639-f878-469b-94ee-47a4aa7e597a/dd55abbabd99bcb4d2ce17ffa77d6f811c90e09202f537c273962a8259cac8a0"),
266 })
267 );
268 assert_eq!(
269 CgroupEntry::from_line("5:memory:/kubepods/pod5b977639-f878-469b-94ee-47a4aa7e597a/dd55abbabd99bcb4d2ce17ffa77d6f811c90e09202f537c273962a8259cac8a0".to_owned()),
270 Some(CgroupEntry {
271 subsystems: vec!["memory".to_owned()],
272 root: PathBuf::from("/kubepods/pod5b977639-f878-469b-94ee-47a4aa7e597a/dd55abbabd99bcb4d2ce17ffa77d6f811c90e09202f537c273962a8259cac8a0"),
273 })
274 );
275 }
276
277 #[crate::test]
278 fn test_mountinfo_from_line() {
279 assert_eq!(Mountinfo::from_line("863 758 0:63 / / rw,relatime master:305 - overlay overlay rw,seclabel,lowerdir=/var/lib/docker/overlay2/l/SUKWDHL7W7YZCJ6YI66I7Z5PR2:/var/lib/docker/overlay2/l/ORL2I23UNUGM7FYF4BSL5JUCAB:/var/lib/docker/overlay2/l/LLKK3J2EHGPF5IGGDSAQGRFHLV:/var/lib/docker/overlay2/l/JEQIUQIQTVNRBAGCU7SLV4KK4K:/var/lib/docker/overlay2/l/5DS7KSJCA7BHWAYWII7BI5DBC5:/var/lib/docker/overlay2/l/ZAGXZ62GNFPZFLNUDZ3JOZIMYR:/var/lib/docker/overlay2/l/6WVXMD372IA24ZXRWGGTIPEQPA,upperdir=/var/lib/docker/overlay2/5c7734eb769484f3469b234181365466eb30bcd7f31c912f4250c8d701637ee4/diff,workdir=/var/lib/docker/overlay2/5c7734eb769484f3469b234181365466eb30bcd7f31c912f4250c8d701637ee4/work".to_owned()),
281 Some(Mountinfo{
282 root: PathBuf::from("/"),
283 mount_point: PathBuf::from("/"),
284 fs_type: "overlay".to_owned(),
285 super_opts: vec![
286 "rw".to_owned(),
287 "seclabel".to_owned(),
288 "lowerdir=/var/lib/docker/overlay2/l/SUKWDHL7W7YZCJ6YI66I7Z5PR2:/var/lib/docker/overlay2/l/ORL2I23UNUGM7FYF4BSL5JUCAB:/var/lib/docker/overlay2/l/LLKK3J2EHGPF5IGGDSAQGRFHLV:/var/lib/docker/overlay2/l/JEQIUQIQTVNRBAGCU7SLV4KK4K:/var/lib/docker/overlay2/l/5DS7KSJCA7BHWAYWII7BI5DBC5:/var/lib/docker/overlay2/l/ZAGXZ62GNFPZFLNUDZ3JOZIMYR:/var/lib/docker/overlay2/l/6WVXMD372IA24ZXRWGGTIPEQPA".to_owned(),
289 "upperdir=/var/lib/docker/overlay2/5c7734eb769484f3469b234181365466eb30bcd7f31c912f4250c8d701637ee4/diff".to_owned(),
290 "workdir=/var/lib/docker/overlay2/5c7734eb769484f3469b234181365466eb30bcd7f31c912f4250c8d701637ee4/work".to_owned(),
291 ],
292 })
293 );
294
295 assert_eq!(Mountinfo::from_line("868 867 0:27 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,seclabel,nsdelegate,memory_recursiveprot".to_owned()), Some(Mountinfo{
297
298 root: PathBuf::from("/"),
299 mount_point: PathBuf::from("/sys/fs/cgroup"),
300 fs_type: "cgroup2".to_owned(),
301 super_opts: vec![
302 "rw".to_owned(),
303 "seclabel".to_owned(),
304 "nsdelegate".to_owned(),
305 "memory_recursiveprot".to_owned(),
306 ],
307 }));
308
309 assert_eq!(Mountinfo::from_line("702 697 0:30 /kubepods/pod5b977639-f878-469b-94ee-47a4aa7e597a/dd55abbabd99bcb4d2ce17ffa77d6f811c90e09202f537c273962a8259cac8a0 /sys/fs/cgroup/memory ro,nosuid,nodev,noexec,relatime master:13 - cgroup cgroup rw,memory".to_owned()), Some(Mountinfo{
311
312 root: PathBuf::from("/kubepods/pod5b977639-f878-469b-94ee-47a4aa7e597a/dd55abbabd99bcb4d2ce17ffa77d6f811c90e09202f537c273962a8259cac8a0"),
313 mount_point: PathBuf::from("/sys/fs/cgroup/memory"),
314 fs_type: "cgroup".to_owned(),
315 super_opts: vec![
316 "rw".to_owned(),
317 "memory".to_owned(),
318 ],
319 }));
320 }
321}