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