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