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