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