1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
use std::fs::{self, File};
use std::io;
use std::fs::OpenOptions;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::io::{BufRead, BufReader};
use std::collections::HashSet;
/// See [parent module docs](../index.html)
#[derive(Debug)]
pub struct Device {
pub(crate) file: File,
}
#[derive(Debug)]
pub enum Type { SCSI }
impl Device {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, io::Error> {
Ok(Device {
file: OpenOptions::new()
.read(true)
// > Under Linux, the O_NONBLOCK flag indicates that one wants to open but does not necessarily have the intention to read or write.
// > This is typically used to open devices in order to get a file descriptor for use with ioctl(2).
// ~ open(2)
// this fixes access to optical drives and other ejectable media
// (https://github.com/vthriller/hdd-rs/issues/1)
.custom_flags(libc::O_NONBLOCK)
.open(path)?,
})
}
pub fn get_type(&self) -> Result<Type, io::Error> { Ok(Type::SCSI) }
}
/// Lists paths to devices currently presented in the system.
pub fn list_devices() -> Result<Vec<PathBuf>, io::Error> {
/*
Various software enumerates block devices in a variety of ways:
- smartd: probes for /dev/hd[a-t], /dev/sd[a-z], /dev/sd[a-c][a-z], /dev/nvme[0-99]
- lsscsi: looks for *:* in /sys/bus/scsi/devices/, skipping {host,target}*
- sg3_utils/sg_scan: iterates over /sys/class/scsi_generic if exists, otherwise probing for /dev/sg{0..8191} or /dev/sg{a..z,aa..zz,...}
- util-linux/lsblk: iterates over /sys/block, skipping devices with major number 1 (RAM disks) by default (see --include/--exclude), as well as devices with no known size or the size of 0 (see /sys/class/block/<X>/size)
- udisks: queries udev for devices in a "block" subsystem
- gnome-disk-utility: just asks udisks
- udev: just reads a bunch of files from /sys, appending irrelevant (in our case) data from hwdb and attributes set via various rules
This code was once written using libudev, but it was dropped for a number of reason:
- it's an extra dependency
- it is much harder to make static builds for x86_64-unknown-linux-musl
- it might not work on exotic systems that run mdev or rely solely on devtmpfs
- data provided by libudev can be easily read from /sys
- the data that libudev does not provide (e.g. `device/generic` symlink target for SCSI block devices), well, needs to be read from /sys anyways, so in a long run it's not, like, super-convenient to use this library
*/
let mut devices = vec![];
let mut skip_generics = HashSet::new();
// XXX do not return Err() if /sys/class/block does not exist but /sys/class/scsi_generic does, or vice versa
// N.B. log entries are indented relative to each other
info!("inspecting /sys/class/block");
for d in fs::read_dir("/sys/class/block")? {
let d = if let Ok(d) = d { d } else { continue };
// XXX this assumes that dir name equals to whatever `DEVNAME` is set to in the uevent file
// (and that `DEVNAME` is even present there)
let name = d.file_name();
let path = if let Ok(path) = d.path().canonicalize() { path } else {
debug!(" {:?}: unable to read canonical device path, skipping", name);
continue
};
debug!(" {:?} → {:?}", name, path);
// skip devices like /dev/{loop,ram,zram,md,fd}*
if path.starts_with("/sys/devices/virtual/") {
debug!(" virtual device, skipping");
continue;
}
// Path.starts_with only works with whole path components so it can't match …/floppy.0
// hence .to_str()
if path.as_path().to_str().unwrap().starts_with("/sys/devices/platform/floppy") {
debug!(" floppy device, skipping");
continue;
}
if name.to_str().unwrap().starts_with('v') {
// probably /dev/vdX, check whether it is a virtio-blk device
// N.B. we do NOT skip virtio_scsi devices due to LUN passthrough
if let Ok(driver) = path.join("device/driver").read_link() {
if driver.file_name() == Some(::std::ffi::OsStr::new("virtio_blk")) {
debug!(" virtio_blk device, skipping");
continue;
}
}
// there are other ways to identify virtio devices;
// one of them relies on PCI vendor id (`device/vendor` should read `0x1af4`, Red Hat, Inc.)
// and device id (`../../../device` → 0x1001)
}
// $ grep -q '^DEVTYPE=disk$' /sys/class/block/sda/uevent
if let Ok(uevent) = File::open(path.join("uevent")) {
let mut is_disk = false;
let mut buf = BufReader::new(uevent);
for line in buf.lines() {
match line {
Ok(ref s) if s.as_str() == "DEVTYPE=disk" => {
debug!(" {}", s);
is_disk = true;
break;
}
Ok(ref s) if s.starts_with("DEVTYPE=") => {
debug!(" {}", s);
is_disk = false; // see first match arm
break;
}
Ok(_) => (), // keep reading
Err(e) => {
debug!(" problem reading uevent file: {}", e);
break;
},
}
}
if ! is_disk {
debug!(" undisclosed block device type, or device is not a disk, skipping");
continue
}
} else {
debug!(" unable to determine device type, skipping");
continue;
}
devices.push(name);
// e.g. `readlink /sys/class/block/sda/device/generic` → `scsi_generic/sg0`
if let Ok(generic_path) = path.join("device/generic").read_link() {
if let Some(generic_name) = generic_path.file_name() {
debug!(" found corresponding scsi_generic device {:?}", generic_name);
skip_generics.insert(generic_name.to_os_string());
}
}
}
/*
Some drivers (e.g. aacraid) also provide generic SCSI devices for disks behind hardware RAIDs;
these devices can be used to query SMART or SCSI logs from disks that are not represented with corresponding block devices
*/
info!("inspecting /sys/class/scsi_generic");
for d in fs::read_dir("/sys/class/scsi_generic")? {
let d = if let Ok(d) = d { d } else { continue };
let name = d.file_name();
debug!(" {:?}", name);
if ! skip_generics.contains(&name) {
devices.push(name);
} else {
debug!(" already covered by corresponding block device, skipping");
}
}
Ok(devices
.into_iter()
.map(|dev| PathBuf::from(
format!("/dev/{}", dev.into_string().unwrap())
))
.collect()
)
}