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