1 use std
::path
::PathBuf
;
2 use std
::collections
::{HashMap, HashSet}
;
3 use std
::os
::unix
::fs
::MetadataExt
;
5 use anyhow
::{bail, Error}
;
6 use lazy_static
::lazy_static
;
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}
,
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
28 type IResult
<I
, O
, E
= VerboseError
<I
>> = Result
<(I
, O
), nom
::Err
<E
>>;
31 pub struct ZFSPoolUsage
{
40 pub struct ZFSPoolStatus
{
43 pub usage
: Option
<ZFSPoolUsage
>,
44 pub devices
: Vec
<String
>,
47 /// Returns kernel IO-stats for zfs pools
48 pub fn zfs_pool_stats(pool
: &OsStr
) -> Result
<Option
<BlockDevStat
>, Error
> {
50 let mut path
= PathBuf
::from("/proc/spl/kstat/zfs");
54 let text
= match proxmox
::tools
::fs
::file_read_optional_string(&path
)?
{
56 None
=> { return Ok(None); }
59 let lines
: Vec
<&str> = text
.lines().collect();
62 bail
!("unable to parse {:?} - got less than 3 lines", path
);
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)
74 let ticks
= (stat
[4] + stat
[7])/1_000_000; // convert to milisec
76 let stat
= BlockDevStat
{
77 read_sectors
: stat
[0]>>9,
78 write_sectors
: stat
[1]>>9,
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
)
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
)
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
)
102 fn parse_optional_u64(i
: &str) -> IResult
<&str, Option
<u64>> {
103 if i
.starts_with('
-'
) {
106 let (i
, value
) = map_res(recognize(digit1
), str::parse
)(i
)?
;
111 fn parse_optional_f64(i
: &str) -> IResult
<&str, Option
<f64>> {
112 if i
.starts_with('
-'
) {
115 let (i
, value
) = nom
::number
::complete
::double(i
)?
;
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'
)),
124 preceded(take_till(|c
| c
== '
\n'
), char('
\n'
)),
127 Ok((i
, device
.to_string()))
130 fn parse_pool_header(i
: &str) -> IResult
<&str, ZFSPoolStatus
> {
131 // name, size, allocated, free, checkpoint, expandsize, fragmentation, capacity, dedupratio, health, altroot.
133 let (i
, (text
, size
, alloc
, free
, _
, _
,
134 frag
, _
, dedup
, health
,
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
150 let status
= if let (Some(size
), Some(alloc
), Some(free
), Some(frag
), Some(dedup
)) = (size
, alloc
, free
, frag
, dedup
) {
153 health
: health
.into(),
154 usage
: Some(ZFSPoolUsage { size, alloc, free, frag, dedup }
),
160 health
: health
.into(),
169 fn parse_pool_status(i
: &str) -> IResult
<&str, ZFSPoolStatus
> {
171 let (i
, mut stat
) = parse_pool_header(i
)?
;
172 let (i
, devices
) = many0(parse_pool_device
)(i
)?
;
174 for device_path
in devices
.into_iter().filter(|n
| n
.starts_with("/dev/")) {
175 stat
.devices
.push(device_path
);
178 let (i
, _
) = many0(tuple((multispace0
, char('
\n'
))))(i
)?
; // skip empty lines
183 /// Parse zpool list outout
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
> {
189 return Ok(Vec
::new());
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
));
197 bail
!("unable to parse calendar event: {}", err
);
199 Ok((_
, ce
)) => Ok(ce
),
203 /// Get set of devices used by zfs (or a specific zfs pool)
205 /// The set is indexed by using the unix raw device number (dev_t is u64)
207 partition_type_map
: &HashMap
<String
, Vec
<String
>>,
208 pool
: Option
<&OsStr
>,
209 ) -> Result
<HashSet
<u64>, Error
> {
211 // Note: zpools list output can include entries for 'special', 'cache' and 'logs'
212 // and maybe other things.
214 let mut command
= std
::process
::Command
::new("/sbin/zpool");
215 command
.args(&["list", "-H", "-v", "-p", "-P"]);
217 if let Some(pool
) = pool { command.arg(pool); }
219 let output
= crate::tools
::run_command(command
, None
)?
;
221 let list
= parse_zfs_list(&output
)?
;
223 let mut device_set
= HashSet
::new();
225 for device
in entry
.devices
{
226 let meta
= std
::fs
::metadata(device
)?
;
227 device_set
.insert(meta
.rdev());
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 }
)
234 for device
in device_list
{
235 let meta
= std
::fs
::metadata(device
)?
;
236 device_set
.insert(meta
.rdev());