2 ###############################################################################
4 # collecty - A system statistics collection daemon for IPFire #
5 # Copyright (C) 2012 IPFire development team #
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. #
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. #
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/>. #
20 ###############################################################################
22 from collecty
import _collecty
30 class GraphTemplateDiskBadSectors(base
.GraphTemplate
):
31 name
= "disk-bad-sectors"
34 "DEF:bad_sectors=%(file)s:bad_sectors:AVERAGE",
36 "AREA:bad_sectors#ff0000:%s" % _("Bad Sectors"),
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",
45 def graph_title(self
):
46 return _("Bad Sectors of %s") % self
.object.device_string
49 def graph_vertical_label(self
):
50 return _("Pending/Relocated Sectors")
53 class GraphTemplateDiskBytes(base
.GraphTemplate
):
57 "DEF:read_sectors=%(file)s:read_sectors:AVERAGE",
58 "DEF:write_sectors=%(file)s:write_sectors:AVERAGE",
60 "CDEF:read_bytes=read_sectors,512,*",
61 "CDEF:write_bytes=write_sectors,512,*",
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",
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",
87 def graph_title(self
):
88 return _("Disk Utilisation of %s") % self
.object.device_string
91 def graph_vertical_label(self
):
92 return _("Byte per Second")
95 class GraphTemplateDiskIoOps(base
.GraphTemplate
):
99 "DEF:read_ios=%(file)s:read_ios:AVERAGE",
100 "DEF:write_ios=%(file)s:write_ios:AVERAGE",
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",
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",
126 def graph_title(self
):
127 return _("Disk IO Operations of %s") % self
.object.device_string
130 def graph_vertical_label(self
):
131 return _("Operations per Second")
134 class GraphTemplateDiskTemperature(base
.GraphTemplate
):
135 name
= "disk-temperature"
138 "DEF:mkelvin=%(file)s:temperature:AVERAGE",
139 "CDEF:celsius=mkelvin,1000,/,273.15,-",
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",
153 def graph_title(self
):
154 return _("Disk Temperature of %s") % self
.object.device_string
157 def graph_vertical_label(self
):
158 return _("° Celsius")
161 def rrd_graph_args(self
):
163 # Make the y-axis have a decimal
164 "--left-axis-format", "%3.1lf",
168 class DiskObject(base
.Object
):
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",
180 return "<%s %s (%s)>" % (self
.__class
__.__name
__, self
.sys_path
, self
.id)
182 def init(self
, device
):
183 self
.dev_path
= os
.path
.join("/dev", device
)
184 self
.sys_path
= os
.path
.join("/sys/block", device
)
186 self
.device
= _collecty
.BlockDevice(self
.dev_path
)
190 return "-".join((self
.device
.model
, self
.device
.serial
))
193 def device_string(self
):
194 return "%s (%s)" % (self
.device
.model
, self
.dev_path
)
197 stats
= self
.parse_stats()
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(),
209 def parse_stats(self
):
211 https://www.kernel.org/doc/Documentation/block/stat.txt
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
227 stats_file
= os
.path
.join(self
.sys_path
, "stat")
229 with
open(stats_file
) as f
:
230 stats
= f
.read().split()
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],
246 def is_smart_supported(self
):
248 We can only query SMART data if SMART is supported by the disk
249 and when the disk is awake.
251 return self
.device
.is_smart_supported() and self
.device
.is_awake()
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():
261 # Otherwise we just assume that the disk is awake
264 def get_temperature(self
):
265 if not self
.is_smart_supported():
268 return self
.device
.get_temperature()
270 def get_bad_sectors(self
):
271 if not self
.is_smart_supported():
274 return self
.device
.get_bad_sectors()
277 class DiskPlugin(base
.Plugin
):
279 description
= "Disk Plugin"
282 GraphTemplateDiskBadSectors
,
283 GraphTemplateDiskBytes
,
284 GraphTemplateDiskIoOps
,
285 GraphTemplateDiskTemperature
,
288 block_device_patterns
= [
289 re
.compile(r
"(x?v|s)d[a-z]+"),
290 re
.compile(r
"mmcblk[0-9]+"),
295 for dev
in self
.find_block_devices():
297 yield DiskObject(self
, dev
)
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
):
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
):