]> git.proxmox.com Git - proxmox-backup.git/blob - src/tools/disks/mod.rs
update to first proxmox crate split
[proxmox-backup.git] / src / tools / disks / mod.rs
1 //! Disk query/management utilities for.
2
3 use std::collections::{HashMap, HashSet};
4 use std::ffi::{OsStr, OsString};
5 use std::io;
6 use std::os::unix::ffi::{OsStrExt, OsStringExt};
7 use std::os::unix::fs::MetadataExt;
8 use std::path::{Path, PathBuf};
9 use std::sync::Arc;
10
11 use anyhow::{bail, format_err, Error};
12 use libc::dev_t;
13 use once_cell::sync::OnceCell;
14
15 use ::serde::{Deserialize, Serialize};
16
17 use proxmox::sys::error::io_err_other;
18 use proxmox::sys::linux::procfs::{MountInfo, mountinfo::Device};
19 use proxmox::{io_bail, io_format_err};
20 use proxmox_schema::api;
21
22 use pbs_api_types::{BLOCKDEVICE_NAME_REGEX, StorageStatus};
23
24 mod zfs;
25 pub use zfs::*;
26 mod zpool_status;
27 pub use zpool_status::*;
28 mod zpool_list;
29 pub use zpool_list::*;
30 mod lvm;
31 pub use lvm::*;
32 mod smart;
33 pub use smart::*;
34
35 lazy_static::lazy_static!{
36 static ref ISCSI_PATH_REGEX: regex::Regex =
37 regex::Regex::new(r"host[^/]*/session[^/]*").unwrap();
38 }
39
40 /// Disk management context.
41 ///
42 /// This provides access to disk information with some caching for faster querying of multiple
43 /// devices.
44 pub struct DiskManage {
45 mount_info: OnceCell<MountInfo>,
46 mounted_devices: OnceCell<HashSet<dev_t>>,
47 }
48
49 /// Information for a device as returned by lsblk.
50 #[derive(Deserialize)]
51 pub struct LsblkInfo {
52 /// Path to the device.
53 path: String,
54 /// Partition type GUID.
55 #[serde(rename = "parttype")]
56 partition_type: Option<String>,
57 /// File system label.
58 #[serde(rename = "fstype")]
59 file_system_type: Option<String>,
60 }
61
62 impl DiskManage {
63 /// Create a new disk management context.
64 pub fn new() -> Arc<Self> {
65 Arc::new(Self {
66 mount_info: OnceCell::new(),
67 mounted_devices: OnceCell::new(),
68 })
69 }
70
71 /// Get the current mount info. This simply caches the result of `MountInfo::read` from the
72 /// `proxmox::sys` module.
73 pub fn mount_info(&self) -> Result<&MountInfo, Error> {
74 self.mount_info.get_or_try_init(MountInfo::read)
75 }
76
77 /// Get a `Disk` from a device node (eg. `/dev/sda`).
78 pub fn disk_by_node<P: AsRef<Path>>(self: Arc<Self>, devnode: P) -> io::Result<Disk> {
79 let devnode = devnode.as_ref();
80
81 let meta = std::fs::metadata(devnode)?;
82 if (meta.mode() & libc::S_IFBLK) == libc::S_IFBLK {
83 self.disk_by_dev_num(meta.rdev())
84 } else {
85 io_bail!("not a block device: {:?}", devnode);
86 }
87 }
88
89 /// Get a `Disk` for a specific device number.
90 pub fn disk_by_dev_num(self: Arc<Self>, devnum: dev_t) -> io::Result<Disk> {
91 self.disk_by_sys_path(format!(
92 "/sys/dev/block/{}:{}",
93 unsafe { libc::major(devnum) },
94 unsafe { libc::minor(devnum) },
95 ))
96 }
97
98 /// Get a `Disk` for a path in `/sys`.
99 pub fn disk_by_sys_path<P: AsRef<Path>>(self: Arc<Self>, path: P) -> io::Result<Disk> {
100 let device = udev::Device::from_syspath(path.as_ref())?;
101 Ok(Disk {
102 manager: self,
103 device,
104 info: Default::default(),
105 })
106 }
107
108 /// Get a `Disk` for a name in `/sys/block/<name>`.
109 pub fn disk_by_name(self: Arc<Self>, name: &str) -> io::Result<Disk> {
110 let syspath = format!("/sys/block/{}", name);
111 self.disk_by_sys_path(&syspath)
112 }
113
114 /// Gather information about mounted disks:
115 fn mounted_devices(&self) -> Result<&HashSet<dev_t>, Error> {
116 self.mounted_devices
117 .get_or_try_init(|| -> Result<_, Error> {
118 let mut mounted = HashSet::new();
119
120 for (_id, mp) in self.mount_info()? {
121 let source = match mp.mount_source.as_deref() {
122 Some(s) => s,
123 None => continue,
124 };
125
126 let path = Path::new(source);
127 if !path.is_absolute() {
128 continue;
129 }
130
131 let meta = match std::fs::metadata(path) {
132 Ok(meta) => meta,
133 Err(ref err) if err.kind() == io::ErrorKind::NotFound => continue,
134 Err(other) => return Err(Error::from(other)),
135 };
136
137 if (meta.mode() & libc::S_IFBLK) != libc::S_IFBLK {
138 // not a block device
139 continue;
140 }
141
142 mounted.insert(meta.rdev());
143 }
144
145 Ok(mounted)
146 })
147 }
148
149 /// Information about file system type and used device for a path
150 ///
151 /// Returns tuple (fs_type, device, mount_source)
152 pub fn find_mounted_device(
153 &self,
154 path: &std::path::Path,
155 ) -> Result<Option<(String, Device, Option<OsString>)>, Error> {
156
157 let stat = nix::sys::stat::stat(path)?;
158 let device = Device::from_dev_t(stat.st_dev);
159
160 let root_path = std::path::Path::new("/");
161
162 for (_id, entry) in self.mount_info()? {
163 if entry.root == root_path && entry.device == device {
164 return Ok(Some((entry.fs_type.clone(), entry.device, entry.mount_source.clone())));
165 }
166 }
167
168 Ok(None)
169 }
170
171 /// Check whether a specific device node is mounted.
172 ///
173 /// Note that this tries to `stat` the sources of all mount points without caching the result
174 /// of doing so, so this is always somewhat expensive.
175 pub fn is_devnum_mounted(&self, dev: dev_t) -> Result<bool, Error> {
176 self.mounted_devices().map(|mounted| mounted.contains(&dev))
177 }
178 }
179
180 /// Queries (and caches) various information about a specific disk.
181 ///
182 /// This belongs to a `Disks` and provides information for a single disk.
183 pub struct Disk {
184 manager: Arc<DiskManage>,
185 device: udev::Device,
186 info: DiskInfo,
187 }
188
189 /// Helper struct (so we can initialize this with Default)
190 ///
191 /// We probably want this to be serializable to the same hash type we use in perl currently.
192 #[derive(Default)]
193 struct DiskInfo {
194 size: OnceCell<u64>,
195 vendor: OnceCell<Option<OsString>>,
196 model: OnceCell<Option<OsString>>,
197 rotational: OnceCell<Option<bool>>,
198 // for perl: #[serde(rename = "devpath")]
199 ata_rotation_rate_rpm: OnceCell<Option<u64>>,
200 // for perl: #[serde(rename = "devpath")]
201 device_path: OnceCell<Option<PathBuf>>,
202 wwn: OnceCell<Option<OsString>>,
203 serial: OnceCell<Option<OsString>>,
204 // for perl: #[serde(skip_serializing)]
205 partition_table_type: OnceCell<Option<OsString>>,
206 gpt: OnceCell<bool>,
207 // ???
208 bus: OnceCell<Option<OsString>>,
209 // ???
210 fs_type: OnceCell<Option<OsString>>,
211 // ???
212 has_holders: OnceCell<bool>,
213 // ???
214 is_mounted: OnceCell<bool>,
215 }
216
217 impl Disk {
218 /// Try to get the device number for this disk.
219 ///
220 /// (In udev this can fail...)
221 pub fn devnum(&self) -> Result<dev_t, Error> {
222 // not sure when this can fail...
223 self.device
224 .devnum()
225 .ok_or_else(|| format_err!("failed to get device number"))
226 }
227
228 /// Get the sys-name of this device. (The final component in the `/sys` path).
229 pub fn sysname(&self) -> &OsStr {
230 self.device.sysname()
231 }
232
233 /// Get the this disk's `/sys` path.
234 pub fn syspath(&self) -> &Path {
235 self.device.syspath()
236 }
237
238 /// Get the device node in `/dev`, if any.
239 pub fn device_path(&self) -> Option<&Path> {
240 //self.device.devnode()
241 self.info
242 .device_path
243 .get_or_init(|| self.device.devnode().map(Path::to_owned))
244 .as_ref()
245 .map(PathBuf::as_path)
246 }
247
248 /// Get the parent device.
249 pub fn parent(&self) -> Option<Self> {
250 self.device.parent().map(|parent| Self {
251 manager: self.manager.clone(),
252 device: parent,
253 info: Default::default(),
254 })
255 }
256
257 /// Read from a file in this device's sys path.
258 ///
259 /// Note: path must be a relative path!
260 pub fn read_sys(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
261 assert!(path.is_relative());
262
263 std::fs::read(self.syspath().join(path))
264 .map(Some)
265 .or_else(|err| {
266 if err.kind() == io::ErrorKind::NotFound {
267 Ok(None)
268 } else {
269 Err(err)
270 }
271 })
272 }
273
274 /// Convenience wrapper for reading a `/sys` file which contains just a simple `OsString`.
275 pub fn read_sys_os_str<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<OsString>> {
276 Ok(self.read_sys(path.as_ref())?.map(|mut v| {
277 if Some(&b'\n') == v.last() {
278 v.pop();
279 }
280 OsString::from_vec(v)
281 }))
282 }
283
284 /// Convenience wrapper for reading a `/sys` file which contains just a simple utf-8 string.
285 pub fn read_sys_str<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<String>> {
286 Ok(match self.read_sys(path.as_ref())? {
287 Some(data) => Some(String::from_utf8(data).map_err(io_err_other)?),
288 None => None,
289 })
290 }
291
292 /// Convenience wrapper for unsigned integer `/sys` values up to 64 bit.
293 pub fn read_sys_u64<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<u64>> {
294 Ok(match self.read_sys_str(path)? {
295 Some(data) => Some(data.trim().parse().map_err(io_err_other)?),
296 None => None,
297 })
298 }
299
300 /// Get the disk's size in bytes.
301 pub fn size(&self) -> io::Result<u64> {
302 Ok(*self.info.size.get_or_try_init(|| {
303 self.read_sys_u64("size")?.map(|s| s*512).ok_or_else(|| {
304 io_format_err!(
305 "failed to get disk size from {:?}",
306 self.syspath().join("size"),
307 )
308 })
309 })?)
310 }
311
312 /// Get the device vendor (`/sys/.../device/vendor`) entry if available.
313 pub fn vendor(&self) -> io::Result<Option<&OsStr>> {
314 Ok(self
315 .info
316 .vendor
317 .get_or_try_init(|| self.read_sys_os_str("device/vendor"))?
318 .as_ref()
319 .map(OsString::as_os_str))
320 }
321
322 /// Get the device model (`/sys/.../device/model`) entry if available.
323 pub fn model(&self) -> Option<&OsStr> {
324 self.info
325 .model
326 .get_or_init(|| self.device.property_value("ID_MODEL").map(OsStr::to_owned))
327 .as_ref()
328 .map(OsString::as_os_str)
329 }
330
331 /// Check whether this is a rotational disk.
332 ///
333 /// Returns `None` if there's no `queue/rotational` file, in which case no information is
334 /// known. `Some(false)` if `queue/rotational` is zero, `Some(true)` if it has a non-zero
335 /// value.
336 pub fn rotational(&self) -> io::Result<Option<bool>> {
337 Ok(*self
338 .info
339 .rotational
340 .get_or_try_init(|| -> io::Result<Option<bool>> {
341 Ok(self.read_sys_u64("queue/rotational")?.map(|n| n != 0))
342 })?)
343 }
344
345 /// Get the WWN if available.
346 pub fn wwn(&self) -> Option<&OsStr> {
347 self.info
348 .wwn
349 .get_or_init(|| self.device.property_value("ID_WWN").map(|v| v.to_owned()))
350 .as_ref()
351 .map(OsString::as_os_str)
352 }
353
354 /// Get the device serial if available.
355 pub fn serial(&self) -> Option<&OsStr> {
356 self.info
357 .serial
358 .get_or_init(|| {
359 self.device
360 .property_value("ID_SERIAL_SHORT")
361 .map(|v| v.to_owned())
362 })
363 .as_ref()
364 .map(OsString::as_os_str)
365 }
366
367 /// Get the ATA rotation rate value from udev. This is not necessarily the same as sysfs'
368 /// `rotational` value.
369 pub fn ata_rotation_rate_rpm(&self) -> Option<u64> {
370 *self.info.ata_rotation_rate_rpm.get_or_init(|| {
371 std::str::from_utf8(
372 self.device
373 .property_value("ID_ATA_ROTATION_RATE_RPM")?
374 .as_bytes(),
375 )
376 .ok()?
377 .parse()
378 .ok()
379 })
380 }
381
382 /// Get the partition table type, if any.
383 pub fn partition_table_type(&self) -> Option<&OsStr> {
384 self.info
385 .partition_table_type
386 .get_or_init(|| {
387 self.device
388 .property_value("ID_PART_TABLE_TYPE")
389 .map(|v| v.to_owned())
390 })
391 .as_ref()
392 .map(OsString::as_os_str)
393 }
394
395 /// Check if this contains a GPT partition table.
396 pub fn has_gpt(&self) -> bool {
397 *self.info.gpt.get_or_init(|| {
398 self.partition_table_type()
399 .map(|s| s == "gpt")
400 .unwrap_or(false)
401 })
402 }
403
404 /// Get the bus type used for this disk.
405 pub fn bus(&self) -> Option<&OsStr> {
406 self.info
407 .bus
408 .get_or_init(|| self.device.property_value("ID_BUS").map(|v| v.to_owned()))
409 .as_ref()
410 .map(OsString::as_os_str)
411 }
412
413 /// Attempt to guess the disk type.
414 pub fn guess_disk_type(&self) -> io::Result<DiskType> {
415 Ok(match self.rotational()? {
416 Some(false) => DiskType::Ssd,
417 Some(true) => DiskType::Hdd,
418 None => match self.ata_rotation_rate_rpm() {
419 Some(_) => DiskType::Hdd,
420 None => match self.bus() {
421 Some(bus) if bus == "usb" => DiskType::Usb,
422 _ => DiskType::Unknown,
423 },
424 },
425 })
426 }
427
428 /// Get the file system type found on the disk, if any.
429 ///
430 /// Note that `None` may also just mean "unknown".
431 pub fn fs_type(&self) -> Option<&OsStr> {
432 self.info
433 .fs_type
434 .get_or_init(|| {
435 self.device
436 .property_value("ID_FS_TYPE")
437 .map(|v| v.to_owned())
438 })
439 .as_ref()
440 .map(OsString::as_os_str)
441 }
442
443 /// Check if there are any "holders" in `/sys`. This usually means the device is in use by
444 /// another kernel driver like the device mapper.
445 pub fn has_holders(&self) -> io::Result<bool> {
446 Ok(*self
447 .info
448 .has_holders
449 .get_or_try_init(|| -> io::Result<bool> {
450 let mut subdir = self.syspath().to_owned();
451 subdir.push("holders");
452 for entry in std::fs::read_dir(subdir)? {
453 match entry?.file_name().as_bytes() {
454 b"." | b".." => (),
455 _ => return Ok(true),
456 }
457 }
458 Ok(false)
459 })?)
460 }
461
462 /// Check if this disk is mounted.
463 pub fn is_mounted(&self) -> Result<bool, Error> {
464 Ok(*self
465 .info
466 .is_mounted
467 .get_or_try_init(|| self.manager.is_devnum_mounted(self.devnum()?))?)
468 }
469
470 /// Read block device stats
471 ///
472 /// see https://www.kernel.org/doc/Documentation/block/stat.txt
473 pub fn read_stat(&self) -> std::io::Result<Option<BlockDevStat>> {
474 if let Some(stat) = self.read_sys(Path::new("stat"))? {
475 let stat = unsafe { std::str::from_utf8_unchecked(&stat) };
476 let stat: Vec<u64> = stat.split_ascii_whitespace().map(|s| {
477 u64::from_str_radix(s, 10).unwrap_or(0)
478 }).collect();
479
480 if stat.len() < 15 { return Ok(None); }
481
482 return Ok(Some(BlockDevStat {
483 read_ios: stat[0],
484 read_sectors: stat[2],
485 write_ios: stat[4] + stat[11], // write + discard
486 write_sectors: stat[6] + stat[13], // write + discard
487 io_ticks: stat[10],
488 }));
489 }
490 Ok(None)
491 }
492
493 /// List device partitions
494 pub fn partitions(&self) -> Result<HashMap<u64, Disk>, Error> {
495
496 let sys_path = self.syspath();
497 let device = self.sysname().to_string_lossy().to_string();
498
499 let mut map = HashMap::new();
500
501 for item in pbs_tools::fs::read_subdir(libc::AT_FDCWD, sys_path)? {
502 let item = item?;
503 let name = match item.file_name().to_str() {
504 Ok(name) => name,
505 Err(_) => continue, // skip non utf8 entries
506 };
507
508 if !name.starts_with(&device) { continue; }
509
510 let mut part_path = sys_path.to_owned();
511 part_path.push(name);
512
513 let disk_part = self.manager.clone().disk_by_sys_path(&part_path)?;
514
515 if let Some(partition) = disk_part.read_sys_u64("partition")? {
516 map.insert(partition, disk_part);
517 }
518 }
519
520 Ok(map)
521 }
522 }
523
524 /// Returns disk usage information (total, used, avail)
525 pub fn disk_usage(path: &std::path::Path) -> Result<StorageStatus, Error> {
526
527 let mut stat: libc::statfs64 = unsafe { std::mem::zeroed() };
528
529 use nix::NixPath;
530
531 let res = path.with_nix_path(|cstr| unsafe { libc::statfs64(cstr.as_ptr(), &mut stat) })?;
532 nix::errno::Errno::result(res)?;
533
534 let bsize = stat.f_bsize as u64;
535
536 Ok(StorageStatus{
537 total: stat.f_blocks*bsize,
538 used: (stat.f_blocks-stat.f_bfree)*bsize,
539 avail: stat.f_bavail*bsize,
540 })
541 }
542
543 #[api()]
544 #[derive(Debug, Serialize, Deserialize)]
545 #[serde(rename_all="lowercase")]
546 /// This is just a rough estimate for a "type" of disk.
547 pub enum DiskType {
548 /// We know nothing.
549 Unknown,
550
551 /// May also be a USB-HDD.
552 Hdd,
553
554 /// May also be a USB-SSD.
555 Ssd,
556
557 /// Some kind of USB disk, but we don't know more than that.
558 Usb,
559 }
560
561 #[derive(Debug)]
562 /// Represents the contents of the /sys/block/<dev>/stat file.
563 pub struct BlockDevStat {
564 pub read_ios: u64,
565 pub read_sectors: u64,
566 pub write_ios: u64,
567 pub write_sectors: u64,
568 pub io_ticks: u64, // milliseconds
569 }
570
571 /// Use lsblk to read partition type uuids and file system types.
572 pub fn get_lsblk_info() -> Result<Vec<LsblkInfo>, Error> {
573
574 let mut command = std::process::Command::new("lsblk");
575 command.args(&["--json", "-o", "path,parttype,fstype"]);
576
577 let output = pbs_tools::run_command(command, None)?;
578
579 let mut output: serde_json::Value = output.parse()?;
580
581 Ok(serde_json::from_value(output["blockdevices"].take())?)
582 }
583
584 /// Get set of devices with a file system label.
585 ///
586 /// The set is indexed by using the unix raw device number (dev_t is u64)
587 fn get_file_system_devices(
588 lsblk_info: &[LsblkInfo],
589 ) -> Result<HashSet<u64>, Error> {
590
591 let mut device_set: HashSet<u64> = HashSet::new();
592
593 for info in lsblk_info.iter() {
594 if info.file_system_type.is_some() {
595 let meta = std::fs::metadata(&info.path)?;
596 device_set.insert(meta.rdev());
597 }
598 }
599
600 Ok(device_set)
601 }
602
603 #[api()]
604 #[derive(Debug, Serialize, Deserialize, PartialEq)]
605 #[serde(rename_all="lowercase")]
606 pub enum DiskUsageType {
607 /// Disk is not used (as far we can tell)
608 Unused,
609 /// Disk is mounted
610 Mounted,
611 /// Disk is used by LVM
612 LVM,
613 /// Disk is used by ZFS
614 ZFS,
615 /// Disk is used by device-mapper
616 DeviceMapper,
617 /// Disk has partitions
618 Partitions,
619 /// Disk contains a file system label
620 FileSystem,
621 }
622
623 #[api(
624 properties: {
625 used: {
626 type: DiskUsageType,
627 },
628 "disk-type": {
629 type: DiskType,
630 },
631 status: {
632 type: SmartStatus,
633 }
634 }
635 )]
636 #[derive(Debug, Serialize, Deserialize)]
637 #[serde(rename_all="kebab-case")]
638 /// Information about how a Disk is used
639 pub struct DiskUsageInfo {
640 /// Disk name (/sys/block/<name>)
641 pub name: String,
642 pub used: DiskUsageType,
643 pub disk_type: DiskType,
644 pub status: SmartStatus,
645 /// Disk wearout
646 pub wearout: Option<f64>,
647 /// Vendor
648 pub vendor: Option<String>,
649 /// Model
650 pub model: Option<String>,
651 /// WWN
652 pub wwn: Option<String>,
653 /// Disk size
654 pub size: u64,
655 /// Serisal number
656 pub serial: Option<String>,
657 /// Linux device path (/dev/xxx)
658 pub devpath: Option<String>,
659 /// Set if disk contains a GPT partition table
660 pub gpt: bool,
661 /// RPM
662 pub rpm: Option<u64>,
663 }
664
665 fn scan_partitions(
666 disk_manager: Arc<DiskManage>,
667 lvm_devices: &HashSet<u64>,
668 zfs_devices: &HashSet<u64>,
669 device: &str,
670 ) -> Result<DiskUsageType, Error> {
671
672 let mut sys_path = std::path::PathBuf::from("/sys/block");
673 sys_path.push(device);
674
675 let mut used = DiskUsageType::Unused;
676
677 let mut found_lvm = false;
678 let mut found_zfs = false;
679 let mut found_mountpoints = false;
680 let mut found_dm = false;
681 let mut found_partitions = false;
682
683 for item in pbs_tools::fs::read_subdir(libc::AT_FDCWD, &sys_path)? {
684 let item = item?;
685 let name = match item.file_name().to_str() {
686 Ok(name) => name,
687 Err(_) => continue, // skip non utf8 entries
688 };
689 if !name.starts_with(device) { continue; }
690
691 found_partitions = true;
692
693 let mut part_path = sys_path.clone();
694 part_path.push(name);
695
696 let data = disk_manager.clone().disk_by_sys_path(&part_path)?;
697
698 let devnum = data.devnum()?;
699
700 if lvm_devices.contains(&devnum) {
701 found_lvm = true;
702 }
703
704 if data.is_mounted()? {
705 found_mountpoints = true;
706 }
707
708 if data.has_holders()? {
709 found_dm = true;
710 }
711
712 if zfs_devices.contains(&devnum) {
713 found_zfs = true;
714 }
715 }
716
717 if found_mountpoints {
718 used = DiskUsageType::Mounted;
719 } else if found_lvm {
720 used = DiskUsageType::LVM;
721 } else if found_zfs {
722 used = DiskUsageType::ZFS;
723 } else if found_dm {
724 used = DiskUsageType::DeviceMapper;
725 } else if found_partitions {
726 used = DiskUsageType::Partitions;
727 }
728
729 Ok(used)
730 }
731
732
733 /// Get disk usage information for a single disk
734 pub fn get_disk_usage_info(
735 disk: &str,
736 no_smart: bool,
737 ) -> Result<DiskUsageInfo, Error> {
738 let mut filter = Vec::new();
739 filter.push(disk.to_string());
740 let mut map = get_disks(Some(filter), no_smart)?;
741 if let Some(info) = map.remove(disk) {
742 Ok(info)
743 } else {
744 bail!("failed to get disk usage info - internal error"); // should not happen
745 }
746 }
747
748 /// Get disk usage information for multiple disks
749 pub fn get_disks(
750 // filter - list of device names (without leading /dev)
751 disks: Option<Vec<String>>,
752 // do no include data from smartctl
753 no_smart: bool,
754 ) -> Result<HashMap<String, DiskUsageInfo>, Error> {
755
756 let disk_manager = DiskManage::new();
757
758 let lsblk_info = get_lsblk_info()?;
759
760 let zfs_devices = zfs_devices(&lsblk_info, None).or_else(|err| -> Result<HashSet<u64>, Error> {
761 eprintln!("error getting zfs devices: {}", err);
762 Ok(HashSet::new())
763 })?;
764
765 let lvm_devices = get_lvm_devices(&lsblk_info)?;
766
767 let file_system_devices = get_file_system_devices(&lsblk_info)?;
768
769 // fixme: ceph journals/volumes
770
771 let mut result = HashMap::new();
772
773 for item in pbs_tools::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX)? {
774 let item = item?;
775
776 let name = item.file_name().to_str().unwrap().to_string();
777
778 if let Some(ref disks) = disks {
779 if !disks.contains(&name) { continue; }
780 }
781
782 let sys_path = format!("/sys/block/{}", name);
783
784 if let Ok(target) = std::fs::read_link(&sys_path) {
785 if let Some(target) = target.to_str() {
786 if ISCSI_PATH_REGEX.is_match(target) { continue; } // skip iSCSI devices
787 }
788 }
789
790 let disk = disk_manager.clone().disk_by_sys_path(&sys_path)?;
791
792 let devnum = disk.devnum()?;
793
794 let size = match disk.size() {
795 Ok(size) => size,
796 Err(_) => continue, // skip devices with unreadable size
797 };
798
799 let disk_type = match disk.guess_disk_type() {
800 Ok(disk_type) => disk_type,
801 Err(_) => continue, // skip devices with undetectable type
802 };
803
804 let mut usage = DiskUsageType::Unused;
805
806 if lvm_devices.contains(&devnum) {
807 usage = DiskUsageType::LVM;
808 }
809
810 match disk.is_mounted() {
811 Ok(true) => usage = DiskUsageType::Mounted,
812 Ok(false) => {},
813 Err(_) => continue, // skip devices with undetectable mount status
814 }
815
816 if zfs_devices.contains(&devnum) {
817 usage = DiskUsageType::ZFS;
818 }
819
820 let vendor = disk.vendor().unwrap_or(None).
821 map(|s| s.to_string_lossy().trim().to_string());
822
823 let model = disk.model().map(|s| s.to_string_lossy().into_owned());
824
825 let serial = disk.serial().map(|s| s.to_string_lossy().into_owned());
826
827 let devpath = disk.device_path().map(|p| p.to_owned())
828 .map(|p| p.to_string_lossy().to_string());
829
830
831 let wwn = disk.wwn().map(|s| s.to_string_lossy().into_owned());
832
833 if usage != DiskUsageType::Mounted {
834 match scan_partitions(disk_manager.clone(), &lvm_devices, &zfs_devices, &name) {
835 Ok(part_usage) => {
836 if part_usage != DiskUsageType::Unused {
837 usage = part_usage;
838 }
839 },
840 Err(_) => continue, // skip devices if scan_partitions fail
841 };
842 }
843
844 if usage == DiskUsageType::Unused && file_system_devices.contains(&devnum) {
845 usage = DiskUsageType::FileSystem;
846 }
847
848 if usage == DiskUsageType::Unused && disk.has_holders()? {
849 usage = DiskUsageType::DeviceMapper;
850 }
851
852 let mut status = SmartStatus::Unknown;
853 let mut wearout = None;
854
855 if !no_smart {
856 if let Ok(smart) = get_smart_data(&disk, false) {
857 status = smart.status;
858 wearout = smart.wearout;
859 }
860 }
861
862 let info = DiskUsageInfo {
863 name: name.clone(),
864 vendor, model, serial, devpath, size, wwn, disk_type,
865 status, wearout,
866 used: usage,
867 gpt: disk.has_gpt(),
868 rpm: disk.ata_rotation_rate_rpm(),
869 };
870
871 result.insert(name, info);
872 }
873
874 Ok(result)
875 }
876
877 /// Try to reload the partition table
878 pub fn reread_partition_table(disk: &Disk) -> Result<(), Error> {
879
880 let disk_path = match disk.device_path() {
881 Some(path) => path,
882 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
883 };
884
885 let mut command = std::process::Command::new("blockdev");
886 command.arg("--rereadpt");
887 command.arg(disk_path);
888
889 pbs_tools::run_command(command, None)?;
890
891 Ok(())
892 }
893
894 /// Initialize disk by writing a GPT partition table
895 pub fn inititialize_gpt_disk(disk: &Disk, uuid: Option<&str>) -> Result<(), Error> {
896
897 let disk_path = match disk.device_path() {
898 Some(path) => path,
899 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
900 };
901
902 let uuid = uuid.unwrap_or("R"); // R .. random disk GUID
903
904 let mut command = std::process::Command::new("sgdisk");
905 command.arg(disk_path);
906 command.args(&["-U", uuid]);
907
908 pbs_tools::run_command(command, None)?;
909
910 Ok(())
911 }
912
913 /// Create a single linux partition using the whole available space
914 pub fn create_single_linux_partition(disk: &Disk) -> Result<Disk, Error> {
915
916 let disk_path = match disk.device_path() {
917 Some(path) => path,
918 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
919 };
920
921 let mut command = std::process::Command::new("sgdisk");
922 command.args(&["-n1", "-t1:8300"]);
923 command.arg(disk_path);
924
925 pbs_tools::run_command(command, None)?;
926
927 let mut partitions = disk.partitions()?;
928
929 match partitions.remove(&1) {
930 Some(partition) => Ok(partition),
931 None => bail!("unable to lookup device partition"),
932 }
933 }
934
935 #[api()]
936 #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
937 #[serde(rename_all="lowercase")]
938 pub enum FileSystemType {
939 /// Linux Ext4
940 Ext4,
941 /// XFS
942 Xfs,
943 }
944
945 impl std::fmt::Display for FileSystemType {
946 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
947 let text = match self {
948 FileSystemType::Ext4 => "ext4",
949 FileSystemType::Xfs => "xfs",
950 };
951 write!(f, "{}", text)
952 }
953 }
954
955 impl std::str::FromStr for FileSystemType {
956 type Err = serde_json::Error;
957
958 fn from_str(s: &str) -> Result<Self, Self::Err> {
959 use serde::de::IntoDeserializer;
960 Self::deserialize(s.into_deserializer())
961 }
962 }
963
964 /// Create a file system on a disk or disk partition
965 pub fn create_file_system(disk: &Disk, fs_type: FileSystemType) -> Result<(), Error> {
966
967 let disk_path = match disk.device_path() {
968 Some(path) => path,
969 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
970 };
971
972 let fs_type = fs_type.to_string();
973
974 let mut command = std::process::Command::new("mkfs");
975 command.args(&["-t", &fs_type]);
976 command.arg(disk_path);
977
978 pbs_tools::run_command(command, None)?;
979
980 Ok(())
981 }
982
983 /// Block device name completion helper
984 pub fn complete_disk_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
985 let mut list = Vec::new();
986
987 let dir = match pbs_tools::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX) {
988 Ok(dir) => dir,
989 Err(_) => return list,
990 };
991
992 for item in dir {
993 if let Ok(item) = item {
994 let name = item.file_name().to_str().unwrap().to_string();
995 list.push(name);
996 }
997 }
998
999 list
1000 }
1001
1002 /// Read the FS UUID (parse blkid output)
1003 ///
1004 /// Note: Calling blkid is more reliable than using the udev ID_FS_UUID property.
1005 pub fn get_fs_uuid(disk: &Disk) -> Result<String, Error> {
1006
1007 let disk_path = match disk.device_path() {
1008 Some(path) => path,
1009 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
1010 };
1011
1012 let mut command = std::process::Command::new("blkid");
1013 command.args(&["-o", "export"]);
1014 command.arg(disk_path);
1015
1016 let output = pbs_tools::run_command(command, None)?;
1017
1018 for line in output.lines() {
1019 if let Some(uuid) = line.strip_prefix("UUID=") {
1020 return Ok(uuid.to_string());
1021 }
1022 }
1023
1024 bail!("get_fs_uuid failed - missing UUID");
1025 }