]>
Commit | Line | Data |
---|---|---|
10effc98 WB |
1 | //! Disk query/management utilities for. |
2 | ||
5c264c8d | 3 | use std::collections::{HashMap, HashSet}; |
10effc98 WB |
4 | use std::ffi::{OsStr, OsString}; |
5 | use std::io; | |
6 | use std::os::unix::ffi::{OsStrExt, OsStringExt}; | |
d406de29 | 7 | use std::os::unix::fs::MetadataExt; |
10effc98 WB |
8 | use std::path::{Path, PathBuf}; |
9 | use std::sync::Arc; | |
10 | ||
707974fd | 11 | use anyhow::{bail, format_err, Error}; |
10effc98 WB |
12 | use libc::dev_t; |
13 | use once_cell::sync::OnceCell; | |
14 | ||
de1e1a9d DM |
15 | use ::serde::{Deserialize, Serialize}; |
16 | ||
f26d7ca5 | 17 | use proxmox_lang::error::io_err_other; |
f26d7ca5 | 18 | use proxmox_lang::{io_bail, io_format_err}; |
4b1c7e35 | 19 | use proxmox_rest_server::WorkerTask; |
9531d2c5 TL |
20 | use proxmox_schema::api; |
21 | use proxmox_sys::linux::procfs::{mountinfo::Device, MountInfo}; | |
4b1c7e35 | 22 | use proxmox_sys::task_log; |
10effc98 | 23 | |
4b1c7e35 | 24 | use pbs_api_types::{BLOCKDEVICE_DISK_AND_PARTITION_NAME_REGEX, BLOCKDEVICE_NAME_REGEX}; |
9069debc | 25 | |
5c264c8d DM |
26 | mod zfs; |
27 | pub use zfs::*; | |
0727e56a DM |
28 | mod zpool_status; |
29 | pub use zpool_status::*; | |
0686b1f4 DM |
30 | mod zpool_list; |
31 | pub use zpool_list::*; | |
620911b4 DM |
32 | mod lvm; |
33 | pub use lvm::*; | |
eb80aac2 DM |
34 | mod smart; |
35 | pub use smart::*; | |
0146133b | 36 | |
af6fdb9d | 37 | lazy_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. | |
46 | pub 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)] | |
53 | pub 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 |
64 | impl 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. | |
194 | pub 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)] | |
204 | struct 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 | ||
228 | impl 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. |
543 | pub 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 |
559 | pub 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 | 568 | pub 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 | 582 | fn 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")] |
598 | pub 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 | 618 | pub 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 |
639 | pub 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 | 678 | pub 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 | ||
706 | fn 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 |
774 | pub struct DiskUsageQuery { |
775 | smart: bool, | |
776 | partitions: bool, | |
777 | } | |
778 | ||
779 | impl 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 |
815 | fn 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 | 874 | fn 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 |
1034 | pub 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 |
1050 | pub 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'. | |
1069 | pub 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 | ||
1135 | pub 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 |
1167 | pub 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 |
1190 | pub enum FileSystemType { |
1191 | /// Linux Ext4 | |
1192 | Ext4, | |
1193 | /// XFS | |
1194 | Xfs, | |
1195 | } | |
1196 | ||
144006fa DM |
1197 | impl 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 |
1207 | impl 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 |
1217 | pub 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 | 1234 | pub 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 |
1247 | pub 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. | |
1265 | pub 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 | } |