]>
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 | ||
0240a325 MT |
33 | @property |
34 | def rrd_graph(self): | |
35 | _ = self.locale.translate | |
30777a6c | 36 | |
0240a325 | 37 | return [ |
0240a325 | 38 | "AREA:bad_sectors#ff0000:%s" % _("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 | ||
54 | class 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 | ||
65 | "LINE1:read_bytes#ff0000:%-15s" % _("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 MT |
70 | |
71 | "LINE1:write_bytes#00ff00:%-15s" % _("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 | ||
93 | class 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 = [ | |
0240a325 | 101 | "LINE1:read_ios#ff0000:%-15s" % _("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 MT |
106 | |
107 | "LINE1:write_ios#00ff00:%-15s" % _("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 | ||
129 | class 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 MT |
142 | |
143 | "LINE2:celsius#ff0000:%s" % _("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 | ||
170 | class 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 | ||
270 | return self.device.get_temperature() | |
271 | ||
272 | def get_bad_sectors(self): | |
273 | if not self.is_smart_supported(): | |
274 | return "NaN" | |
275 | ||
276 | return self.device.get_bad_sectors() | |
277 | ||
278 | ||
279 | class DiskPlugin(base.Plugin): | |
280 | name = "disk" | |
281 | description = "Disk Plugin" | |
282 | ||
283 | templates = [ | |
284 | GraphTemplateDiskBadSectors, | |
285 | GraphTemplateDiskBytes, | |
286 | GraphTemplateDiskIoOps, | |
287 | GraphTemplateDiskTemperature, | |
288 | ] | |
289 | ||
290 | block_device_patterns = [ | |
291 | re.compile(r"(x?v|s)d[a-z]+"), | |
292 | re.compile(r"mmcblk[0-9]+"), | |
293 | ] | |
294 | ||
295 | @property | |
296 | def objects(self): | |
297 | for dev in self.find_block_devices(): | |
298 | try: | |
299 | yield DiskObject(self, dev) | |
300 | except OSError: | |
301 | pass | |
302 | ||
303 | def find_block_devices(self): | |
304 | for device in os.listdir("/sys/block"): | |
305 | # Skip invalid device names | |
306 | if not self._valid_block_device_name(device): | |
307 | continue | |
308 | ||
309 | yield device | |
310 | ||
311 | def _valid_block_device_name(self, name): | |
312 | # Check if the given name matches any of the valid patterns. | |
313 | for pattern in self.block_device_patterns: | |
314 | if pattern.match(name): | |
315 | return True | |
316 | ||
317 | return False |