Skip to content

Commit fc283fe

Browse files
authored
Add script for ZFS snapshots (#136)
Signed-off-by: Lars Strojny <[email protected]>
1 parent 1fbdeca commit fc283fe

File tree

3 files changed

+111
-0
lines changed

3 files changed

+111
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
zpool/vol/0@snapshot-name 0 1685082363
2+
zpool/vol/0@snapshot-name 128 1685085436
3+
zpool/vol1@snapshot-name 0 1685099827
4+
zpool/vol1@snapshot-name 256 1685100606

mock/zfs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
3+
# zfs mock script for testing zfs snapshot provider
4+
5+
fixtures_dir=$(dirname "$0")/fixtures
6+
cat "${fixtures_dir}/zfs_list_-p_-H_-t_snapshot_-o_name,used,creation"

zfs-snapshots.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import subprocess
4+
from functools import reduce, partial
5+
from itertools import groupby
6+
from operator import itemgetter, add
7+
from prometheus_client import CollectorRegistry, Gauge, generate_latest
8+
9+
10+
def row_to_metric(metric: Gauge, row):
11+
return metric.labels(pool=row[0][0], volume=row[0][1]).set(row[1])
12+
13+
14+
def collect_metrics(metric: Gauge, it) -> None:
15+
list(map(partial(row_to_metric, metric), it))
16+
17+
18+
def zfs_parse_line(line):
19+
cols = line.split("\t")
20+
rest, snapshot = cols[0].rsplit("@", 1)
21+
pool = rest
22+
volume = None
23+
if "/" in rest:
24+
pool, volume = rest.split("/", 1)
25+
volume = "/" + volume
26+
return pool, volume, snapshot, *map(int, cols[1:])
27+
28+
29+
def zfs_list_snapshots():
30+
cmd = [
31+
"zfs",
32+
"list",
33+
"-p",
34+
"-H",
35+
"-t",
36+
"snapshot",
37+
"-o",
38+
"name,used,creation",
39+
]
40+
# zfs list can be relatively slow (couple of seconds)
41+
# Use Popen to incrementally read from stdout to not waste further time
42+
popen = subprocess.Popen(
43+
cmd, stdout=subprocess.PIPE, env=dict(os.environ, LC_ALL="C")
44+
)
45+
for stdout_line in iter(popen.stdout.readline, ""):
46+
stdout_line = stdout_line.strip()
47+
if stdout_line == b"":
48+
break
49+
yield stdout_line.decode("utf-8")
50+
return_code = popen.wait()
51+
if return_code:
52+
raise subprocess.CalledProcessError(return_code, cmd)
53+
54+
55+
def aggregate_rows(rows, index, operator):
56+
return map(
57+
lambda row: (row[0], reduce(operator, map(itemgetter(index), row[1]), 0)), rows
58+
)
59+
60+
61+
NAMESPACE = "zfs_snapshot"
62+
LABEL_NAMES = ["pool", "volume"]
63+
64+
65+
def main():
66+
registry = CollectorRegistry()
67+
latest_time_metric = Gauge(
68+
"latest_time",
69+
"Timestamp of the latest snapshot",
70+
labelnames=LABEL_NAMES,
71+
namespace=NAMESPACE,
72+
registry=registry,
73+
unit="seconds",
74+
)
75+
space_used_metric = Gauge(
76+
"space_used",
77+
"Space used by snapshots in bytes",
78+
labelnames=LABEL_NAMES,
79+
namespace=NAMESPACE,
80+
registry=registry,
81+
unit="bytes",
82+
)
83+
84+
snapshots = map(zfs_parse_line, zfs_list_snapshots())
85+
per_fs = list(
86+
map(
87+
lambda row: (row[0], list(row[1])), groupby(snapshots, lambda row: row[0:2])
88+
)
89+
)
90+
91+
space_used = aggregate_rows(per_fs, -2, add)
92+
latest_time = aggregate_rows(per_fs, -1, max)
93+
94+
collect_metrics(latest_time_metric, latest_time)
95+
collect_metrics(space_used_metric, space_used)
96+
97+
print(generate_latest(registry).decode(), end="")
98+
99+
100+
if __name__ == "__main__":
101+
main()

0 commit comments

Comments
 (0)