]> git.ipfire.org Git - collecty.git/blame - src/collecty/plugins/disk.py
psi: Add graph template
[collecty.git] / src / collecty / plugins / disk.py
CommitLineData
f37913e8 1#!/usr/bin/python3
30777a6c
MT
2###############################################################################
3# #
4# collecty - A system statistics collection daemon for IPFire #
5# Copyright (C) 2012 IPFire development team #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
21
30777a6c
MT
22import os
23import re
24
a031204a 25from .. import _collecty
f37913e8 26from . import base
30777a6c 27
331fcf57 28from ..colours import *
60d1723e 29from ..constants import *
72fc5ca5 30from ..i18n import _
30777a6c
MT
31
32class GraphTemplateDiskBadSectors(base.GraphTemplate):
33 name = "disk-bad-sectors"
34
0240a325
MT
35 @property
36 def rrd_graph(self):
0240a325 37 return [
60d1723e
MT
38 "COMMENT:%s" % EMPTY_LABEL,
39 "COMMENT:%s" % (COLUMN % _("Current")),
40 "COMMENT:%s\\j" % (COLUMN % _("Maximum")),
41
42 "AREA:bad_sectors%s:%s" % (
43 transparency(COLOUR_CRITICAL, AREA_OPACITY),
44 LABEL % _("Bad Sectors"),
45 ),
46 "GPRINT:bad_sectors_cur:%s" % INTEGER,
47 "GPRINT:bad_sectors_max:%s\\j" % INTEGER,
48
49 # Contour line
50 "LINE:bad_sectors%s" % COLOUR_CRITICAL,
0240a325 51 ]
30777a6c
MT
52
53 @property
54 def graph_title(self):
55 return _("Bad Sectors of %s") % self.object.device_string
56
57 @property
58 def graph_vertical_label(self):
59 return _("Pending/Relocated Sectors")
60
61
62class GraphTemplateDiskBytes(base.GraphTemplate):
63 name = "disk-bytes"
64
0240a325
MT
65 @property
66 def rrd_graph(self):
0240a325 67 rrd_graph = [
0240a325
MT
68 "CDEF:read_bytes=read_sectors,512,*",
69 "CDEF:write_bytes=write_sectors,512,*",
70
03ba5630 71 "LINE1:read_bytes%s:%-15s" % (COLOUR_READ, _("Read")),
cd8bba0b
MT
72 "GPRINT:read_bytes_cur:%12s\:" % _("Current") + " %9.2lf",
73 "GPRINT:read_bytes_max:%12s\:" % _("Maximum") + " %9.2lf",
74 "GPRINT:read_bytes_min:%12s\:" % _("Minimum") + " %9.2lf",
8ec1299b 75 "GPRINT:read_bytes_avg:%12s\:" % _("Average") + " %9.2lf",
0240a325 76
03ba5630 77 "LINE1:write_bytes%s:%-15s" % (COLOUR_WRITE, _("Written")),
cd8bba0b
MT
78 "GPRINT:write_bytes_cur:%12s\:" % _("Current") + " %9.2lf",
79 "GPRINT:write_bytes_max:%12s\:" % _("Maximum") + " %9.2lf",
80 "GPRINT:write_bytes_min:%12s\:" % _("Minimum") + " %9.2lf",
8ec1299b 81 "GPRINT:write_bytes_avg:%12s\:" % _("Average") + " %9.2lf",
0240a325
MT
82 ]
83
84 return rrd_graph
30777a6c
MT
85
86 lower_limit = 0
87
88 @property
89 def graph_title(self):
90 return _("Disk Utilisation of %s") % self.object.device_string
91
92 @property
93 def graph_vertical_label(self):
94 return _("Byte per Second")
95
96
97class GraphTemplateDiskIoOps(base.GraphTemplate):
98 name = "disk-io-ops"
99
0240a325
MT
100 @property
101 def rrd_graph(self):
0240a325 102 rrd_graph = [
03ba5630 103 "LINE1:read_ios%s:%-15s" % (COLOUR_READ, _("Read")),
cd8bba0b
MT
104 "GPRINT:read_ios_cur:%12s\:" % _("Current") + " %6.2lf",
105 "GPRINT:read_ios_max:%12s\:" % _("Maximum") + " %6.2lf",
106 "GPRINT:read_ios_min:%12s\:" % _("Minimum") + " %6.2lf",
8ec1299b 107 "GPRINT:read_ios_avg:%12s\:" % _("Average") + " %6.2lf",
0240a325 108
03ba5630 109 "LINE1:write_ios%s:%-15s" % (COLOUR_WRITE, _("Written")),
cd8bba0b
MT
110 "GPRINT:write_ios_cur:%12s\:" % _("Current") + " %6.2lf",
111 "GPRINT:write_ios_max:%12s\:" % _("Maximum") + " %6.2lf",
112 "GPRINT:write_ios_min:%12s\:" % _("Minimum") + " %6.2lf",
8ec1299b 113 "GPRINT:write_ios_avg:%12s\:" % _("Average") + " %6.2lf",
0240a325
MT
114 ]
115
116 return rrd_graph
30777a6c
MT
117
118 lower_limit = 0
119
120 @property
121 def graph_title(self):
122 return _("Disk IO Operations of %s") % self.object.device_string
123
124 @property
125 def graph_vertical_label(self):
126 return _("Operations per Second")
127
128
129class GraphTemplateDiskTemperature(base.GraphTemplate):
130 name = "disk-temperature"
131
0240a325
MT
132 @property
133 def rrd_graph(self):
0240a325 134 rrd_graph = [
cd8bba0b 135 "CDEF:celsius=temperature,273.15,-",
0240a325
MT
136 "VDEF:temp_cur=celsius,LAST",
137 "VDEF:temp_min=celsius,MINIMUM",
138 "VDEF:temp_max=celsius,MAXIMUM",
139 "VDEF:temp_avg=celsius,AVERAGE",
cd8bba0b 140
03ba5630 141 "LINE2:celsius%s:%s" % (PRIMARY, _("Temperature")),
0240a325
MT
142 "GPRINT:temp_cur:%12s\:" % _("Current") + " %3.2lf",
143 "GPRINT:temp_max:%12s\:" % _("Maximum") + " %3.2lf",
144 "GPRINT:temp_min:%12s\:" % _("Minimum") + " %3.2lf",
8ec1299b 145 "GPRINT:temp_avg:%12s\:" % _("Average") + " %3.2lf",
0240a325
MT
146 ]
147
148 return rrd_graph
30777a6c
MT
149
150 @property
151 def graph_title(self):
152 return _("Disk Temperature of %s") % self.object.device_string
153
154 @property
155 def graph_vertical_label(self):
156 return _("° Celsius")
157
158 @property
159 def rrd_graph_args(self):
160 return [
161 # Make the y-axis have a decimal
162 "--left-axis-format", "%3.1lf",
163 ]
164
165
166class DiskObject(base.Object):
167 rrd_schema = [
168 "DS:awake:GAUGE:0:1",
169 "DS:read_ios:DERIVE:0:U",
170 "DS:read_sectors:DERIVE:0:U",
171 "DS:write_ios:DERIVE:0:U",
172 "DS:write_sectors:DERIVE:0:U",
173 "DS:bad_sectors:GAUGE:0:U",
174 "DS:temperature:GAUGE:U:U",
175 ]
176
177 def __repr__(self):
178 return "<%s %s (%s)>" % (self.__class__.__name__, self.sys_path, self.id)
179
180 def init(self, device):
181 self.dev_path = os.path.join("/dev", device)
182 self.sys_path = os.path.join("/sys/block", device)
183
184 self.device = _collecty.BlockDevice(self.dev_path)
185
186 @property
187 def id(self):
188 return "-".join((self.device.model, self.device.serial))
189
190 @property
191 def device_string(self):
192 return "%s (%s)" % (self.device.model, self.dev_path)
193
194 def collect(self):
195 stats = self.parse_stats()
196
f648421a 197 return (
30777a6c
MT
198 self.is_awake(),
199 stats.get("read_ios"),
200 stats.get("read_sectors"),
201 stats.get("write_ios"),
202 stats.get("write_sectors"),
203 self.get_bad_sectors(),
204 self.get_temperature(),
f648421a 205 )
30777a6c
MT
206
207 def parse_stats(self):
208 """
209 https://www.kernel.org/doc/Documentation/block/stat.txt
210
211 Name units description
212 ---- ----- -----------
213 read I/Os requests number of read I/Os processed
214 read merges requests number of read I/Os merged with in-queue I/O
215 read sectors sectors number of sectors read
216 read ticks milliseconds total wait time for read requests
217 write I/Os requests number of write I/Os processed
218 write merges requests number of write I/Os merged with in-queue I/O
219 write sectors sectors number of sectors written
220 write ticks milliseconds total wait time for write requests
221 in_flight requests number of I/Os currently in flight
222 io_ticks milliseconds total time this block device has been active
223 time_in_queue milliseconds total wait time for all requests
224 """
225 stats_file = os.path.join(self.sys_path, "stat")
226
227 with open(stats_file) as f:
228 stats = f.read().split()
229
230 return {
231 "read_ios" : stats[0],
232 "read_merges" : stats[1],
233 "read_sectors" : stats[2],
234 "read_ticks" : stats[3],
235 "write_ios" : stats[4],
236 "write_merges" : stats[5],
237 "write_sectors" : stats[6],
238 "write_ticks" : stats[7],
239 "in_flight" : stats[8],
240 "io_ticks" : stats[9],
241 "time_in_queue" : stats[10],
242 }
243
244 def is_smart_supported(self):
245 """
246 We can only query SMART data if SMART is supported by the disk
247 and when the disk is awake.
248 """
249 return self.device.is_smart_supported() and self.device.is_awake()
250
251 def is_awake(self):
252 # If SMART is supported we can get the data from the disk
253 if self.device.is_smart_supported():
254 if self.device.is_awake():
255 return 1
256 else:
257 return 0
258
259 # Otherwise we just assume that the disk is awake
260 return 1
261
262 def get_temperature(self):
263 if not self.is_smart_supported():
264 return "NaN"
265
60b179c1
AF
266 try:
267 return self.device.get_temperature()
268 except OSError:
269 return "NaN"
30777a6c
MT
270
271 def get_bad_sectors(self):
272 if not self.is_smart_supported():
273 return "NaN"
274
275 return self.device.get_bad_sectors()
276
277
278class DiskPlugin(base.Plugin):
279 name = "disk"
280 description = "Disk Plugin"
281
282 templates = [
283 GraphTemplateDiskBadSectors,
284 GraphTemplateDiskBytes,
285 GraphTemplateDiskIoOps,
286 GraphTemplateDiskTemperature,
287 ]
288
289 block_device_patterns = [
aa0c868a
MT
290 r"(x?v|s)d[a-z]+",
291 r"mmcblk[0-9]+",
30777a6c
MT
292 ]
293
294 @property
295 def objects(self):
296 for dev in self.find_block_devices():
297 try:
298 yield DiskObject(self, dev)
299 except OSError:
300 pass
301
302 def find_block_devices(self):
303 for device in os.listdir("/sys/block"):
304 # Skip invalid device names
305 if not self._valid_block_device_name(device):
306 continue
307
308 yield device
309
310 def _valid_block_device_name(self, name):
311 # Check if the given name matches any of the valid patterns.
312 for pattern in self.block_device_patterns:
aa0c868a 313 if re.match(pattern, name):
30777a6c
MT
314 return True
315
316 return False