]>
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 | 27 | |
331fcf57 | 28 | from ..colours import * |
30777a6c MT |
29 | from ..i18n import _ |
30 | ||
31 | class 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 | ||
55 | class 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 | ||
94 | class 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 | ||
130 | class 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 | ||
171 | class 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 | ||
283 | class 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 |