Test Failed
Push — master ( 05aaee...10b5c2 )
by Nicolas
04:12 queued 14s
created

Plugin.get_stats_action()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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