]> git.proxmox.com Git - proxmox-backup.git/blob - src/tools/disks/zfs.rs
src/tools/disks/zfs.rs: cleanup (rename usage properties)
[proxmox-backup.git] / src / tools / disks / zfs.rs
1 use std::path::PathBuf;
2 use std::collections::{HashMap, HashSet};
3 use std::os::unix::fs::MetadataExt;
4
5 use anyhow::{bail, Error};
6 use lazy_static::lazy_static;
7
8 use nom::{
9 error::VerboseError,
10 bytes::complete::{take_while, take_while1, take_till, take_till1},
11 combinator::{map_res, all_consuming, recognize, opt},
12 sequence::{preceded, tuple},
13 character::complete::{space1, digit1, char, line_ending},
14 multi::{many0, many1},
15 };
16
17 use super::*;
18
19 lazy_static!{
20 static ref ZFS_UUIDS: HashSet<&'static str> = {
21 let mut set = HashSet::new();
22 set.insert("6a898cc3-1dd2-11b2-99a6-080020736631"); // apple
23 set.insert("516e7cba-6ecf-11d6-8ff8-00022d09712b"); // bsd
24 set
25 };
26 }
27
28 type IResult<I, O, E = VerboseError<I>> = Result<(I, O), nom::Err<E>>;
29
30 #[derive(Debug)]
31 pub struct ZFSPoolUsage {
32 pub size: u64,
33 pub alloc: u64,
34 pub free: u64,
35 pub dedup: f64,
36 pub frag: u64,
37 }
38
39 #[derive(Debug)]
40 pub struct ZFSPoolStatus {
41 pub name: String,
42 pub health: String,
43 pub usage: Option<ZFSPoolUsage>,
44 pub devices: Vec<String>,
45 }
46
47 /// Returns kernel IO-stats for zfs pools
48 pub fn zfs_pool_stats(pool: &OsStr) -> Result<Option<BlockDevStat>, Error> {
49
50 let mut path = PathBuf::from("/proc/spl/kstat/zfs");
51 path.push(pool);
52 path.push("io");
53
54 let text = match proxmox::tools::fs::file_read_optional_string(&path)? {
55 Some(text) => text,
56 None => { return Ok(None); }
57 };
58
59 let lines: Vec<&str> = text.lines().collect();
60
61 if lines.len() < 3 {
62 bail!("unable to parse {:?} - got less than 3 lines", path);
63 }
64
65 // https://github.com/openzfs/zfs/blob/master/lib/libspl/include/sys/kstat.h#L578
66 // nread nwritten reads writes wtime wlentime wupdate rtime rlentime rupdate wcnt rcnt
67 // Note: w -> wait (wtime -> wait time)
68 // Note: r -> run (rtime -> run time)
69 // All times are nanoseconds
70 let stat: Vec<u64> = lines[2].split_ascii_whitespace().map(|s| {
71 u64::from_str_radix(s, 10).unwrap_or(0)
72 }).collect();
73
74 let ticks = (stat[4] + stat[7])/1_000_000; // convert to milisec
75
76 let stat = BlockDevStat {
77 read_sectors: stat[0]>>9,
78 write_sectors: stat[1]>>9,
79 read_ios: stat[2],
80 write_ios: stat[3],
81 io_ticks: ticks,
82 };
83
84 Ok(Some(stat))
85 }
86
87 /// Recognizes zero or more spaces and tabs (but not carage returns or line feeds)
88 fn multispace0(i: &str) -> IResult<&str, &str> {
89 take_while(|c| c == ' ' || c == '\t')(i)
90 }
91
92 /// Recognizes one or more spaces and tabs (but not carage returns or line feeds)
93 fn multispace1(i: &str) -> IResult<&str, &str> {
94 take_while1(|c| c == ' ' || c == '\t')(i)
95 }
96
97 /// Recognizes one or more non-whitespace-characters
98 fn notspace1(i: &str) -> IResult<&str, &str> {
99 take_while1(|c| !(c == ' ' || c == '\t' || c == '\n'))(i)
100 }
101
102 fn parse_optional_u64(i: &str) -> IResult<&str, Option<u64>> {
103 if i.starts_with('-') {
104 Ok((&i[1..], None))
105 } else {
106 let (i, value) = map_res(recognize(digit1), str::parse)(i)?;
107 Ok((i, Some(value)))
108 }
109 }
110
111 fn parse_optional_f64(i: &str) -> IResult<&str, Option<f64>> {
112 if i.starts_with('-') {
113 Ok((&i[1..], None))
114 } else {
115 let (i, value) = nom::number::complete::double(i)?;
116 Ok((i, Some(value)))
117 }
118 }
119
120 fn parse_pool_device(i: &str) -> IResult<&str, String> {
121 let (i, (device, _, _rest)) = tuple((
122 preceded(multispace1, take_till1(|c| c == ' ' || c == '\t')),
123 multispace1,
124 preceded(take_till(|c| c == '\n'), char('\n')),
125 ))(i)?;
126
127 Ok((i, device.to_string()))
128 }
129
130 fn parse_pool_header(i: &str) -> IResult<&str, ZFSPoolStatus> {
131 // name, size, allocated, free, checkpoint, expandsize, fragmentation, capacity, dedupratio, health, altroot.
132
133 let (i, (text, size, alloc, free, _, _,
134 frag, _, dedup, health,
135 _, _eol)) = tuple((
136 take_while1(|c| char::is_alphanumeric(c)), // name
137 preceded(multispace1, parse_optional_u64), // size
138 preceded(multispace1, parse_optional_u64), // allocated
139 preceded(multispace1, parse_optional_u64), // free
140 preceded(multispace1, notspace1), // checkpoint
141 preceded(multispace1, notspace1), // expandsize
142 preceded(multispace1, parse_optional_u64), // fragmentation
143 preceded(multispace1, notspace1), // capacity
144 preceded(multispace1, parse_optional_f64), // dedup
145 preceded(multispace1, notspace1), // health
146 opt(preceded(space1, take_till(|c| c == '\n'))), // skip rest
147 line_ending,
148 ))(i)?;
149
150 let status = if let (Some(size), Some(alloc), Some(free), Some(frag), Some(dedup)) = (size, alloc, free, frag, dedup) {
151 ZFSPoolStatus {
152 name: text.into(),
153 health: health.into(),
154 usage: Some(ZFSPoolUsage { size, alloc, free, frag, dedup }),
155 devices: Vec::new(),
156 }
157 } else {
158 ZFSPoolStatus {
159 name: text.into(),
160 health: health.into(),
161 usage: None,
162 devices: Vec::new(),
163 }
164 };
165
166 Ok((i, status))
167 }
168
169 fn parse_pool_status(i: &str) -> IResult<&str, ZFSPoolStatus> {
170
171 let (i, mut stat) = parse_pool_header(i)?;
172 let (i, devices) = many0(parse_pool_device)(i)?;
173
174 for device_path in devices.into_iter().filter(|n| n.starts_with("/dev/")) {
175 stat.devices.push(device_path);
176 }
177
178 let (i, _) = many0(tuple((multispace0, char('\n'))))(i)?; // skip empty lines
179
180 Ok((i, stat))
181 }
182
183 /// Parse zpool list outout
184 ///
185 /// Note: This does not reveal any details on how the pool uses the devices, because
186 /// the zpool list output format is not really defined...
187 pub fn parse_zfs_list(i: &str) -> Result<Vec<ZFSPoolStatus>, Error> {
188 if i.is_empty() {
189 return Ok(Vec::new());
190 }
191 match all_consuming(many1(parse_pool_status))(i) {
192 Err(nom::Err::Error(err)) |
193 Err(nom::Err::Failure(err)) => {
194 bail!("unable to parse zfs list output - {}", nom::error::convert_error(i, err));
195 }
196 Err(err) => {
197 bail!("unable to parse calendar event: {}", err);
198 }
199 Ok((_, ce)) => Ok(ce),
200 }
201 }
202
203 /// Get set of devices used by zfs (or a specific zfs pool)
204 ///
205 /// The set is indexed by using the unix raw device number (dev_t is u64)
206 pub fn zfs_devices(
207 partition_type_map: &HashMap<String, Vec<String>>,
208 pool: Option<&OsStr>,
209 ) -> Result<HashSet<u64>, Error> {
210
211 // Note: zpools list output can include entries for 'special', 'cache' and 'logs'
212 // and maybe other things.
213
214 let mut command = std::process::Command::new("/sbin/zpool");
215 command.args(&["list", "-H", "-v", "-p", "-P"]);
216
217 if let Some(pool) = pool { command.arg(pool); }
218
219 let output = crate::tools::run_command(command, None)?;
220
221 let list = parse_zfs_list(&output)?;
222
223 let mut device_set = HashSet::new();
224 for entry in list {
225 for device in entry.devices {
226 let meta = std::fs::metadata(device)?;
227 device_set.insert(meta.rdev());
228 }
229 }
230
231 for device_list in partition_type_map.iter()
232 .filter_map(|(uuid, list)| if ZFS_UUIDS.contains(uuid.as_str()) { Some(list) } else { None })
233 {
234 for device in device_list {
235 let meta = std::fs::metadata(device)?;
236 device_set.insert(meta.rdev());
237 }
238 }
239
240 Ok(device_set)
241 }