]>
Commit | Line | Data |
---|---|---|
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 | ||
22 | from collecty import _collecty | |
23 | import os | |
24 | import re | |
25 | ||
f37913e8 | 26 | from . import base |
30777a6c MT |
27 | |
28 | from ..i18n import _ | |
29 | ||
30 | class 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 | ||
53 | class 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 | ||
95 | class 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 | ||
134 | class 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 | ||
168 | class 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 | ||
277 | class 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 |