Skip to main content

linux_info/
storage.rs

1//! get information about drives and raids.
2
3use crate::unit::DataSize;
4use crate::util::{blkdev_sector_size, read_to_string_mut};
5
6use std::convert::TryInto;
7use std::path::Path;
8use std::{fs, io};
9
10use byte_parser::{parse_iter, ParseIterator, StrParser};
11
12/// Read partitions from /proc/partitions.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct Partitions {
15	raw: String,
16}
17
18impl Partitions {
19	fn path() -> &'static Path {
20		Path::new("/proc/partitions")
21	}
22
23	#[cfg(test)]
24	fn from_string(raw: String) -> Self {
25		Self { raw }
26	}
27
28	/// Read partitions from /proc/partitions.
29	pub fn read() -> io::Result<Self> {
30		Ok(Self {
31			raw: fs::read_to_string(Self::path())?,
32		})
33	}
34
35	/// Reloads information without allocating.
36	pub fn reload(&mut self) -> io::Result<()> {
37		read_to_string_mut(Self::path(), &mut self.raw)
38	}
39
40	pub fn entries<'a>(&'a self) -> impl Iterator<Item = PartitionEntry<'a>> {
41		self.raw
42			.trim()
43			.split('\n')
44			.skip(2) // skip headers
45			.map(PartitionEntry::from_str)
46	}
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct PartitionEntry<'a> {
51	raw: &'a str,
52}
53
54impl<'a> PartitionEntry<'a> {
55	fn from_str(raw: &'a str) -> Self {
56		Self { raw }
57	}
58
59	/// returns every key and valu ein the cpu info
60	pub fn values(&self) -> impl Iterator<Item = &'a str> {
61		self.raw.split(' ').map(str::trim).filter(|s| !s.is_empty())
62	}
63
64	/// Returns the major value.
65	pub fn major(&self) -> Option<usize> {
66		self.values().nth(0)?.parse().ok()
67	}
68
69	/// Returns the minor value.
70	pub fn minor(&self) -> Option<usize> {
71		self.values().nth(1)?.parse().ok()
72	}
73
74	/// Returns the blocks value.
75	pub fn blocks(&self) -> Option<usize> {
76		self.values().nth(2)?.parse().ok()
77	}
78
79	/// Returns the name value.
80	pub fn name(&self) -> Option<&'a str> {
81		self.values().nth(3)
82	}
83}
84
85/// Read mount points from /proc/self/mountinfo.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct MountPoints {
88	raw: String,
89}
90
91impl MountPoints {
92	fn path() -> &'static Path {
93		Path::new("/proc/self/mountinfo")
94	}
95
96	#[cfg(test)]
97	fn from_string(raw: String) -> Self {
98		Self { raw }
99	}
100
101	/// Read mount points from /proc/self/mountinfo.
102	pub fn read() -> io::Result<Self> {
103		Ok(Self {
104			raw: fs::read_to_string(Self::path())?,
105		})
106	}
107
108	/// Reloads information without allocating.
109	pub fn reload(&mut self) -> io::Result<()> {
110		read_to_string_mut(Self::path(), &mut self.raw)
111	}
112
113	/// Get the mount points.
114	pub fn points<'a>(&'a self) -> impl Iterator<Item = MountPoint<'a>> {
115		self.raw.trim().split('\n').map(MountPoint::from_str)
116	}
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct MountPoint<'a> {
121	raw: &'a str,
122}
123
124impl<'a> MountPoint<'a> {
125	fn from_str(raw: &'a str) -> Self {
126		Self { raw }
127	}
128
129	/// Returns every value separated by a space.
130	#[inline]
131	pub fn values(&self) -> impl Iterator<Item = &'a str> {
132		self.raw.split(' ')
133	}
134
135	/// A unique ID for the mount (may be reused after umount).
136	pub fn mount_id(&self) -> Option<usize> {
137		self.values().nth(0)?.parse().ok()
138	}
139
140	/// The ID of the parent mount (or of self for
141	/// the root of this mount namespace's mount tree).
142	pub fn parent_id(&self) -> Option<usize> {
143		self.values().nth(1)?.parse().ok()
144	}
145
146	/// major:minor: the value of st_dev for files on this filesystem.
147	#[inline]
148	pub fn major_minor(&self) -> Option<&'a str> {
149		self.values().nth(2)
150	}
151
152	/// Gets the major value.
153	pub fn major(&self) -> Option<usize> {
154		self.major_minor()?.split(':').nth(0)?.parse().ok()
155	}
156
157	/// Gets the minor value.
158	pub fn minor(&self) -> Option<usize> {
159		self.major_minor()?.split(':').nth(1)?.parse().ok()
160	}
161
162	/// the pathname of the directory in the filesystem
163	/// which forms the root of this mount.
164	pub fn root(&self) -> Option<&'a str> {
165		self.values().nth(3)
166	}
167
168	/// The pathname of the mount point relative
169	/// to the process's root directory.
170	pub fn mount_point(&self) -> Option<&'a str> {
171		self.values().nth(4)
172	}
173
174	/// Per-mount options.
175	pub fn mount_options(&self) -> Option<&'a str> {
176		self.values().nth(5)
177	}
178
179	/// Currently, the possible optional fields are `shared`, `master`,
180	/// `propagate_from`, and `unbindable`.
181	pub fn optional_fields(
182		&self,
183	) -> impl Iterator<Item = (&'a str, Option<&'a str>)> {
184		self.values().skip(6).take_while(|&i| i != "-").map(|opt| {
185			let mut iters = opt.split(':');
186			(
187				iters.next().unwrap(),
188				iters.next(), // TODO: update when https://github.com/rust-lang/rust/issues/77998 gets closed
189				              // Some(iters.as_str()).filter(str::is_empty)
190			)
191		})
192	}
193
194	fn after_separator(&self) -> impl Iterator<Item = &'a str> {
195		self.values().skip(5).skip_while(|&i| i != "-").skip(1) // skip separator
196	}
197
198	/// The filesystem type in the form "type[.subtype]".
199	pub fn filesystem_type(&self) -> Option<&'a str> {
200		// maybe parse subtype?
201		self.after_separator().nth(0)
202	}
203
204	// Filesystem-specific information if available.
205	// Returns none if its the same as filesystem_type
206	/// Filesystem-specific information.  
207	/// df command uses this information as Filesystem.
208	pub fn mount_source(&self) -> Option<&'a str> {
209		self.after_separator().nth(1)
210		// let src = self.after_separator().nth(1)?;
211		// match self.filesystem_type() {
212		// 	Some(fst) if fst == src => None,
213		// 	_ => Some(src)
214		// }
215	}
216
217	/// Per-superblock options.
218	pub fn super_options(&self) -> Option<&'a str> {
219		self.after_separator().nth(2)
220	}
221
222	/// Returns the filesystem statistics of this mount point.
223	pub fn stats(&self) -> io::Result<FsStat> {
224		FsStat::read(self.mount_point().unwrap_or(""))
225	}
226}
227
228/// Filesystem statistics
229#[derive(Clone)]
230pub struct FsStat {
231	raw: libc::statfs,
232}
233
234impl FsStat {
235	/// Reads filesystems staticstics for a given
236	/// file descriptor.
237	pub fn read(path: impl AsRef<Path>) -> io::Result<Self> {
238		crate::util::statfs(path).map(|raw| Self { raw })
239	}
240
241	/// Returns `true` if the total blocks is bigger than zero.
242	pub fn has_blocks(&self) -> bool {
243		self.total_blocks().map(|b| b > 0).unwrap_or(false)
244	}
245
246	/// The block size in bytes used for this filesystem.
247	pub fn block_size(&self) -> Option<usize> {
248		self.raw.f_bsize.try_into().ok()
249	}
250
251	/// The total block count.
252	pub fn total_blocks(&self) -> Option<usize> {
253		self.raw.f_blocks.try_into().ok()
254	}
255
256	/// The blocks that are still free may not all
257	/// be accessible to unprivileged users.
258	pub fn free_blocks(&self) -> Option<usize> {
259		self.raw.f_bfree.try_into().ok()
260	}
261
262	/// The blocks that are free and accessible to unprivileged
263	/// users.
264	pub fn available_blocks(&self) -> Option<usize> {
265		self.raw.f_bavail.try_into().ok()
266	}
267
268	/// The blocks that are already used.
269	pub fn used_blocks(&self) -> Option<usize> {
270		Some(self.total_blocks()? - self.free_blocks()?)
271	}
272
273	/// The size of the filesystem.
274	pub fn total(&self) -> Option<DataSize> {
275		DataSize::from_size_bytes(self.total_blocks()? * self.block_size()?)
276	}
277
278	/// The size of the free space.
279	pub fn free(&self) -> Option<DataSize> {
280		DataSize::from_size_bytes(self.free_blocks()? * self.block_size()?)
281	}
282
283	/// The size of the available space to unprivileged
284	/// users.
285	pub fn available(&self) -> Option<DataSize> {
286		DataSize::from_size_bytes(self.available_blocks()? * self.block_size()?)
287	}
288
289	/// The size of the space that is currently
290	/// used.
291	pub fn used(&self) -> Option<DataSize> {
292		DataSize::from_size_bytes(self.used_blocks()? * self.block_size()?)
293	}
294}
295
296/// Read mount points from /proc/mdstat.
297#[derive(Debug, Clone, PartialEq, Eq)]
298pub struct Raids {
299	raw: String,
300}
301
302impl Raids {
303	fn path() -> &'static Path {
304		Path::new("/proc/mdstat")
305	}
306
307	#[cfg(test)]
308	fn from_string(raw: String) -> Self {
309		Self { raw }
310	}
311
312	/// Read raid devices from /proc/mdstat.
313	pub fn read() -> io::Result<Self> {
314		Ok(Self {
315			raw: fs::read_to_string(Self::path())?,
316		})
317	}
318
319	/// Reloads information without allocating.
320	pub fn reload(&mut self) -> io::Result<()> {
321		read_to_string_mut(Self::path(), &mut self.raw)
322	}
323
324	/// Returns all listed devices in /proc/mdstat.
325	pub fn raids(&self) -> impl Iterator<Item = Raid<'_>> {
326		let mut first_line = false;
327		parse_iter(StrParser::new(self.raw.trim()), move |parser| {
328			if !first_line {
329				parser.consume_while_byte_fn(|&b| b != b'\n');
330				// remove newline
331				parser.advance();
332				first_line = true;
333			}
334			parser.peek()?;
335			let key = parser
336				.record()
337				.while_byte_fn(|&b| b != b':')
338				.consume_to_str()
339				.trim();
340
341			if key == "unused devices" {
342				return None;
343			}
344			// remove colon
345			parser.advance();
346
347			let mut parser = parser.record();
348			let mut one = false;
349
350			loop {
351				if one && matches!(parser.peek(), Some(b'\n')) {
352					// finished
353					let s = parser.to_str().trim();
354					parser.advance();
355					return Some(Raid::from_str(key, s));
356				}
357
358				if one {
359					one = false;
360					continue;
361				}
362
363				match parser.next() {
364					Some(b'\n') => one = true,
365					None => {
366						// The end
367						return Some(Raid::from_str(
368							key,
369							parser.to_str().trim(),
370						));
371					}
372					_ => {}
373				}
374			}
375		})
376	}
377}
378
379// https://raid.wiki.kernel.org/index.php/Mdstat
380/// A raid device.
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct Raid<'a> {
383	name: &'a str,
384	raw: &'a str,
385}
386
387impl<'a> Raid<'a> {
388	fn from_str(name: &'a str, raw: &'a str) -> Self {
389		Self { name, raw }
390	}
391
392	/// Returns every line and their values with out the name.
393	#[inline]
394	pub fn values(
395		&self,
396	) -> impl Iterator<Item = impl Iterator<Item = &'a str>> {
397		self.raw.split('\n').map(str::trim).map(|l| l.split(' '))
398	}
399
400	/// The name of the raid for example `md0`.
401	pub fn name(&self) -> &'a str {
402		self.name
403	}
404
405	/// The state of the current device.
406	pub fn state(&self) -> Option<&'a str> {
407		self.values().nth(0)?.nth(0)
408	}
409
410	fn line(&self, line: usize) -> impl Iterator<Item = &'a str> {
411		let mut iter = self.values().nth(line);
412		std::iter::from_fn(move || iter.as_mut()?.next())
413	}
414
415	/// Returns the kind of raid device.  
416	/// Maybe in the future will return an enum.
417	pub fn kind(&self) -> Option<&'a str> {
418		self.line(0).nth(1)
419	}
420
421	/// Returns all devices (id, name) in this raid array.
422	pub fn devices(&self) -> impl Iterator<Item = (usize, &'a str)> {
423		self.line(0).skip(2).filter_map(|dev| {
424			let mut split = dev.split(&['[', ']'][..]);
425			let name = split.next()?;
426			Some((split.next()?.parse().ok()?, name))
427		})
428	}
429
430	/// Returns all usable blocks.
431	pub fn usable_blocks(&self) -> Option<usize> {
432		self.line(1).nth(0)?.parse().ok()
433	}
434
435	/// The amount of devices that are currently used. Should
436	/// be `raid.used_devices()? == raid.devices().count()`.
437	pub fn used_devices(&self) -> Option<usize> {
438		self.line(1)
439			.find(|l| l.starts_with('['))?
440			.split('/')
441			.nth(0)?
442			.strip_prefix('[')?
443			.parse()
444			.ok()
445	}
446
447	/// The amount of devices that would be ideal for this
448	/// array configuration.
449	pub fn ideal_devices(&self) -> Option<usize> {
450		self.line(1)
451			.find(|l| l.starts_with('['))?
452			.split('/')
453			.nth(1)?
454			.strip_suffix(']')?
455			.parse()
456			.ok()
457	}
458
459	/// Returns the progress line if there is any, for example:  
460	/// `[==>..................]  recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec`
461	pub fn progress(&self) -> Option<&'a str> {
462		let l = self.raw.split('\n').nth(2)?.trim();
463		l.starts_with('[').then(|| l)
464	}
465
466	/// Returns filesystem statistics to this raid array.
467	pub fn stats(&self) -> io::Result<FsStat> {
468		FsStat::read(format!("/dev/{}", self.name()))
469	}
470}
471
472/// Returns the sector size for a given path.
473///
474/// This uses the ioctl call `BLKSSZGET`.
475pub fn sector_size(path: impl AsRef<Path>) -> io::Result<u64> {
476	blkdev_sector_size(fs::File::open(path)?)
477}
478
479#[cfg(test)]
480mod tests {
481	use super::*;
482
483	fn partitions() -> Partitions {
484		Partitions::from_string(
485			"\
486major minor  #blocks  name
487
488   7        0     142152 loop0
489   7        1     101528 loop1
490 259        0  500107608 nvme0n1
491 259        1     510976 nvme0n1p1\n\
492		"
493			.into(),
494		)
495	}
496
497	fn cmp_entry(
498		major: usize,
499		minor: usize,
500		blocks: usize,
501		name: &str,
502		e: &PartitionEntry<'_>,
503	) {
504		assert_eq!(e.major().unwrap(), major);
505		assert_eq!(e.minor().unwrap(), minor);
506		assert_eq!(e.blocks().unwrap(), blocks);
507		assert_eq!(e.name().unwrap(), name);
508	}
509
510	#[test]
511	fn all_partitions() {
512		let part = partitions();
513		let mut e = part.entries();
514		println!("e: {:?}", part.entries().collect::<Vec<_>>());
515		cmp_entry(7, 0, 142152, "loop0", &e.next().unwrap());
516		cmp_entry(7, 1, 101528, "loop1", &e.next().unwrap());
517		cmp_entry(259, 0, 500107608, "nvme0n1", &e.next().unwrap());
518		cmp_entry(259, 1, 510976, "nvme0n1p1", &e.next().unwrap());
519		assert!(e.next().is_none());
520	}
521
522	fn mount_points() -> MountPoints {
523		MountPoints::from_string("\
52426 29 0:5 / /dev rw,nosuid,noexec,relatime shared:2 - devtmpfs udev rw,size=8123832k,nr_inodes=2030958,mode=755
52527 26 0:24 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000
52635 33 0:30 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:11 other - cgroup cgroup rw,xattr,name=systemd
5272509 28 0:25 /snapd/ns /run/snapd/ns rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,size=1631264k,mode=755
5282893 2509 0:4 mnt:[4026532961] /run/snapd/ns/snap-store.mnt rw - nsfs nsfs rw\n\
529		".into())
530	}
531
532	fn cmp_point(
533		mount_id: usize,
534		parent_id: usize,
535		major_minor: &str,
536		root: &str,
537		mount_point: &str,
538		mount_options: &str,
539		optional_fields: &[(&str, Option<&str>)],
540		filesystem_type: &str,
541		mount_source: &str,
542		super_options: &str,
543		point: &MountPoint<'_>,
544	) {
545		assert_eq!(point.mount_id().unwrap(), mount_id);
546		assert_eq!(point.parent_id().unwrap(), parent_id);
547		assert_eq!(point.major_minor().unwrap(), major_minor);
548		assert_eq!(point.root().unwrap(), root);
549		assert_eq!(point.mount_point().unwrap(), mount_point);
550		assert_eq!(point.mount_options().unwrap(), mount_options);
551		assert_eq!(
552			point.optional_fields().collect::<Vec<_>>(),
553			optional_fields
554		);
555		assert_eq!(point.filesystem_type().unwrap(), filesystem_type);
556		assert_eq!(point.mount_source().unwrap(), mount_source);
557		assert_eq!(point.super_options().unwrap(), super_options);
558	}
559
560	#[test]
561	fn all_mount_points() {
562		let mt = mount_points();
563		let mut mt = mt.points();
564		cmp_point(
565			26,
566			29,
567			"0:5",
568			"/",
569			"/dev",
570			"rw,nosuid,noexec,relatime",
571			&[("shared", Some("2"))],
572			"devtmpfs",
573			"udev",
574			"rw,size=8123832k,nr_inodes=2030958,mode=755",
575			&mt.next().unwrap(),
576		);
577		cmp_point(
578			27,
579			26,
580			"0:24",
581			"/",
582			"/dev/pts",
583			"rw,nosuid,noexec,relatime",
584			&[("shared", Some("3"))],
585			"devpts",
586			"devpts",
587			"rw,gid=5,mode=620,ptmxmode=000",
588			&mt.next().unwrap(),
589		);
590		cmp_point(
591			35,
592			33,
593			"0:30",
594			"/",
595			"/sys/fs/cgroup/systemd",
596			"rw,nosuid,nodev,noexec,relatime",
597			&[("shared", Some("11")), ("other", None)],
598			"cgroup",
599			"cgroup",
600			"rw,xattr,name=systemd",
601			&mt.next().unwrap(),
602		);
603		cmp_point(
604			2509,
605			28,
606			"0:25",
607			"/snapd/ns",
608			"/run/snapd/ns",
609			"rw,nosuid,nodev,noexec,relatime",
610			&[],
611			"tmpfs",
612			"tmpfs",
613			"rw,size=1631264k,mode=755",
614			&mt.next().unwrap(),
615		);
616		cmp_point(
617			2893,
618			2509,
619			"0:4",
620			"mnt:[4026532961]",
621			"/run/snapd/ns/snap-store.mnt",
622			"rw",
623			&[],
624			"nsfs",
625			"nsfs",
626			"rw",
627			&mt.next().unwrap(),
628		);
629	}
630
631	#[test]
632	fn raid_case_1() {
633		let raids = Raids::from_string("\
634Personalities : [raid1] [linear] [multipath] [raid0] [raid6] [raid5] [raid4] [raid10] 
635md10 : active raid1 sdd[0] sdc[1]
636      3906886464 blocks super 1.2 [2/2] [UU]
637      bitmap: 0/30 pages [0KB], 65536KB chunk
638
639md0 : active raid1 sdb[1] sda[0]
640      499975488 blocks super 1.2 [2/2] [UU]
641      bitmap: 3/4 pages [12KB], 65536KB chunk
642
643unused devices: <none>\n".into());
644		assert_eq!(raids.raids().count(), 2);
645		let first = raids.raids().next().unwrap();
646		assert_eq!(first.name(), "md10");
647		assert_eq!(first.used_devices().unwrap(), 2);
648		assert_eq!(first.ideal_devices().unwrap(), 2);
649		assert!(first.progress().is_none());
650		assert_eq!(first.devices().count(), first.used_devices().unwrap());
651	}
652
653	#[test]
654	fn raid_case_2() {
655		let raids = Raids::from_string("\
656Personalities : [raid1] [raid6] [raid5] [raid4]
657md127 : active raid5 sdh1[6] sdg1[4] sdf1[3] sde1[2] sdd1[1] sdc1[0]
658      1464725760 blocks level 5, 64k chunk, algorithm 2 [6/5] [UUUUU_]
659      [==>..................]  recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec
660
661unused devices: <none>\n".into());
662		assert_eq!(raids.raids().count(), 1);
663		let first = raids.raids().next().unwrap();
664		let comp_dev: Vec<_> = first.devices().collect();
665		assert_eq!(
666			comp_dev,
667			[
668				(6, "sdh1"),
669				(4, "sdg1"),
670				(3, "sdf1"),
671				(2, "sde1"),
672				(1, "sdd1"),
673				(0, "sdc1")
674			]
675		);
676		assert_eq!(first.kind().unwrap(), "raid5");
677		assert_eq!(first.usable_blocks().unwrap(), 1464725760);
678		assert_eq!(first.used_devices().unwrap(), 6);
679		assert_eq!(first.ideal_devices().unwrap(), 5);
680		assert_eq!(first.progress().unwrap(), "[==>..................]  recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec");
681		assert_eq!(first.devices().count(), first.used_devices().unwrap());
682	}
683
684	#[test]
685	fn raid_case_3() {
686		let raids = Raids::from_string(
687			"\
688Personalities : [linear] [raid0] [raid1] [raid5] [raid4] [raid6]
689md0 : active raid6 sdf1[0] sde1[1] sdd1[2] sdc1[3] sdb1[4] sda1[5] hdb1[6]
690      1225557760 blocks level 6, 256k chunk, algorithm 2 [7/7] [UUUUUUU]
691      bitmap: 0/234 pages [0KB], 512KB chunk
692
693unused devices: <none>\n"
694				.into(),
695		);
696		assert_eq!(raids.raids().count(), 1);
697		let first = raids.raids().next().unwrap();
698		assert_eq!(first.devices().count(), first.used_devices().unwrap());
699	}
700}
701
702// get block number
703// /sys/block/<part>/dev   returns 7:0
704// uuid /sys/dev/block/7:0/dm/uuid
705
706/*
707Personalities : [raid1] [linear] [multipath] [raid0] [raid6] [raid5] [raid4] [raid10]
708md10 : active raid1 sdd[0] sdc[1]
709	  3906886464 blocks super 1.2 [2/2] [UU]
710	  bitmap: 0/30 pages [0KB], 65536KB chunk
711
712md0 : active raid1 sdb[1] sda[0]
713	  499975488 blocks super 1.2 [2/2] [UU]
714	  bitmap: 3/4 pages [12KB], 65536KB chunk
715
716unused devices: <none>
717
718
719Personalities : [raid1] [raid6] [raid5] [raid4]
720md127 : active raid5 sdh1[6] sdg1[4] sdf1[3] sde1[2] sdd1[1] sdc1[0]
721	  1464725760 blocks level 5, 64k chunk, algorithm 2 [6/5] [UUUUU_]
722	  [==>..................]  recovery = 12.6% (37043392/292945152) finish=127.5min speed=33440K/sec
723
724unused devices: <none>
725
726
727Personalities : [linear] [raid0] [raid1] [raid5] [raid4] [raid6]
728md0 : active raid6 sdf1[0] sde1[1] sdd1[2] sdc1[3] sdb1[4] sda1[5] hdb1[6]
729	  1225557760 blocks level 6, 256k chunk, algorithm 2 [7/7] [UUUUUUU]
730	  bitmap: 0/234 pages [0KB], 512KB chunk
731
732unused devices: <none>
733*/