]> git.proxmox.com Git - proxmox-backup.git/blame - src/tools/disks/mod.rs
tree-wide: fix various typos
[proxmox-backup.git] / src / tools / disks / mod.rs
CommitLineData
10effc98
WB
1//! Disk query/management utilities for.
2
5c264c8d 3use std::collections::{HashMap, HashSet};
10effc98
WB
4use std::ffi::{OsStr, OsString};
5use std::io;
6use std::os::unix::ffi::{OsStrExt, OsStringExt};
d406de29 7use std::os::unix::fs::MetadataExt;
10effc98
WB
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
707974fd 11use anyhow::{bail, format_err, Error};
10effc98
WB
12use libc::dev_t;
13use once_cell::sync::OnceCell;
14
de1e1a9d
DM
15use ::serde::{Deserialize, Serialize};
16
f26d7ca5 17use proxmox_lang::error::io_err_other;
f26d7ca5 18use proxmox_lang::{io_bail, io_format_err};
4b1c7e35 19use proxmox_rest_server::WorkerTask;
9531d2c5
TL
20use proxmox_schema::api;
21use proxmox_sys::linux::procfs::{mountinfo::Device, MountInfo};
4b1c7e35 22use proxmox_sys::task_log;
10effc98 23
4b1c7e35 24use pbs_api_types::{BLOCKDEVICE_DISK_AND_PARTITION_NAME_REGEX, BLOCKDEVICE_NAME_REGEX};
9069debc 25
5c264c8d
DM
26mod zfs;
27pub use zfs::*;
0727e56a
DM
28mod zpool_status;
29pub use zpool_status::*;
0686b1f4
DM
30mod zpool_list;
31pub use zpool_list::*;
620911b4
DM
32mod lvm;
33pub use lvm::*;
eb80aac2
DM
34mod smart;
35pub use smart::*;
0146133b 36
af6fdb9d 37lazy_static::lazy_static! {
d2522b2d
DM
38 static ref ISCSI_PATH_REGEX: regex::Regex =
39 regex::Regex::new(r"host[^/]*/session[^/]*").unwrap();
d2522b2d
DM
40}
41
10effc98
WB
42/// Disk management context.
43///
44/// This provides access to disk information with some caching for faster querying of multiple
45/// devices.
46pub struct DiskManage {
47 mount_info: OnceCell<MountInfo>,
48 mounted_devices: OnceCell<HashSet<dev_t>>,
49}
50
36429974
FE
51/// Information for a device as returned by lsblk.
52#[derive(Deserialize)]
53pub struct LsblkInfo {
54 /// Path to the device.
55 path: String,
56 /// Partition type GUID.
57 #[serde(rename = "parttype")]
58 partition_type: Option<String>,
20429238
FE
59 /// File system label.
60 #[serde(rename = "fstype")]
61 file_system_type: Option<String>,
36429974
FE
62}
63
10effc98
WB
64impl DiskManage {
65 /// Create a new disk management context.
66 pub fn new() -> Arc<Self> {
67 Arc::new(Self {
68 mount_info: OnceCell::new(),
69 mounted_devices: OnceCell::new(),
70 })
71 }
72
73 /// Get the current mount info. This simply caches the result of `MountInfo::read` from the
74 /// `proxmox::sys` module.
75 pub fn mount_info(&self) -> Result<&MountInfo, Error> {
76 self.mount_info.get_or_try_init(MountInfo::read)
77 }
78
79 /// Get a `Disk` from a device node (eg. `/dev/sda`).
80 pub fn disk_by_node<P: AsRef<Path>>(self: Arc<Self>, devnode: P) -> io::Result<Disk> {
10effc98
WB
81 let devnode = devnode.as_ref();
82
83 let meta = std::fs::metadata(devnode)?;
84 if (meta.mode() & libc::S_IFBLK) == libc::S_IFBLK {
85 self.disk_by_dev_num(meta.rdev())
86 } else {
87 io_bail!("not a block device: {:?}", devnode);
88 }
89 }
90
91 /// Get a `Disk` for a specific device number.
92 pub fn disk_by_dev_num(self: Arc<Self>, devnum: dev_t) -> io::Result<Disk> {
93 self.disk_by_sys_path(format!(
94 "/sys/dev/block/{}:{}",
95 unsafe { libc::major(devnum) },
96 unsafe { libc::minor(devnum) },
97 ))
98 }
99
100 /// Get a `Disk` for a path in `/sys`.
101 pub fn disk_by_sys_path<P: AsRef<Path>>(self: Arc<Self>, path: P) -> io::Result<Disk> {
102 let device = udev::Device::from_syspath(path.as_ref())?;
103 Ok(Disk {
104 manager: self,
105 device,
106 info: Default::default(),
107 })
108 }
109
042afd6e
DM
110 /// Get a `Disk` for a name in `/sys/block/<name>`.
111 pub fn disk_by_name(self: Arc<Self>, name: &str) -> io::Result<Disk> {
112 let syspath = format!("/sys/block/{}", name);
cd0daa8b 113 self.disk_by_sys_path(syspath)
042afd6e
DM
114 }
115
4b1c7e35
MF
116 /// Get a `Disk` for a name in `/sys/class/block/<name>`.
117 pub fn partition_by_name(self: Arc<Self>, name: &str) -> io::Result<Disk> {
118 let syspath = format!("/sys/class/block/{}", name);
119 self.disk_by_sys_path(syspath)
120 }
121
10effc98
WB
122 /// Gather information about mounted disks:
123 fn mounted_devices(&self) -> Result<&HashSet<dev_t>, Error> {
10effc98
WB
124 self.mounted_devices
125 .get_or_try_init(|| -> Result<_, Error> {
126 let mut mounted = HashSet::new();
127
128 for (_id, mp) in self.mount_info()? {
1e0c6194 129 let source = match mp.mount_source.as_deref() {
10effc98
WB
130 Some(s) => s,
131 None => continue,
132 };
133
134 let path = Path::new(source);
135 if !path.is_absolute() {
136 continue;
137 }
138
139 let meta = match std::fs::metadata(path) {
140 Ok(meta) => meta,
141 Err(ref err) if err.kind() == io::ErrorKind::NotFound => continue,
142 Err(other) => return Err(Error::from(other)),
143 };
144
145 if (meta.mode() & libc::S_IFBLK) != libc::S_IFBLK {
146 // not a block device
147 continue;
148 }
149
150 mounted.insert(meta.rdev());
151 }
152
153 Ok(mounted)
154 })
155 }
156
1ffe0301 157 /// Information about file system type and used device for a path
934f5bb8
DM
158 ///
159 /// Returns tuple (fs_type, device, mount_source)
160 pub fn find_mounted_device(
161 &self,
162 path: &std::path::Path,
163 ) -> Result<Option<(String, Device, Option<OsString>)>, Error> {
934f5bb8
DM
164 let stat = nix::sys::stat::stat(path)?;
165 let device = Device::from_dev_t(stat.st_dev);
166
167 let root_path = std::path::Path::new("/");
168
169 for (_id, entry) in self.mount_info()? {
170 if entry.root == root_path && entry.device == device {
af6fdb9d
TL
171 return Ok(Some((
172 entry.fs_type.clone(),
173 entry.device,
174 entry.mount_source.clone(),
175 )));
934f5bb8
DM
176 }
177 }
178
179 Ok(None)
180 }
181
10effc98
WB
182 /// Check whether a specific device node is mounted.
183 ///
184 /// Note that this tries to `stat` the sources of all mount points without caching the result
185 /// of doing so, so this is always somewhat expensive.
186 pub fn is_devnum_mounted(&self, dev: dev_t) -> Result<bool, Error> {
187 self.mounted_devices().map(|mounted| mounted.contains(&dev))
188 }
189}
190
191/// Queries (and caches) various information about a specific disk.
192///
193/// This belongs to a `Disks` and provides information for a single disk.
194pub struct Disk {
195 manager: Arc<DiskManage>,
196 device: udev::Device,
197 info: DiskInfo,
198}
199
200/// Helper struct (so we can initialize this with Default)
201///
202/// We probably want this to be serializable to the same hash type we use in perl currently.
203#[derive(Default)]
204struct DiskInfo {
205 size: OnceCell<u64>,
206 vendor: OnceCell<Option<OsString>>,
207 model: OnceCell<Option<OsString>>,
208 rotational: OnceCell<Option<bool>>,
209 // for perl: #[serde(rename = "devpath")]
210 ata_rotation_rate_rpm: OnceCell<Option<u64>>,
211 // for perl: #[serde(rename = "devpath")]
212 device_path: OnceCell<Option<PathBuf>>,
213 wwn: OnceCell<Option<OsString>>,
214 serial: OnceCell<Option<OsString>>,
215 // for perl: #[serde(skip_serializing)]
216 partition_table_type: OnceCell<Option<OsString>>,
217 gpt: OnceCell<bool>,
218 // ???
219 bus: OnceCell<Option<OsString>>,
220 // ???
221 fs_type: OnceCell<Option<OsString>>,
222 // ???
223 has_holders: OnceCell<bool>,
224 // ???
225 is_mounted: OnceCell<bool>,
226}
227
228impl Disk {
229 /// Try to get the device number for this disk.
230 ///
231 /// (In udev this can fail...)
232 pub fn devnum(&self) -> Result<dev_t, Error> {
233 // not sure when this can fail...
234 self.device
235 .devnum()
236 .ok_or_else(|| format_err!("failed to get device number"))
237 }
238
239 /// Get the sys-name of this device. (The final component in the `/sys` path).
240 pub fn sysname(&self) -> &OsStr {
241 self.device.sysname()
242 }
243
244 /// Get the this disk's `/sys` path.
245 pub fn syspath(&self) -> &Path {
246 self.device.syspath()
247 }
248
249 /// Get the device node in `/dev`, if any.
250 pub fn device_path(&self) -> Option<&Path> {
251 //self.device.devnode()
252 self.info
253 .device_path
254 .get_or_init(|| self.device.devnode().map(Path::to_owned))
255 .as_ref()
256 .map(PathBuf::as_path)
257 }
258
259 /// Get the parent device.
260 pub fn parent(&self) -> Option<Self> {
261 self.device.parent().map(|parent| Self {
262 manager: self.manager.clone(),
263 device: parent,
264 info: Default::default(),
265 })
266 }
267
268 /// Read from a file in this device's sys path.
269 ///
270 /// Note: path must be a relative path!
3ed07ed2 271 pub fn read_sys(&self, path: &Path) -> io::Result<Option<Vec<u8>>> {
10effc98
WB
272 assert!(path.is_relative());
273
274 std::fs::read(self.syspath().join(path))
275 .map(Some)
276 .or_else(|err| {
277 if err.kind() == io::ErrorKind::NotFound {
278 Ok(None)
279 } else {
280 Err(err)
281 }
282 })
283 }
284
285 /// Convenience wrapper for reading a `/sys` file which contains just a simple `OsString`.
ca6124d5 286 pub fn read_sys_os_str<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<OsString>> {
4c1e8855
DM
287 Ok(self.read_sys(path.as_ref())?.map(|mut v| {
288 if Some(&b'\n') == v.last() {
289 v.pop();
290 }
291 OsString::from_vec(v)
292 }))
10effc98
WB
293 }
294
295 /// Convenience wrapper for reading a `/sys` file which contains just a simple utf-8 string.
ca6124d5 296 pub fn read_sys_str<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<String>> {
10effc98
WB
297 Ok(match self.read_sys(path.as_ref())? {
298 Some(data) => Some(String::from_utf8(data).map_err(io_err_other)?),
299 None => None,
300 })
301 }
302
303 /// Convenience wrapper for unsigned integer `/sys` values up to 64 bit.
ca6124d5 304 pub fn read_sys_u64<P: AsRef<Path>>(&self, path: P) -> io::Result<Option<u64>> {
10effc98
WB
305 Ok(match self.read_sys_str(path)? {
306 Some(data) => Some(data.trim().parse().map_err(io_err_other)?),
307 None => None,
308 })
309 }
310
311 /// Get the disk's size in bytes.
312 pub fn size(&self) -> io::Result<u64> {
313 Ok(*self.info.size.get_or_try_init(|| {
af6fdb9d 314 self.read_sys_u64("size")?.map(|s| s * 512).ok_or_else(|| {
10effc98
WB
315 io_format_err!(
316 "failed to get disk size from {:?}",
317 self.syspath().join("size"),
318 )
319 })
320 })?)
321 }
322
323 /// Get the device vendor (`/sys/.../device/vendor`) entry if available.
324 pub fn vendor(&self) -> io::Result<Option<&OsStr>> {
325 Ok(self
326 .info
327 .vendor
328 .get_or_try_init(|| self.read_sys_os_str("device/vendor"))?
329 .as_ref()
330 .map(OsString::as_os_str))
331 }
332
333 /// Get the device model (`/sys/.../device/model`) entry if available.
334 pub fn model(&self) -> Option<&OsStr> {
335 self.info
336 .model
337 .get_or_init(|| self.device.property_value("ID_MODEL").map(OsStr::to_owned))
338 .as_ref()
339 .map(OsString::as_os_str)
340 }
341
342 /// Check whether this is a rotational disk.
343 ///
344 /// Returns `None` if there's no `queue/rotational` file, in which case no information is
345 /// known. `Some(false)` if `queue/rotational` is zero, `Some(true)` if it has a non-zero
346 /// value.
347 pub fn rotational(&self) -> io::Result<Option<bool>> {
348 Ok(*self
349 .info
350 .rotational
351 .get_or_try_init(|| -> io::Result<Option<bool>> {
352 Ok(self.read_sys_u64("queue/rotational")?.map(|n| n != 0))
353 })?)
354 }
355
356 /// Get the WWN if available.
357 pub fn wwn(&self) -> Option<&OsStr> {
358 self.info
359 .wwn
360 .get_or_init(|| self.device.property_value("ID_WWN").map(|v| v.to_owned()))
361 .as_ref()
362 .map(OsString::as_os_str)
363 }
364
365 /// Get the device serial if available.
366 pub fn serial(&self) -> Option<&OsStr> {
367 self.info
368 .serial
369 .get_or_init(|| {
370 self.device
371 .property_value("ID_SERIAL_SHORT")
372 .map(|v| v.to_owned())
373 })
374 .as_ref()
375 .map(OsString::as_os_str)
376 }
377
378 /// Get the ATA rotation rate value from udev. This is not necessarily the same as sysfs'
379 /// `rotational` value.
380 pub fn ata_rotation_rate_rpm(&self) -> Option<u64> {
381 *self.info.ata_rotation_rate_rpm.get_or_init(|| {
382 std::str::from_utf8(
383 self.device
384 .property_value("ID_ATA_ROTATION_RATE_RPM")?
385 .as_bytes(),
386 )
387 .ok()?
388 .parse()
389 .ok()
390 })
391 }
392
393 /// Get the partition table type, if any.
394 pub fn partition_table_type(&self) -> Option<&OsStr> {
395 self.info
396 .partition_table_type
397 .get_or_init(|| {
398 self.device
399 .property_value("ID_PART_TABLE_TYPE")
400 .map(|v| v.to_owned())
401 })
402 .as_ref()
403 .map(OsString::as_os_str)
404 }
405
406 /// Check if this contains a GPT partition table.
407 pub fn has_gpt(&self) -> bool {
408 *self.info.gpt.get_or_init(|| {
409 self.partition_table_type()
410 .map(|s| s == "gpt")
411 .unwrap_or(false)
412 })
413 }
414
415 /// Get the bus type used for this disk.
416 pub fn bus(&self) -> Option<&OsStr> {
417 self.info
418 .bus
419 .get_or_init(|| self.device.property_value("ID_BUS").map(|v| v.to_owned()))
420 .as_ref()
421 .map(OsString::as_os_str)
422 }
423
424 /// Attempt to guess the disk type.
425 pub fn guess_disk_type(&self) -> io::Result<DiskType> {
426 Ok(match self.rotational()? {
4c1e8855 427 Some(false) => DiskType::Ssd,
10effc98 428 Some(true) => DiskType::Hdd,
4c1e8855 429 None => match self.ata_rotation_rate_rpm() {
10effc98
WB
430 Some(_) => DiskType::Hdd,
431 None => match self.bus() {
432 Some(bus) if bus == "usb" => DiskType::Usb,
433 _ => DiskType::Unknown,
434 },
435 },
436 })
437 }
438
439 /// Get the file system type found on the disk, if any.
440 ///
441 /// Note that `None` may also just mean "unknown".
442 pub fn fs_type(&self) -> Option<&OsStr> {
443 self.info
444 .fs_type
445 .get_or_init(|| {
446 self.device
447 .property_value("ID_FS_TYPE")
448 .map(|v| v.to_owned())
449 })
450 .as_ref()
451 .map(OsString::as_os_str)
452 }
453
454 /// Check if there are any "holders" in `/sys`. This usually means the device is in use by
455 /// another kernel driver like the device mapper.
456 pub fn has_holders(&self) -> io::Result<bool> {
457 Ok(*self
af6fdb9d
TL
458 .info
459 .has_holders
460 .get_or_try_init(|| -> io::Result<bool> {
461 let mut subdir = self.syspath().to_owned();
462 subdir.push("holders");
463 for entry in std::fs::read_dir(subdir)? {
464 match entry?.file_name().as_bytes() {
465 b"." | b".." => (),
466 _ => return Ok(true),
467 }
468 }
469 Ok(false)
470 })?)
10effc98
WB
471 }
472
473 /// Check if this disk is mounted.
474 pub fn is_mounted(&self) -> Result<bool, Error> {
475 Ok(*self
476 .info
477 .is_mounted
478 .get_or_try_init(|| self.manager.is_devnum_mounted(self.devnum()?))?)
479 }
3fcc4b4e
DM
480
481 /// Read block device stats
c94e1f65 482 ///
e92df238 483 /// see <https://www.kernel.org/doc/Documentation/block/stat.txt>
3fcc4b4e
DM
484 pub fn read_stat(&self) -> std::io::Result<Option<BlockDevStat>> {
485 if let Some(stat) = self.read_sys(Path::new("stat"))? {
486 let stat = unsafe { std::str::from_utf8_unchecked(&stat) };
af6fdb9d
TL
487 let stat: Vec<u64> = stat
488 .split_ascii_whitespace()
e1db0670 489 .map(|s| s.parse().unwrap_or_default())
af6fdb9d 490 .collect();
3fcc4b4e 491
af6fdb9d
TL
492 if stat.len() < 15 {
493 return Ok(None);
494 }
3fcc4b4e
DM
495
496 return Ok(Some(BlockDevStat {
497 read_ios: stat[0],
3fcc4b4e 498 read_sectors: stat[2],
af6fdb9d 499 write_ios: stat[4] + stat[11], // write + discard
c94e1f65
DM
500 write_sectors: stat[6] + stat[13], // write + discard
501 io_ticks: stat[10],
af6fdb9d 502 }));
3fcc4b4e
DM
503 }
504 Ok(None)
505 }
0f358204
DM
506
507 /// List device partitions
508 pub fn partitions(&self) -> Result<HashMap<u64, Disk>, Error> {
0f358204
DM
509 let sys_path = self.syspath();
510 let device = self.sysname().to_string_lossy().to_string();
511
512 let mut map = HashMap::new();
513
25877d05 514 for item in proxmox_sys::fs::read_subdir(libc::AT_FDCWD, sys_path)? {
0f358204
DM
515 let item = item?;
516 let name = match item.file_name().to_str() {
517 Ok(name) => name,
518 Err(_) => continue, // skip non utf8 entries
519 };
520
af6fdb9d
TL
521 if !name.starts_with(&device) {
522 continue;
523 }
0f358204
DM
524
525 let mut part_path = sys_path.to_owned();
526 part_path.push(name);
527
528 let disk_part = self.manager.clone().disk_by_sys_path(&part_path)?;
529
530 if let Some(partition) = disk_part.read_sys_u64("partition")? {
531 map.insert(partition, disk_part);
532 }
533 }
534
535 Ok(map)
536 }
10effc98
WB
537}
538
de1e1a9d
DM
539#[api()]
540#[derive(Debug, Serialize, Deserialize)]
af6fdb9d 541#[serde(rename_all = "lowercase")]
10effc98
WB
542/// This is just a rough estimate for a "type" of disk.
543pub enum DiskType {
544 /// We know nothing.
545 Unknown,
546
547 /// May also be a USB-HDD.
548 Hdd,
549
550 /// May also be a USB-SSD.
551 Ssd,
552
553 /// Some kind of USB disk, but we don't know more than that.
554 Usb,
555}
3fcc4b4e
DM
556
557#[derive(Debug)]
5116d051 558/// Represents the contents of the `/sys/block/<dev>/stat` file.
3fcc4b4e
DM
559pub struct BlockDevStat {
560 pub read_ios: u64,
3fcc4b4e 561 pub read_sectors: u64,
3fcc4b4e 562 pub write_ios: u64,
3fcc4b4e 563 pub write_sectors: u64,
c94e1f65 564 pub io_ticks: u64, // milliseconds
3fcc4b4e 565}
5c264c8d 566
20429238 567/// Use lsblk to read partition type uuids and file system types.
36429974 568pub fn get_lsblk_info() -> Result<Vec<LsblkInfo>, Error> {
cbef49bf 569 let mut command = std::process::Command::new("lsblk");
16f6766a 570 command.args(["--json", "-o", "path,parttype,fstype"]);
5c264c8d 571
25877d05 572 let output = proxmox_sys::command::run_command(command, None)?;
5c264c8d 573
36429974 574 let mut output: serde_json::Value = output.parse()?;
5c264c8d 575
36429974 576 Ok(serde_json::from_value(output["blockdevices"].take())?)
5c264c8d 577}
c26aad40 578
20429238
FE
579/// Get set of devices with a file system label.
580///
581/// The set is indexed by using the unix raw device number (dev_t is u64)
af6fdb9d 582fn get_file_system_devices(lsblk_info: &[LsblkInfo]) -> Result<HashSet<u64>, Error> {
20429238
FE
583 let mut device_set: HashSet<u64> = HashSet::new();
584
585 for info in lsblk_info.iter() {
586 if info.file_system_type.is_some() {
587 let meta = std::fs::metadata(&info.path)?;
588 device_set.insert(meta.rdev());
589 }
590 }
591
592 Ok(device_set)
593}
594
6a6ba4cd 595#[api()]
e1ea9135 596#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
6a6ba4cd
HL
597#[serde(rename_all = "lowercase")]
598pub enum PartitionUsageType {
599 /// Partition is not used (as far we can tell)
600 Unused,
601 /// Partition is used by LVM
602 LVM,
603 /// Partition is used by ZFS
604 ZFS,
605 /// Partition is ZFS reserved
606 ZfsReserved,
607 /// Partition is an EFI partition
608 EFI,
609 /// Partition is a BIOS partition
610 BIOS,
611 /// Partition contains a file system label
612 FileSystem,
613}
614
de1e1a9d 615#[api()]
e1ea9135 616#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
af6fdb9d 617#[serde(rename_all = "lowercase")]
c26aad40 618pub enum DiskUsageType {
de1e1a9d 619 /// Disk is not used (as far we can tell)
c26aad40 620 Unused,
de1e1a9d 621 /// Disk is mounted
c26aad40 622 Mounted,
de1e1a9d 623 /// Disk is used by LVM
c26aad40 624 LVM,
de1e1a9d 625 /// Disk is used by ZFS
c26aad40 626 ZFS,
de1e1a9d 627 /// Disk is used by device-mapper
c26aad40 628 DeviceMapper,
de1e1a9d 629 /// Disk has partitions
c26aad40 630 Partitions,
20429238
FE
631 /// Disk contains a file system label
632 FileSystem,
c26aad40
DM
633}
634
6a6ba4cd
HL
635#[api()]
636#[derive(Debug, Serialize, Deserialize)]
637#[serde(rename_all = "kebab-case")]
6685122c 638/// Basic information about a partition
6a6ba4cd
HL
639pub struct PartitionInfo {
640 /// The partition name
641 pub name: String,
642 /// What the partition is used for
643 pub used: PartitionUsageType,
644 /// Is the partition mounted
645 pub mounted: bool,
646 /// The filesystem of the partition
647 pub filesystem: Option<String>,
648 /// The partition devpath
649 pub devpath: Option<String>,
650 /// Size in bytes
651 pub size: Option<u64>,
652 /// GPT partition
653 pub gpt: bool,
654}
655
de1e1a9d
DM
656#[api(
657 properties: {
658 used: {
659 type: DiskUsageType,
660 },
661 "disk-type": {
662 type: DiskType,
663 },
664 status: {
665 type: SmartStatus,
6a6ba4cd
HL
666 },
667 partitions: {
668 optional: true,
669 items: {
670 type: PartitionInfo
671 }
de1e1a9d
DM
672 }
673 }
674)]
675#[derive(Debug, Serialize, Deserialize)]
af6fdb9d 676#[serde(rename_all = "kebab-case")]
de1e1a9d 677/// Information about how a Disk is used
c26aad40 678pub struct DiskUsageInfo {
5116d051 679 /// Disk name (`/sys/block/<name>`)
c26aad40
DM
680 pub name: String,
681 pub used: DiskUsageType,
682 pub disk_type: DiskType,
91960d61 683 pub status: SmartStatus,
de1e1a9d 684 /// Disk wearout
91960d61 685 pub wearout: Option<f64>,
de1e1a9d 686 /// Vendor
c26aad40 687 pub vendor: Option<String>,
de1e1a9d 688 /// Model
c26aad40 689 pub model: Option<String>,
de1e1a9d 690 /// WWN
c26aad40 691 pub wwn: Option<String>,
de1e1a9d 692 /// Disk size
c26aad40 693 pub size: u64,
de1e1a9d 694 /// Serisal number
c26aad40 695 pub serial: Option<String>,
6a6ba4cd
HL
696 /// Partitions on the device
697 pub partitions: Option<Vec<PartitionInfo>>,
de1e1a9d
DM
698 /// Linux device path (/dev/xxx)
699 pub devpath: Option<String>,
700 /// Set if disk contains a GPT partition table
c26aad40 701 pub gpt: bool,
de1e1a9d 702 /// RPM
c26aad40
DM
703 pub rpm: Option<u64>,
704}
705
706fn scan_partitions(
707 disk_manager: Arc<DiskManage>,
d406de29
DM
708 lvm_devices: &HashSet<u64>,
709 zfs_devices: &HashSet<u64>,
c26aad40
DM
710 device: &str,
711) -> Result<DiskUsageType, Error> {
c26aad40
DM
712 let mut sys_path = std::path::PathBuf::from("/sys/block");
713 sys_path.push(device);
714
715 let mut used = DiskUsageType::Unused;
716
717 let mut found_lvm = false;
718 let mut found_zfs = false;
719 let mut found_mountpoints = false;
720 let mut found_dm = false;
721 let mut found_partitions = false;
722
25877d05 723 for item in proxmox_sys::fs::read_subdir(libc::AT_FDCWD, &sys_path)? {
c26aad40
DM
724 let item = item?;
725 let name = match item.file_name().to_str() {
726 Ok(name) => name,
727 Err(_) => continue, // skip non utf8 entries
728 };
af6fdb9d
TL
729 if !name.starts_with(device) {
730 continue;
731 }
c26aad40
DM
732
733 found_partitions = true;
734
735 let mut part_path = sys_path.clone();
736 part_path.push(name);
737
738 let data = disk_manager.clone().disk_by_sys_path(&part_path)?;
739
d406de29
DM
740 let devnum = data.devnum()?;
741
742 if lvm_devices.contains(&devnum) {
c26aad40
DM
743 found_lvm = true;
744 }
745
746 if data.is_mounted()? {
747 found_mountpoints = true;
748 }
749
750 if data.has_holders()? {
751 found_dm = true;
752 }
753
af6fdb9d 754 if zfs_devices.contains(&devnum) {
c26aad40 755 found_zfs = true;
af6fdb9d 756 }
c26aad40
DM
757 }
758
759 if found_mountpoints {
760 used = DiskUsageType::Mounted;
761 } else if found_lvm {
762 used = DiskUsageType::LVM;
763 } else if found_zfs {
764 used = DiskUsageType::ZFS;
765 } else if found_dm {
766 used = DiskUsageType::DeviceMapper;
767 } else if found_partitions {
768 used = DiskUsageType::Partitions;
769 }
770
771 Ok(used)
772}
773
be260410
HL
774pub struct DiskUsageQuery {
775 smart: bool,
776 partitions: bool,
777}
778
779impl DiskUsageQuery {
c54aeedb 780 pub const fn new() -> Self {
be260410
HL
781 Self {
782 smart: true,
783 partitions: false,
784 }
785 }
786
787 pub fn smart(&mut self, smart: bool) -> &mut Self {
788 self.smart = smart;
789 self
790 }
791
792 pub fn partitions(&mut self, partitions: bool) -> &mut Self {
793 self.partitions = partitions;
794 self
795 }
796
797 pub fn query(&self) -> Result<HashMap<String, DiskUsageInfo>, Error> {
798 get_disks(None, !self.smart, self.partitions)
799 }
800
801 pub fn find(&self, disk: &str) -> Result<DiskUsageInfo, Error> {
802 let mut map = get_disks(Some(vec![disk.to_string()]), !self.smart, self.partitions)?;
803 if let Some(info) = map.remove(disk) {
804 Ok(info)
805 } else {
806 bail!("failed to get disk usage info - internal error"); // should not happen
807 }
808 }
809
810 pub fn find_all(&self, disks: Vec<String>) -> Result<HashMap<String, DiskUsageInfo>, Error> {
811 get_disks(Some(disks), !self.smart, self.partitions)
707974fd
DM
812 }
813}
814
6a6ba4cd
HL
815fn get_partitions_info(
816 partitions: HashMap<u64, Disk>,
817 lvm_devices: &HashSet<u64>,
818 zfs_devices: &HashSet<u64>,
819 file_system_devices: &HashSet<u64>,
820) -> Vec<PartitionInfo> {
821 let lsblk_infos = get_lsblk_info().ok();
822 partitions
b6e7fc9b
TL
823 .values()
824 .map(|disk| {
6a6ba4cd
HL
825 let devpath = disk
826 .device_path()
827 .map(|p| p.to_owned())
828 .map(|p| p.to_string_lossy().to_string());
829
830 let mut used = PartitionUsageType::Unused;
831
e1db0670 832 if let Ok(devnum) = disk.devnum() {
6a6ba4cd
HL
833 if lvm_devices.contains(&devnum) {
834 used = PartitionUsageType::LVM;
835 } else if zfs_devices.contains(&devnum) {
836 used = PartitionUsageType::ZFS;
837 } else if file_system_devices.contains(&devnum) {
838 used = PartitionUsageType::FileSystem;
839 }
840 }
841
842 let mounted = disk.is_mounted().unwrap_or(false);
843 let mut filesystem = None;
844 if let (Some(devpath), Some(infos)) = (devpath.as_ref(), lsblk_infos.as_ref()) {
845 for info in infos.iter().filter(|i| i.path.eq(devpath)) {
846 used = match info.partition_type.as_deref() {
847 Some("21686148-6449-6e6f-744e-656564454649") => PartitionUsageType::BIOS,
848 Some("c12a7328-f81f-11d2-ba4b-00a0c93ec93b") => PartitionUsageType::EFI,
849 Some("6a945a3b-1dd2-11b2-99a6-080020736631") => {
850 PartitionUsageType::ZfsReserved
851 }
852 _ => used,
853 };
854 if used == PartitionUsageType::FileSystem {
855 filesystem = info.file_system_type.clone();
856 }
857 }
858 }
859
860 PartitionInfo {
861 name: disk.sysname().to_str().unwrap_or("?").to_string(),
862 devpath,
863 used,
864 mounted,
865 filesystem,
866 size: disk.size().ok(),
867 gpt: disk.has_gpt(),
868 }
869 })
870 .collect()
871}
872
707974fd 873/// Get disk usage information for multiple disks
be260410 874fn get_disks(
c26aad40
DM
875 // filter - list of device names (without leading /dev)
876 disks: Option<Vec<String>>,
877 // do no include data from smartctl
878 no_smart: bool,
6a6ba4cd
HL
879 // include partitions
880 include_partitions: bool,
c26aad40 881) -> Result<HashMap<String, DiskUsageInfo>, Error> {
c26aad40
DM
882 let disk_manager = DiskManage::new();
883
36429974 884 let lsblk_info = get_lsblk_info()?;
c26aad40 885
af6fdb9d
TL
886 let zfs_devices =
887 zfs_devices(&lsblk_info, None).or_else(|err| -> Result<HashSet<u64>, Error> {
888 eprintln!("error getting zfs devices: {}", err);
889 Ok(HashSet::new())
890 })?;
c26aad40 891
36429974 892 let lvm_devices = get_lvm_devices(&lsblk_info)?;
c26aad40 893
20429238
FE
894 let file_system_devices = get_file_system_devices(&lsblk_info)?;
895
c26aad40
DM
896 // fixme: ceph journals/volumes
897
c26aad40
DM
898 let mut result = HashMap::new();
899
af6fdb9d
TL
900 for item in proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX)?
901 {
c26aad40
DM
902 let item = item?;
903
904 let name = item.file_name().to_str().unwrap().to_string();
905
906 if let Some(ref disks) = disks {
af6fdb9d
TL
907 if !disks.contains(&name) {
908 continue;
909 }
c26aad40
DM
910 }
911
912 let sys_path = format!("/sys/block/{}", name);
913
914 if let Ok(target) = std::fs::read_link(&sys_path) {
915 if let Some(target) = target.to_str() {
af6fdb9d
TL
916 if ISCSI_PATH_REGEX.is_match(target) {
917 continue;
918 } // skip iSCSI devices
c26aad40
DM
919 }
920 }
921
91960d61 922 let disk = disk_manager.clone().disk_by_sys_path(&sys_path)?;
c26aad40 923
d406de29
DM
924 let devnum = disk.devnum()?;
925
91960d61 926 let size = match disk.size() {
c26aad40
DM
927 Ok(size) => size,
928 Err(_) => continue, // skip devices with unreadable size
929 };
930
91960d61 931 let disk_type = match disk.guess_disk_type() {
c26aad40
DM
932 Ok(disk_type) => disk_type,
933 Err(_) => continue, // skip devices with undetectable type
934 };
935
936 let mut usage = DiskUsageType::Unused;
937
d406de29 938 if lvm_devices.contains(&devnum) {
c26aad40
DM
939 usage = DiskUsageType::LVM;
940 }
941
91960d61 942 match disk.is_mounted() {
c26aad40 943 Ok(true) => usage = DiskUsageType::Mounted,
af6fdb9d 944 Ok(false) => {}
c26aad40
DM
945 Err(_) => continue, // skip devices with undetectable mount status
946 }
947
d406de29 948 if zfs_devices.contains(&devnum) {
c26aad40
DM
949 usage = DiskUsageType::ZFS;
950 }
951
af6fdb9d
TL
952 let vendor = disk
953 .vendor()
954 .unwrap_or(None)
955 .map(|s| s.to_string_lossy().trim().to_string());
c26aad40 956
91960d61 957 let model = disk.model().map(|s| s.to_string_lossy().into_owned());
c26aad40 958
91960d61 959 let serial = disk.serial().map(|s| s.to_string_lossy().into_owned());
c26aad40 960
af6fdb9d
TL
961 let devpath = disk
962 .device_path()
963 .map(|p| p.to_owned())
de1e1a9d 964 .map(|p| p.to_string_lossy().to_string());
c26aad40 965
91960d61 966 let wwn = disk.wwn().map(|s| s.to_string_lossy().into_owned());
c26aad40 967
6a6ba4cd
HL
968 let partitions: Option<Vec<PartitionInfo>> = if include_partitions {
969 disk.partitions().map_or(None, |parts| {
970 Some(get_partitions_info(
971 parts,
972 &lvm_devices,
973 &zfs_devices,
974 &file_system_devices,
975 ))
976 })
977 } else {
978 None
979 };
980
c26aad40
DM
981 if usage != DiskUsageType::Mounted {
982 match scan_partitions(disk_manager.clone(), &lvm_devices, &zfs_devices, &name) {
983 Ok(part_usage) => {
984 if part_usage != DiskUsageType::Unused {
985 usage = part_usage;
986 }
af6fdb9d 987 }
c26aad40
DM
988 Err(_) => continue, // skip devices if scan_partitions fail
989 };
990 }
991
20429238
FE
992 if usage == DiskUsageType::Unused && file_system_devices.contains(&devnum) {
993 usage = DiskUsageType::FileSystem;
994 }
995
be10cdb1
DC
996 if usage == DiskUsageType::Unused && disk.has_holders()? {
997 usage = DiskUsageType::DeviceMapper;
998 }
999
af6fdb9d 1000 let mut status = SmartStatus::Unknown;
91960d61
DM
1001 let mut wearout = None;
1002
1003 if !no_smart {
1004 if let Ok(smart) = get_smart_data(&disk, false) {
1005 status = smart.status;
1006 wearout = smart.wearout;
1007 }
1008 }
1009
c26aad40
DM
1010 let info = DiskUsageInfo {
1011 name: name.clone(),
af6fdb9d
TL
1012 vendor,
1013 model,
6a6ba4cd 1014 partitions,
af6fdb9d
TL
1015 serial,
1016 devpath,
1017 size,
1018 wwn,
1019 disk_type,
1020 status,
1021 wearout,
c26aad40 1022 used: usage,
91960d61
DM
1023 gpt: disk.has_gpt(),
1024 rpm: disk.ata_rotation_rate_rpm(),
c26aad40
DM
1025 };
1026
c26aad40
DM
1027 result.insert(name, info);
1028 }
1029
1030 Ok(result)
1031}
d2522b2d 1032
04405506
DM
1033/// Try to reload the partition table
1034pub fn reread_partition_table(disk: &Disk) -> Result<(), Error> {
04405506
DM
1035 let disk_path = match disk.device_path() {
1036 Some(path) => path,
1037 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
1038 };
1039
cbef49bf 1040 let mut command = std::process::Command::new("blockdev");
04405506
DM
1041 command.arg("--rereadpt");
1042 command.arg(disk_path);
1043
25877d05 1044 proxmox_sys::command::run_command(command, None)?;
04405506
DM
1045
1046 Ok(())
1047}
1048
707974fd
DM
1049/// Initialize disk by writing a GPT partition table
1050pub fn inititialize_gpt_disk(disk: &Disk, uuid: Option<&str>) -> Result<(), Error> {
707974fd
DM
1051 let disk_path = match disk.device_path() {
1052 Some(path) => path,
1053 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
1054 };
1055
1056 let uuid = uuid.unwrap_or("R"); // R .. random disk GUID
1057
cbef49bf 1058 let mut command = std::process::Command::new("sgdisk");
707974fd 1059 command.arg(disk_path);
16f6766a 1060 command.args(["-U", uuid]);
707974fd 1061
25877d05 1062 proxmox_sys::command::run_command(command, None)?;
707974fd
DM
1063
1064 Ok(())
1065}
1066
4b1c7e35
MF
1067/// Wipes all labels and the first 200 MiB of a disk/partition (or the whole if it is smaller).
1068/// If called with a partition, also sets the partition type to 0x83 'Linux filesystem'.
1069pub fn wipe_blockdev(disk: &Disk, worker: Arc<WorkerTask>) -> Result<(), Error> {
1070 let disk_path = match disk.device_path() {
1071 Some(path) => path,
1072 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
1073 };
1074 let disk_path_str = match disk_path.to_str() {
1075 Some(path) => path,
1076 None => bail!("disk {:?} could not transform into a str", disk.syspath()),
1077 };
1078
1079 let mut is_partition = false;
1080 for disk_info in get_lsblk_info()?.iter() {
1081 if disk_info.path == disk_path_str && disk_info.partition_type.is_some() {
1082 is_partition = true;
1083 }
1084 }
1085
1086 let mut to_wipe: Vec<PathBuf> = Vec::new();
1087
1088 let partitions_map = disk.partitions()?;
1089 for part_disk in partitions_map.values() {
1090 let part_path = match part_disk.device_path() {
1091 Some(path) => path,
1092 None => bail!("disk {:?} has no node in /dev", part_disk.syspath()),
1093 };
1094 to_wipe.push(part_path.to_path_buf());
1095 }
1096
1097 to_wipe.push(disk_path.to_path_buf());
1098
1099 task_log!(worker, "Wiping block device {}", disk_path.display());
1100
1101 let mut wipefs_command = std::process::Command::new("wipefs");
1102 wipefs_command.arg("--all").args(&to_wipe);
1103
1104 let wipefs_output = proxmox_sys::command::run_command(wipefs_command, None)?;
1105 task_log!(worker, "wipefs output: {}", wipefs_output);
1106
1107 let size = disk.size().map(|size| size / 1024 / 1024)?;
1108 let count = size.min(200);
1109
1110 let mut dd_command = std::process::Command::new("dd");
1111 let mut of_path = OsString::from("of=");
1112 of_path.push(disk_path);
1113 let mut count_str = OsString::from("count=");
1114 count_str.push(count.to_string());
1115 let args = [
1116 "if=/dev/zero".into(),
1117 of_path,
1118 "bs=1M".into(),
1119 "conv=fdatasync".into(),
1120 count_str.into(),
1121 ];
1122 dd_command.args(args);
1123
1124 let dd_output = proxmox_sys::command::run_command(dd_command, None)?;
1125 task_log!(worker, "dd output: {}", dd_output);
1126
1127 if is_partition {
1128 // set the partition type to 0x83 'Linux filesystem'
1129 change_parttype(&disk, "8300", worker)?;
1130 }
1131
1132 Ok(())
1133}
1134
1135pub fn change_parttype(
1136 part_disk: &Disk,
1137 part_type: &str,
1138 worker: Arc<WorkerTask>,
1139) -> Result<(), Error> {
1140 let part_path = match part_disk.device_path() {
1141 Some(path) => path,
1142 None => bail!("disk {:?} has no node in /dev", part_disk.syspath()),
1143 };
1144 if let Ok(stat) = nix::sys::stat::stat(part_path) {
1145 let mut sgdisk_command = std::process::Command::new("sgdisk");
1146 let major = unsafe { libc::major(stat.st_rdev) };
1147 let minor = unsafe { libc::minor(stat.st_rdev) };
1148 let partnum_path = &format!("/sys/dev/block/{}:{}/partition", major, minor);
1149 let partnum: u32 = std::fs::read_to_string(partnum_path)?.trim_end().parse()?;
1150 sgdisk_command.arg(&format!("-t{}:{}", partnum, part_type));
1151 let part_disk_parent = match part_disk.parent() {
1152 Some(disk) => disk,
1153 None => bail!("disk {:?} has no node in /dev", part_disk.syspath()),
1154 };
1155 let part_disk_parent_path = match part_disk_parent.device_path() {
1156 Some(path) => path,
1157 None => bail!("disk {:?} has no node in /dev", part_disk.syspath()),
1158 };
1159 sgdisk_command.arg(part_disk_parent_path);
1160 let sgdisk_output = proxmox_sys::command::run_command(sgdisk_command, None)?;
1161 task_log!(worker, "sgdisk output: {}", sgdisk_output);
1162 }
1163 Ok(())
1164}
1165
9bb161c8
DM
1166/// Create a single linux partition using the whole available space
1167pub fn create_single_linux_partition(disk: &Disk) -> Result<Disk, Error> {
9bb161c8
DM
1168 let disk_path = match disk.device_path() {
1169 Some(path) => path,
1170 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
1171 };
1172
cbef49bf 1173 let mut command = std::process::Command::new("sgdisk");
16f6766a 1174 command.args(["-n1", "-t1:8300"]);
9bb161c8
DM
1175 command.arg(disk_path);
1176
25877d05 1177 proxmox_sys::command::run_command(command, None)?;
9bb161c8
DM
1178
1179 let mut partitions = disk.partitions()?;
1180
1181 match partitions.remove(&1) {
1182 Some(partition) => Ok(partition),
1183 None => bail!("unable to lookup device partition"),
1184 }
1185}
1186
1187#[api()]
e1ea9135 1188#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq)]
af6fdb9d 1189#[serde(rename_all = "lowercase")]
9bb161c8
DM
1190pub enum FileSystemType {
1191 /// Linux Ext4
1192 Ext4,
1193 /// XFS
1194 Xfs,
1195}
1196
144006fa
DM
1197impl std::fmt::Display for FileSystemType {
1198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1199 let text = match self {
1200 FileSystemType::Ext4 => "ext4",
1201 FileSystemType::Xfs => "xfs",
1202 };
1203 write!(f, "{}", text)
1204 }
1205}
1206
d4f2397d
DM
1207impl std::str::FromStr for FileSystemType {
1208 type Err = serde_json::Error;
1209
1210 fn from_str(s: &str) -> Result<Self, Self::Err> {
1211 use serde::de::IntoDeserializer;
1212 Self::deserialize(s.into_deserializer())
1213 }
1214}
1215
9bb161c8
DM
1216/// Create a file system on a disk or disk partition
1217pub fn create_file_system(disk: &Disk, fs_type: FileSystemType) -> Result<(), Error> {
9bb161c8
DM
1218 let disk_path = match disk.device_path() {
1219 Some(path) => path,
1220 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
1221 };
1222
144006fa 1223 let fs_type = fs_type.to_string();
9bb161c8 1224
cbef49bf 1225 let mut command = std::process::Command::new("mkfs");
16f6766a 1226 command.args(["-t", &fs_type]);
9bb161c8
DM
1227 command.arg(disk_path);
1228
25877d05 1229 proxmox_sys::command::run_command(command, None)?;
9bb161c8
DM
1230
1231 Ok(())
1232}
707974fd 1233/// Block device name completion helper
d2522b2d 1234pub fn complete_disk_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
af6fdb9d
TL
1235 let dir =
1236 match proxmox_sys::fs::scan_subdir(libc::AT_FDCWD, "/sys/block", &BLOCKDEVICE_NAME_REGEX) {
1237 Ok(dir) => dir,
1238 Err(_) => return vec![],
1239 };
d2522b2d 1240
af6fdb9d
TL
1241 dir.flatten()
1242 .map(|item| item.file_name().to_str().unwrap().to_string())
1243 .collect()
d2522b2d 1244}
ed7b3a7d 1245
4b1c7e35
MF
1246/// Block device partition name completion helper
1247pub fn complete_partition_name(_arg: &str, _param: &HashMap<String, String>) -> Vec<String> {
1248 let dir = match proxmox_sys::fs::scan_subdir(
1249 libc::AT_FDCWD,
1250 "/sys/class/block",
1251 &BLOCKDEVICE_DISK_AND_PARTITION_NAME_REGEX,
1252 ) {
1253 Ok(dir) => dir,
1254 Err(_) => return vec![],
1255 };
1256
1257 dir.flatten()
1258 .map(|item| item.file_name().to_str().unwrap().to_string())
1259 .collect()
1260}
1261
ed7b3a7d
DM
1262/// Read the FS UUID (parse blkid output)
1263///
1264/// Note: Calling blkid is more reliable than using the udev ID_FS_UUID property.
1265pub fn get_fs_uuid(disk: &Disk) -> Result<String, Error> {
ed7b3a7d
DM
1266 let disk_path = match disk.device_path() {
1267 Some(path) => path,
1268 None => bail!("disk {:?} has no node in /dev", disk.syspath()),
1269 };
1270
cbef49bf 1271 let mut command = std::process::Command::new("blkid");
16f6766a 1272 command.args(["-o", "export"]);
ed7b3a7d
DM
1273 command.arg(disk_path);
1274
25877d05 1275 let output = proxmox_sys::command::run_command(command, None)?;
ed7b3a7d
DM
1276
1277 for line in output.lines() {
365915da
FG
1278 if let Some(uuid) = line.strip_prefix("UUID=") {
1279 return Ok(uuid.to_string());
ed7b3a7d
DM
1280 }
1281 }
1282
1283 bail!("get_fs_uuid failed - missing UUID");
1284}