linux_info/
storage.rs

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