1 | # -*- coding: utf-8 -*- |
||
2 | # |
||
3 | # This file is part of Glances. |
||
4 | # |
||
5 | # SPDX-FileCopyrightText: 2022 Nicolas Hennion <[email protected]> |
||
6 | # |
||
7 | # SPDX-License-Identifier: LGPL-3.0-only |
||
8 | # |
||
9 | |||
10 | """Containers plugin.""" |
||
11 | |||
12 | import os |
||
13 | from copy import deepcopy |
||
14 | |||
15 | from glances.logger import logger |
||
16 | from glances.plugins.containers.engines.docker import DockerContainersExtension, import_docker_error_tag |
||
17 | from glances.plugins.containers.engines.podman import PodmanContainersExtension, import_podman_error_tag |
||
18 | from glances.plugins.plugin.model import GlancesPluginModel |
||
19 | from glances.processes import glances_processes |
||
20 | from glances.processes import sort_stats as sort_stats_processes |
||
21 | |||
22 | # Fields description |
||
23 | # description: human readable description |
||
24 | # short_name: shortname to use un UI |
||
25 | # unit: unit type |
||
26 | # rate: is it a rate ? If yes, // by time_since_update when displayed, |
||
27 | # min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)... |
||
28 | fields_description = { |
||
29 | 'name': { |
||
30 | 'description': 'Container name', |
||
31 | }, |
||
32 | 'id': { |
||
33 | 'description': 'Container ID', |
||
34 | }, |
||
35 | 'image': { |
||
36 | 'description': 'Container image', |
||
37 | }, |
||
38 | 'status': { |
||
39 | 'description': 'Container status', |
||
40 | }, |
||
41 | 'created': { |
||
42 | 'description': 'Container creation date', |
||
43 | }, |
||
44 | 'command': { |
||
45 | 'description': 'Container command', |
||
46 | }, |
||
47 | 'cpu_percent': { |
||
48 | 'description': 'Container CPU consumption', |
||
49 | 'unit': 'percent', |
||
50 | }, |
||
51 | 'memory_usage': { |
||
52 | 'description': 'Container memory usage', |
||
53 | 'unit': 'byte', |
||
54 | }, |
||
55 | 'io_rx': { |
||
56 | 'description': 'Container IO bytes read rate', |
||
57 | 'unit': 'bytepersecond', |
||
58 | }, |
||
59 | 'io_wx': { |
||
60 | 'description': 'Container IO bytes write rate', |
||
61 | 'unit': 'bytepersecond', |
||
62 | }, |
||
63 | 'network_rx': { |
||
64 | 'description': 'Container network RX bitrate', |
||
65 | 'unit': 'bitpersecond', |
||
66 | }, |
||
67 | 'network_tx': { |
||
68 | 'description': 'Container network TX bitrate', |
||
69 | 'unit': 'bitpersecond', |
||
70 | }, |
||
71 | 'uptime': { |
||
72 | 'description': 'Container uptime', |
||
73 | }, |
||
74 | 'engine': { |
||
75 | 'description': 'Container engine (Docker and Podman are currently supported)', |
||
76 | }, |
||
77 | 'pod_name': { |
||
78 | 'description': 'Pod name (only with Podman)', |
||
79 | }, |
||
80 | 'pod_id': { |
||
81 | 'description': 'Pod ID (only with Podman)', |
||
82 | }, |
||
83 | } |
||
84 | |||
85 | # Define the items history list (list of items to add to history) |
||
86 | # TODO: For the moment limited to the CPU. Had to change the graph exports |
||
87 | # method to display one graph per container. |
||
88 | # items_history_list = [{'name': 'cpu_percent', |
||
89 | # 'description': 'Container CPU consumption in %', |
||
90 | # 'y_unit': '%'}, |
||
91 | # {'name': 'memory_usage', |
||
92 | # 'description': 'Container memory usage in bytes', |
||
93 | # 'y_unit': 'B'}, |
||
94 | # {'name': 'network_rx', |
||
95 | # 'description': 'Container network RX bitrate in bits per second', |
||
96 | # 'y_unit': 'bps'}, |
||
97 | # {'name': 'network_tx', |
||
98 | # 'description': 'Container network TX bitrate in bits per second', |
||
99 | # 'y_unit': 'bps'}, |
||
100 | # {'name': 'io_r', |
||
101 | # 'description': 'Container IO bytes read per second', |
||
102 | # 'y_unit': 'Bps'}, |
||
103 | # {'name': 'io_w', |
||
104 | # 'description': 'Container IO bytes write per second', |
||
105 | # 'y_unit': 'Bps'}] |
||
106 | items_history_list = [{'name': 'cpu_percent', 'description': 'Container CPU consumption in %', 'y_unit': '%'}] |
||
107 | |||
108 | # List of key to remove before export |
||
109 | export_exclude_list = ['cpu', 'io', 'memory', 'network'] |
||
110 | |||
111 | # Sort dictionary for human |
||
112 | sort_for_human = { |
||
113 | 'io_counters': 'disk IO', |
||
114 | 'cpu_percent': 'CPU consumption', |
||
115 | 'memory_usage': 'memory consumption', |
||
116 | 'cpu_times': 'uptime', |
||
117 | 'name': 'container name', |
||
118 | None: 'None', |
||
119 | } |
||
120 | |||
121 | |||
122 | class PluginModel(GlancesPluginModel): |
||
123 | """Glances Docker plugin. |
||
124 | |||
125 | stats is a dict: {'version': {...}, 'containers': [{}, {}]} |
||
126 | """ |
||
127 | |||
128 | def __init__(self, args=None, config=None): |
||
129 | """Init the plugin.""" |
||
130 | super(PluginModel, self).__init__( |
||
131 | args=args, config=config, |
||
132 | items_history_list=items_history_list, |
||
133 | fields_description=fields_description |
||
134 | ) |
||
135 | |||
136 | # The plugin can be disabled using: args.disable_docker |
||
137 | self.args = args |
||
138 | |||
139 | # Default config keys |
||
140 | self.config = config |
||
141 | |||
142 | # We want to display the stat in the curse interface |
||
143 | self.display_curse = True |
||
144 | |||
145 | # Init the Docker API |
||
146 | self.docker_extension = DockerContainersExtension() if not import_docker_error_tag else None |
||
147 | |||
148 | # Init the Podman API |
||
149 | if import_podman_error_tag: |
||
150 | self.podman_extension = None |
||
151 | else: |
||
152 | self.podman_extension = PodmanContainersExtension(podman_sock=self._podman_sock()) |
||
153 | |||
154 | # Sort key |
||
155 | self.sort_key = None |
||
156 | |||
157 | # Force a first update because we need two update to have the first stat |
||
158 | self.update() |
||
159 | self.refresh_timer.set(0) |
||
160 | |||
161 | def _podman_sock(self): |
||
162 | """Return the podman sock. |
||
163 | Could be desfined in the [docker] section thanks to the podman_sock option. |
||
164 | Default value: unix:///run/user/1000/podman/podman.sock |
||
165 | """ |
||
166 | conf_podman_sock = self.get_conf_value('podman_sock') |
||
167 | if len(conf_podman_sock) == 0: |
||
168 | return "unix:///run/user/1000/podman/podman.sock" |
||
169 | else: |
||
170 | return conf_podman_sock[0] |
||
171 | |||
172 | def exit(self): |
||
173 | """Overwrite the exit method to close threads.""" |
||
174 | if self.docker_extension: |
||
175 | self.docker_extension.stop() |
||
176 | if self.podman_extension: |
||
177 | self.podman_extension.stop() |
||
178 | # Call the father class |
||
179 | super(PluginModel, self).exit() |
||
180 | |||
181 | def get_key(self): |
||
182 | """Return the key of the list.""" |
||
183 | return 'name' |
||
184 | |||
185 | def get_export(self): |
||
186 | """Overwrite the default export method. |
||
187 | |||
188 | - Only exports containers |
||
189 | - The key is the first container name |
||
190 | """ |
||
191 | try: |
||
192 | ret = deepcopy(self.stats) |
||
193 | except KeyError as e: |
||
194 | logger.debug("docker plugin - Docker export error {}".format(e)) |
||
195 | ret = [] |
||
196 | |||
197 | # Remove fields uses to compute rate |
||
198 | for container in ret: |
||
199 | for i in export_exclude_list: |
||
200 | container.pop(i) |
||
201 | |||
202 | return ret |
||
203 | |||
204 | def _all_tag(self): |
||
205 | """Return the all tag of the Glances/Docker configuration file. |
||
206 | |||
207 | # By default, Glances only display running containers |
||
208 | # Set the following key to True to display all containers |
||
209 | all=True |
||
210 | """ |
||
211 | all_tag = self.get_conf_value('all') |
||
212 | if len(all_tag) == 0: |
||
213 | return False |
||
214 | else: |
||
215 | return all_tag[0].lower() == 'true' |
||
216 | |||
217 | @GlancesPluginModel._check_decorator |
||
218 | @GlancesPluginModel._log_result_decorator |
||
219 | def update(self): |
||
220 | """Update Docker and podman stats using the input method.""" |
||
221 | # Connection should be ok |
||
222 | if self.docker_extension is None and self.podman_extension is None: |
||
223 | return self.get_init_value() |
||
224 | |||
225 | if self.input_method == 'local': |
||
226 | # Update stats |
||
227 | stats_docker = self.update_docker() if self.docker_extension else {} |
||
228 | stats_podman = self.update_podman() if self.podman_extension else {} |
||
229 | stats = stats_docker.get('containers', []) + stats_podman.get('containers', []) |
||
230 | elif self.input_method == 'snmp': |
||
231 | # Update stats using SNMP |
||
232 | # Not available |
||
233 | pass |
||
234 | |||
235 | # Sort and update the stats |
||
236 | # @TODO: Have a look because sort did not work for the moment (need memory stats ?) |
||
237 | self.sort_key, self.stats = sort_docker_stats(stats) |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
238 | |||
239 | return self.stats |
||
240 | |||
241 | def update_docker(self): |
||
242 | """Update Docker stats using the input method.""" |
||
243 | version, containers = self.docker_extension.update(all_tag=self._all_tag()) |
||
244 | for container in containers: |
||
245 | container["engine"] = 'docker' |
||
246 | return {"version": version, "containers": containers} |
||
247 | |||
248 | def update_podman(self): |
||
249 | """Update Podman stats.""" |
||
250 | version, containers = self.podman_extension.update(all_tag=self._all_tag()) |
||
251 | for container in containers: |
||
252 | container["engine"] = 'podman' |
||
253 | return {"version": version, "containers": containers} |
||
254 | |||
255 | def get_user_ticks(self): |
||
256 | """Return the user ticks by reading the environment variable.""" |
||
257 | return os.sysconf(os.sysconf_names['SC_CLK_TCK']) |
||
258 | |||
259 | def memory_usage_no_cache(self, mem): |
||
260 | """Return the 'real' memory usage by removing inactive_file to usage""" |
||
261 | # Ref: https://github.com/docker/docker-py/issues/3210 |
||
262 | return mem['usage'] - (mem['inactive_file'] if 'inactive_file' in mem else 0) |
||
263 | |||
264 | def update_views(self): |
||
265 | """Update stats views.""" |
||
266 | # Call the father's method |
||
267 | super(PluginModel, self).update_views() |
||
268 | |||
269 | if not self.stats: |
||
270 | return False |
||
271 | |||
272 | # Add specifics information |
||
273 | # Alert |
||
274 | for i in self.stats: |
||
275 | # Init the views for the current container (key = container name) |
||
276 | self.views[i[self.get_key()]] = {'cpu': {}, 'mem': {}} |
||
277 | # CPU alert |
||
278 | if 'cpu' in i and 'total' in i['cpu']: |
||
279 | # Looking for specific CPU container threshold in the conf file |
||
280 | alert = self.get_alert(i['cpu']['total'], header=i['name'] + '_cpu', action_key=i['name']) |
||
281 | if alert == 'DEFAULT': |
||
282 | # Not found ? Get back to default CPU threshold value |
||
283 | alert = self.get_alert(i['cpu']['total'], header='cpu') |
||
284 | self.views[i[self.get_key()]]['cpu']['decoration'] = alert |
||
285 | # MEM alert |
||
286 | if 'memory' in i and 'usage' in i['memory']: |
||
287 | # Looking for specific MEM container threshold in the conf file |
||
288 | alert = self.get_alert( |
||
289 | self.memory_usage_no_cache(i['memory']), |
||
290 | maximum=i['memory']['limit'], |
||
291 | header=i['name'] + '_mem', action_key=i['name'] |
||
292 | ) |
||
293 | if alert == 'DEFAULT': |
||
294 | # Not found ? Get back to default MEM threshold value |
||
295 | alert = self.get_alert( |
||
296 | self.memory_usage_no_cache(i['memory']), |
||
297 | maximum=i['memory']['limit'], |
||
298 | header='mem' |
||
299 | ) |
||
300 | self.views[i[self.get_key()]]['mem']['decoration'] = alert |
||
301 | |||
302 | # Display Engine and Pod name ? |
||
303 | show_pod_name = False |
||
304 | if any(ct.get("pod_name") for ct in self.stats): |
||
305 | show_pod_name = True |
||
306 | self.views['show_pod_name'] = show_pod_name |
||
307 | show_engine_name = False |
||
308 | if len(set(ct["engine"] for ct in self.stats)) > 1: |
||
309 | show_engine_name = True |
||
310 | self.views['show_engine_name'] = show_engine_name |
||
311 | |||
312 | return True |
||
313 | |||
314 | def msg_curse(self, args=None, max_width=None): |
||
315 | """Return the dict to display in the curse interface.""" |
||
316 | # Init the return message |
||
317 | ret = [] |
||
318 | |||
319 | # Only process if stats exist (and non null) and display plugin enable... |
||
320 | if not self.stats or len(self.stats) == 0 or self.is_disabled(): |
||
321 | return ret |
||
322 | |||
323 | # Build the string message |
||
324 | # Title |
||
325 | msg = '{}'.format('CONTAINERS') |
||
326 | ret.append(self.curse_add_line(msg, "TITLE")) |
||
327 | msg = ' {}'.format(len(self.stats)) |
||
328 | ret.append(self.curse_add_line(msg)) |
||
329 | msg = ' sorted by {}'.format(sort_for_human[self.sort_key]) |
||
330 | ret.append(self.curse_add_line(msg)) |
||
331 | ret.append(self.curse_new_line()) |
||
332 | # Header |
||
333 | ret.append(self.curse_new_line()) |
||
334 | # Get the maximum containers name |
||
335 | # Max size is configurable. See feature request #1723. |
||
336 | name_max_width = min( |
||
337 | self.config.get_int_value('containers', 'max_name_size', default=20) if self.config is not None else 20, |
||
338 | len(max(self.stats, key=lambda x: len(x['name']))['name']), |
||
339 | ) |
||
340 | |||
341 | if self.views['show_engine_name']: |
||
342 | msg = ' {:{width}}'.format('Engine', width=6) |
||
343 | ret.append(self.curse_add_line(msg)) |
||
344 | if self.views['show_pod_name']: |
||
345 | msg = ' {:{width}}'.format('Pod', width=12) |
||
346 | ret.append(self.curse_add_line(msg)) |
||
347 | msg = ' {:{width}}'.format('Name', width=name_max_width) |
||
348 | ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'name' else 'DEFAULT')) |
||
349 | msg = '{:>10}'.format('Status') |
||
350 | ret.append(self.curse_add_line(msg)) |
||
351 | msg = '{:>10}'.format('Uptime') |
||
352 | ret.append(self.curse_add_line(msg)) |
||
353 | msg = '{:>6}'.format('CPU%') |
||
354 | ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'cpu_percent' else 'DEFAULT')) |
||
355 | msg = '{:>7}'.format('MEM') |
||
356 | ret.append(self.curse_add_line(msg, 'SORT' if self.sort_key == 'memory_usage' else 'DEFAULT')) |
||
357 | msg = '/{:<7}'.format('MAX') |
||
358 | ret.append(self.curse_add_line(msg)) |
||
359 | msg = '{:>7}'.format('IOR/s') |
||
360 | ret.append(self.curse_add_line(msg)) |
||
361 | msg = ' {:<7}'.format('IOW/s') |
||
362 | ret.append(self.curse_add_line(msg)) |
||
363 | msg = '{:>7}'.format('Rx/s') |
||
364 | ret.append(self.curse_add_line(msg)) |
||
365 | msg = ' {:<7}'.format('Tx/s') |
||
366 | ret.append(self.curse_add_line(msg)) |
||
367 | msg = ' {:8}'.format('Command') |
||
368 | ret.append(self.curse_add_line(msg)) |
||
369 | |||
370 | # Data |
||
371 | for container in self.stats: |
||
372 | ret.append(self.curse_new_line()) |
||
373 | if self.views['show_engine_name']: |
||
374 | ret.append(self.curse_add_line(' {:{width}}'.format(container["engine"], width=6))) |
||
375 | if self.views['show_pod_name']: |
||
376 | ret.append(self.curse_add_line(' {:{width}}'.format(container.get("pod_id", "-"), width=12))) |
||
377 | # Name |
||
378 | ret.append(self.curse_add_line(self._msg_name(container=container, max_width=name_max_width))) |
||
379 | # Status |
||
380 | status = self.container_alert(container['status']) |
||
381 | msg = '{:>10}'.format(container['status'][0:10]) |
||
382 | ret.append(self.curse_add_line(msg, status)) |
||
383 | # Uptime |
||
384 | if container['uptime']: |
||
385 | msg = '{:>10}'.format(container['uptime']) |
||
386 | else: |
||
387 | msg = '{:>10}'.format('_') |
||
388 | ret.append(self.curse_add_line(msg)) |
||
389 | # CPU |
||
390 | try: |
||
391 | msg = '{:>6.1f}'.format(container['cpu']['total']) |
||
392 | except (KeyError, TypeError): |
||
393 | msg = '{:>6}'.format('_') |
||
394 | ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='cpu', option='decoration'))) |
||
395 | # MEM |
||
396 | try: |
||
397 | msg = '{:>7}'.format(self.auto_unit(self.memory_usage_no_cache(container['memory']))) |
||
398 | except KeyError: |
||
399 | msg = '{:>7}'.format('_') |
||
400 | ret.append(self.curse_add_line(msg, self.get_views(item=container['name'], key='mem', option='decoration'))) |
||
401 | try: |
||
402 | msg = '/{:<7}'.format(self.auto_unit(container['memory']['limit'])) |
||
403 | except (KeyError, TypeError): |
||
404 | msg = '/{:<7}'.format('_') |
||
405 | ret.append(self.curse_add_line(msg)) |
||
406 | # IO R/W |
||
407 | unit = 'B' |
||
408 | try: |
||
409 | value = self.auto_unit(int(container['io_rx'])) + unit |
||
410 | msg = '{:>7}'.format(value) |
||
411 | except (KeyError, TypeError): |
||
412 | msg = '{:>7}'.format('_') |
||
413 | ret.append(self.curse_add_line(msg)) |
||
414 | try: |
||
415 | value = self.auto_unit(int(container['io_wx'])) + unit |
||
416 | msg = ' {:<7}'.format(value) |
||
417 | except (KeyError, TypeError): |
||
418 | msg = ' {:<7}'.format('_') |
||
419 | ret.append(self.curse_add_line(msg)) |
||
420 | # NET RX/TX |
||
421 | if args.byte: |
||
422 | # Bytes per second (for dummy) |
||
423 | to_bit = 1 |
||
424 | unit = '' |
||
425 | else: |
||
426 | # Bits per second (for real network administrator | Default) |
||
427 | to_bit = 8 |
||
428 | unit = 'b' |
||
429 | try: |
||
430 | value = ( |
||
431 | self.auto_unit( |
||
432 | int(container['network_rx'] * to_bit) |
||
433 | ) |
||
434 | + unit |
||
435 | ) |
||
436 | msg = '{:>7}'.format(value) |
||
437 | except (KeyError, TypeError): |
||
438 | msg = '{:>7}'.format('_') |
||
439 | ret.append(self.curse_add_line(msg)) |
||
440 | try: |
||
441 | value = ( |
||
442 | self.auto_unit( |
||
443 | int(container['network_tx'] * to_bit) |
||
444 | ) |
||
445 | + unit |
||
446 | ) |
||
447 | msg = ' {:<7}'.format(value) |
||
448 | except (KeyError, TypeError): |
||
449 | msg = ' {:<7}'.format('_') |
||
450 | ret.append(self.curse_add_line(msg)) |
||
451 | # Command |
||
452 | if container['command'] is not None: |
||
453 | msg = ' {}'.format(container['command']) |
||
454 | else: |
||
455 | msg = ' {}'.format('_') |
||
456 | ret.append(self.curse_add_line(msg, splittable=True)) |
||
457 | |||
458 | return ret |
||
459 | |||
460 | def _msg_name(self, container, max_width): |
||
461 | """Build the container name.""" |
||
462 | name = container['name'][:max_width] |
||
463 | return ' {:{width}}'.format(name, width=max_width) |
||
464 | |||
465 | def container_alert(self, status): |
||
466 | """Analyse the container status.""" |
||
467 | if status == 'running': |
||
468 | return 'OK' |
||
469 | elif status == 'exited': |
||
470 | return 'WARNING' |
||
471 | elif status == 'dead': |
||
472 | return 'CRITICAL' |
||
473 | else: |
||
474 | return 'CAREFUL' |
||
475 | |||
476 | |||
477 | def sort_docker_stats(stats): |
||
478 | # Sort Docker stats using the same function than processes |
||
479 | sort_by = glances_processes.sort_key |
||
480 | sort_by_secondary = 'memory_usage' |
||
481 | if sort_by == 'memory_percent': |
||
482 | sort_by = 'memory_usage' |
||
483 | sort_by_secondary = 'cpu_percent' |
||
484 | elif sort_by in ['username', 'io_counters', 'cpu_times']: |
||
485 | sort_by = 'cpu_percent' |
||
486 | |||
487 | # Sort docker stats |
||
488 | sort_stats_processes( |
||
489 | stats, |
||
490 | sorted_by=sort_by, |
||
491 | sorted_by_secondary=sort_by_secondary, |
||
492 | # Reverse for all but name |
||
493 | reverse=glances_processes.sort_key != 'name', |
||
494 | ) |
||
495 | |||
496 | # Return the main sort key and the sorted stats |
||
497 | return sort_by, stats |
||
498 |