]> git.ipfire.org Git - collecty.git/blame - src/collecty/plugins/disk.py
disk: Localise plugin
[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
22from collecty import _collecty
23import os
24import re
25
f37913e8 26from . import base
30777a6c
MT
27
28from ..i18n import _
29
30class GraphTemplateDiskBadSectors(base.GraphTemplate):
31 name = "disk-bad-sectors"
32
0240a325
MT
33 @property
34 def rrd_graph(self):
35 _ = self.locale.translate
30777a6c 36
0240a325
MT
37 return [
38 "DEF:bad_sectors=%(file)s:bad_sectors:AVERAGE",
30777a6c 39
0240a325
MT
40 "AREA:bad_sectors#ff0000:%s" % _("Bad Sectors"),
41
42 "VDEF:bad_sectors_cur=bad_sectors,LAST",
43 "VDEF:bad_sectors_max=bad_sectors,MAXIMUM",
44 "GPRINT:bad_sectors_cur:%12s\:" % _("Current") + " %9.2lf",
45 "GPRINT:bad_sectors_max:%12s\:" % _("Maximum") + " %9.2lf\\n",
46 ]
30777a6c
MT
47
48 @property
49 def graph_title(self):
0240a325 50 _ = self.locale.translate
30777a6c
MT
51 return _("Bad Sectors of %s") % self.object.device_string
52
53 @property
54 def graph_vertical_label(self):
0240a325 55 _ = self.locale.translate
30777a6c
MT
56 return _("Pending/Relocated Sectors")
57
58
59class GraphTemplateDiskBytes(base.GraphTemplate):
60 name = "disk-bytes"
61
0240a325
MT
62 @property
63 def rrd_graph(self):
64 _ = self.locale.translate
65
66 rrd_graph = [
67 "DEF:read_sectors=%(file)s:read_sectors:AVERAGE",
68 "DEF:write_sectors=%(file)s:write_sectors:AVERAGE",
69
70 "CDEF:read_bytes=read_sectors,512,*",
71 "CDEF:write_bytes=write_sectors,512,*",
72
73 "LINE1:read_bytes#ff0000:%-15s" % _("Read"),
74 "VDEF:read_cur=read_bytes,LAST",
75 "VDEF:read_min=read_bytes,MINIMUM",
76 "VDEF:read_max=read_bytes,MAXIMUM",
77 "VDEF:read_avg=read_bytes,AVERAGE",
78 "GPRINT:read_cur:%12s\:" % _("Current") + " %9.2lf",
79 "GPRINT:read_max:%12s\:" % _("Maximum") + " %9.2lf",
80 "GPRINT:read_min:%12s\:" % _("Minimum") + " %9.2lf",
81 "GPRINT:read_avg:%12s\:" % _("Average") + " %9.2lf\\n",
82
83 "LINE1:write_bytes#00ff00:%-15s" % _("Written"),
84 "VDEF:write_cur=write_bytes,LAST",
85 "VDEF:write_min=write_bytes,MINIMUM",
86 "VDEF:write_max=write_bytes,MAXIMUM",
87 "VDEF:write_avg=write_bytes,AVERAGE",
88 "GPRINT:write_cur:%12s\:" % _("Current") + " %9.2lf",
89 "GPRINT:write_max:%12s\:" % _("Maximum") + " %9.2lf",
90 "GPRINT:write_min:%12s\:" % _("Minimum") + " %9.2lf",
91 "GPRINT:write_avg:%12s\:" % _("Average") + " %9.2lf\\n",
92 ]
93
94 return rrd_graph
30777a6c
MT
95
96 lower_limit = 0
97
98 @property
99 def graph_title(self):
0240a325 100 _ = self.locale.translate
30777a6c
MT
101 return _("Disk Utilisation of %s") % self.object.device_string
102
103 @property
104 def graph_vertical_label(self):
0240a325 105 _ = self.locale.translate
30777a6c
MT
106 return _("Byte per Second")
107
108
109class GraphTemplateDiskIoOps(base.GraphTemplate):
110 name = "disk-io-ops"
111
0240a325
MT
112 @property
113 def rrd_graph(self):
114 _ = self.locale.translate
115
116 rrd_graph = [
117 "DEF:read_ios=%(file)s:read_ios:AVERAGE",
118 "DEF:write_ios=%(file)s:write_ios:AVERAGE",
119
120 "LINE1:read_ios#ff0000:%-15s" % _("Read"),
121 "VDEF:read_cur=read_ios,LAST",
122 "VDEF:read_min=read_ios,MINIMUM",
123 "VDEF:read_max=read_ios,MAXIMUM",
124 "VDEF:read_avg=read_ios,AVERAGE",
125 "GPRINT:read_cur:%12s\:" % _("Current") + " %6.2lf",
126 "GPRINT:read_max:%12s\:" % _("Maximum") + " %6.2lf",
127 "GPRINT:read_min:%12s\:" % _("Minimum") + " %6.2lf",
128 "GPRINT:read_avg:%12s\:" % _("Average") + " %6.2lf\\n",
129
130 "LINE1:write_ios#00ff00:%-15s" % _("Written"),
131 "VDEF:write_cur=write_ios,LAST",
132 "VDEF:write_min=write_ios,MINIMUM",
133 "VDEF:write_max=write_ios,MAXIMUM",
134 "VDEF:write_avg=write_ios,AVERAGE",
135 "GPRINT:write_cur:%12s\:" % _("Current") + " %6.2lf",
136 "GPRINT:write_max:%12s\:" % _("Maximum") + " %6.2lf",
137 "GPRINT:write_min:%12s\:" % _("Minimum") + " %6.2lf",
138 "GPRINT:write_avg:%12s\:" % _("Average") + " %6.2lf\\n",
139 ]
140
141 return rrd_graph
30777a6c
MT
142
143 lower_limit = 0
144
145 @property
146 def graph_title(self):
0240a325 147 _ = self.locale.translate
30777a6c
MT
148 return _("Disk IO Operations of %s") % self.object.device_string
149
150 @property
151 def graph_vertical_label(self):
0240a325 152 _ = self.locale.translate
30777a6c
MT
153 return _("Operations per Second")
154
155
156class GraphTemplateDiskTemperature(base.GraphTemplate):
157 name = "disk-temperature"
158
0240a325
MT
159 @property
160 def rrd_graph(self):
161 _ = self.locale.translate
162
163 rrd_graph = [
164 "DEF:kelvin=%(file)s:temperature:AVERAGE",
165 "CDEF:celsius=kelvin,273.15,-",
166
167 "LINE2:celsius#ff0000:%s" % _("Temperature"),
168 "VDEF:temp_cur=celsius,LAST",
169 "VDEF:temp_min=celsius,MINIMUM",
170 "VDEF:temp_max=celsius,MAXIMUM",
171 "VDEF:temp_avg=celsius,AVERAGE",
172 "GPRINT:temp_cur:%12s\:" % _("Current") + " %3.2lf",
173 "GPRINT:temp_max:%12s\:" % _("Maximum") + " %3.2lf",
174 "GPRINT:temp_min:%12s\:" % _("Minimum") + " %3.2lf",
175 "GPRINT:temp_avg:%12s\:" % _("Average") + " %3.2lf\\n",
176 ]
177
178 return rrd_graph
30777a6c
MT
179
180 @property
181 def graph_title(self):
0240a325 182 _ = self.locale.translate
30777a6c
MT
183 return _("Disk Temperature of %s") % self.object.device_string
184
185 @property
186 def graph_vertical_label(self):
0240a325 187 _ = self.locale.translate
30777a6c
MT
188 return _("° Celsius")
189
190 @property
191 def rrd_graph_args(self):
192 return [
193 # Make the y-axis have a decimal
194 "--left-axis-format", "%3.1lf",
195 ]
196
197
198class DiskObject(base.Object):
199 rrd_schema = [
200 "DS:awake:GAUGE:0:1",
201 "DS:read_ios:DERIVE:0:U",
202 "DS:read_sectors:DERIVE:0:U",
203 "DS:write_ios:DERIVE:0:U",
204 "DS:write_sectors:DERIVE:0:U",
205 "DS:bad_sectors:GAUGE:0:U",
206 "DS:temperature:GAUGE:U:U",
207 ]
208
209 def __repr__(self):
210 return "<%s %s (%s)>" % (self.__class__.__name__, self.sys_path, self.id)
211
212 def init(self, device):
213 self.dev_path = os.path.join("/dev", device)
214 self.sys_path = os.path.join("/sys/block", device)
215
216 self.device = _collecty.BlockDevice(self.dev_path)
217
218 @property
219 def id(self):
220 return "-".join((self.device.model, self.device.serial))
221
222 @property
223 def device_string(self):
224 return "%s (%s)" % (self.device.model, self.dev_path)
225
226 def collect(self):
227 stats = self.parse_stats()
228
f648421a 229 return (
30777a6c
MT
230 self.is_awake(),
231 stats.get("read_ios"),
232 stats.get("read_sectors"),
233 stats.get("write_ios"),
234 stats.get("write_sectors"),
235 self.get_bad_sectors(),
236 self.get_temperature(),
f648421a 237 )
30777a6c
MT
238
239 def parse_stats(self):
240 """
241 https://www.kernel.org/doc/Documentation/block/stat.txt
242
243 Name units description
244 ---- ----- -----------
245 read I/Os requests number of read I/Os processed
246 read merges requests number of read I/Os merged with in-queue I/O
247 read sectors sectors number of sectors read
248 read ticks milliseconds total wait time for read requests
249 write I/Os requests number of write I/Os processed
250 write merges requests number of write I/Os merged with in-queue I/O
251 write sectors sectors number of sectors written
252 write ticks milliseconds total wait time for write requests
253 in_flight requests number of I/Os currently in flight
254 io_ticks milliseconds total time this block device has been active
255 time_in_queue milliseconds total wait time for all requests
256 """
257 stats_file = os.path.join(self.sys_path, "stat")
258
259 with open(stats_file) as f:
260 stats = f.read().split()
261
262 return {
263 "read_ios" : stats[0],
264 "read_merges" : stats[1],
265 "read_sectors" : stats[2],
266 "read_ticks" : stats[3],
267 "write_ios" : stats[4],
268 "write_merges" : stats[5],
269 "write_sectors" : stats[6],
270 "write_ticks" : stats[7],
271 "in_flight" : stats[8],
272 "io_ticks" : stats[9],
273 "time_in_queue" : stats[10],
274 }
275
276 def is_smart_supported(self):
277 """
278 We can only query SMART data if SMART is supported by the disk
279 and when the disk is awake.
280 """
281 return self.device.is_smart_supported() and self.device.is_awake()
282
283 def is_awake(self):
284 # If SMART is supported we can get the data from the disk
285 if self.device.is_smart_supported():
286 if self.device.is_awake():
287 return 1
288 else:
289 return 0
290
291 # Otherwise we just assume that the disk is awake
292 return 1
293
294 def get_temperature(self):
295 if not self.is_smart_supported():
296 return "NaN"
297
298 return self.device.get_temperature()
299
300 def get_bad_sectors(self):
301 if not self.is_smart_supported():
302 return "NaN"
303
304 return self.device.get_bad_sectors()
305
306
307class DiskPlugin(base.Plugin):
308 name = "disk"
309 description = "Disk Plugin"
310
311 templates = [
312 GraphTemplateDiskBadSectors,
313 GraphTemplateDiskBytes,
314 GraphTemplateDiskIoOps,
315 GraphTemplateDiskTemperature,
316 ]
317
318 block_device_patterns = [
319 re.compile(r"(x?v|s)d[a-z]+"),
320 re.compile(r"mmcblk[0-9]+"),
321 ]
322
323 @property
324 def objects(self):
325 for dev in self.find_block_devices():
326 try:
327 yield DiskObject(self, dev)
328 except OSError:
329 pass
330
331 def find_block_devices(self):
332 for device in os.listdir("/sys/block"):
333 # Skip invalid device names
334 if not self._valid_block_device_name(device):
335 continue
336
337 yield device
338
339 def _valid_block_device_name(self, name):
340 # Check if the given name matches any of the valid patterns.
341 for pattern in self.block_device_patterns:
342 if pattern.match(name):
343 return True
344
345 return False