Issues (49)

glances/plugins/network/__init__.py (1 issue)

1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""Network plugin."""
10
11
import psutil
12
13
from glances.logger import logger
14
from glances.plugins.plugin.model import GlancesPluginModel
15
16
# Fields description
17
# description: human readable description
18
# short_name: shortname to use un UI
19
# unit: unit type
20
# rate: if True then compute and add *_gauge and *_rate_per_is fields
21
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
22
fields_description = {
23
    'interface_name': {'description': 'Interface name.'},
24
    'alias': {'description': 'Interface alias name (optional).'},
25
    'bytes_recv': {
26
        'description': 'Number of bytes received.',
27
        'rate': True,
28
        'unit': 'byte',
29
    },
30
    'bytes_sent': {
31
        'description': 'Number of bytes sent.',
32
        'rate': True,
33
        'unit': 'byte',
34
    },
35
    'bytes_all': {
36
        'description': 'Number of bytes received and sent.',
37
        'rate': True,
38
        'unit': 'byte',
39
    },
40
    'speed': {
41
        'description': 'Maximum interface speed (in bit per second). Can return 0 on some operating-system.',
42
        'unit': 'bitpersecond',
43
    },
44
    'is_up': {'description': 'Is the interface up ?', 'unit': 'bool'},
45
}
46
47
# SNMP OID
48
# http://www.net-snmp.org/docs/mibs/interfaces.html
49
# Dict key = interface_name
50
snmp_oid = {
51
    'default': {
52
        'interface_name': '1.3.6.1.2.1.2.2.1.2',
53
        'bytes_recv': '1.3.6.1.2.1.2.2.1.10',
54
        'bytes_sent': '1.3.6.1.2.1.2.2.1.16',
55
    }
56
}
57
58
# Define the history items list
59
items_history_list = [
60
    {'name': 'bytes_recv_rate_per_sec', 'description': 'Download rate per second', 'y_unit': 'B/s'},
61
    {'name': 'bytes_sent_rate_per_sec', 'description': 'Upload rate per second', 'y_unit': 'B/s'},
62
]
63
64
65
class PluginModel(GlancesPluginModel):
66
    """Glances network plugin.
67
68
    stats is a list
69
    """
70
71 View Code Duplication
    def __init__(self, args=None, config=None):
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
72
        """Init the plugin."""
73
        super().__init__(
74
            args=args,
75
            config=config,
76
            items_history_list=items_history_list,
77
            fields_description=fields_description,
78
            stats_init_value=[],
79
        )
80
81
        # We want to display the stat in the curse interface
82
        self.display_curse = True
83
84
        # Hide stats if it has never been != 0
85
        if config is not None:
86
            self.hide_zero = config.get_bool_value(self.plugin_name, 'hide_zero', default=False)
87
        else:
88
            self.hide_zero = False
89
        self.hide_zero_fields = ['bytes_recv', 'bytes_sent']
90
91
        # Force a first update because we need two updates to have the first stat
92
        self.update()
93
        self.refresh_timer.set(0)
94
95
    def get_key(self):
96
        """Return the key of the list."""
97
        return 'interface_name'
98
99
    # @GlancesPluginModel._check_decorator
100
    @GlancesPluginModel._log_result_decorator
101
    def update(self):
102
        """Update network stats using the input method.
103
104
        :return: list of stats dict (one dict per interface)
105
        """
106
        if self.input_method == 'local':
107
            stats = self.update_local()
108
        else:
109
            stats = self.get_init_value()
110
111
        # Update the stats
112
        self.stats = stats
113
114
        return self.stats
115
116
    @GlancesPluginModel._manage_rate
117
    def update_local(self):
118
        # Update stats using the standard system lib
119
120
        stats = self.get_init_value()
121
122
        # Grab network interface stat using the psutil net_io_counter method
123
        # Example:
124
        # { 'veth4cbf8f0a': snetio(
125
        #   bytes_sent=102038421, bytes_recv=1263258,
126
        #   packets_sent=25046, packets_recv=14114,
127
        #   errin=0, errout=0, dropin=0, dropout=0), ... }
128
        try:
129
            net_io_counters = psutil.net_io_counters(pernic=True)
130
        except UnicodeDecodeError as e:
131
            logger.debug(f'Can not get network interface counters ({e})')
132
            return self.stats
133
134
        # Grab interface's status (issue #765)
135
        # Grab interface's speed (issue #718)
136
        # Example:
137
        # { 'veth4cbf8f0a': snicstats(
138
        #   isup=True, duplex=<NicDuplex.NIC_DUPLEX_FULL: 2>,
139
        #   speed=10000, mtu=1500, flags='up,broadcast,running,multicast'), ... }
140
        net_status = {}
141
        try:
142
            net_status = psutil.net_if_stats()
143
        except OSError as e:
144
            # see psutil #797/glances #1106
145
            logger.debug(f'Can not get network interface status ({e})')
146
147
        for interface_name, interface_stat in net_io_counters.items():
148
            # Do not take hidden interface into account
149
            # or KeyError: 'eth0' when interface is not connected #1348
150
            if not self.is_display(interface_name) or interface_name not in net_status:
151
                continue
152
153
            # Filter stats to keep only the fields we want (define in fields_description)
154
            # It will also convert psutil objects to a standard Python dict
155
            stat = self.filter_stats(interface_stat)
156
            stat.update(self.filter_stats(net_status[interface_name]))
157
158
            # Add the key
159
            stat['key'] = self.get_key()
160
161
            # Add disk name
162
            stat['interface_name'] = interface_name
163
164
            # Add alias define in the configuration file
165
            stat['alias'] = self.has_alias(interface_name)
166
167
            # Add sent + revc  stats
168
            stat['bytes_all'] = stat['bytes_sent'] + stat['bytes_recv']
169
170
            # Interface speed in Mbps, convert it to bps
171
            # Can be always 0 on some OSes
172
            stat['speed'] = stat['speed'] * 1048576
173
174
            stats.append(stat)
175
176
        return stats
177
178
    def update_views(self):
179
        """Update stats views."""
180
        # Call the father's method
181
        super().update_views()
182
183
        # Check if the stats should be hidden
184
        self.update_views_hidden()
185
186
        # Add specifics information
187
        # Alert
188
        for i in self.get_raw():
189
            # Skip alert if no timespan to measure
190
            if 'bytes_recv_rate_per_sec' not in i or 'bytes_sent_rate_per_sec' not in i:
191
                continue
192
193
            # Convert rate to bps (to be able to compare to interface speed)
194
            bps_rx = int(i['bytes_recv_rate_per_sec'] * 8)
195
            bps_tx = int(i['bytes_sent_rate_per_sec'] * 8)
196
197
            # Decorate the bitrate with the configuration file thresholds
198
            if_real_name = i['interface_name'].split(':')[0]
199
            alert_rx = self.get_alert(bps_rx, header=if_real_name + '_rx')
200
            alert_tx = self.get_alert(bps_tx, header=if_real_name + '_tx')
201
202
            # If nothing is define in the configuration file...
203
            # ... then use the interface speed (not available on all systems)
204
            if alert_rx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
205
                alert_rx = self.get_alert(current=bps_rx, maximum=i['speed'], header='rx')
206
            if alert_tx == 'DEFAULT' and 'speed' in i and i['speed'] != 0:
207
                alert_tx = self.get_alert(current=bps_tx, maximum=i['speed'], header='tx')
208
209
            # then decorates
210
            self.views[i[self.get_key()]]['bytes_recv']['decoration'] = alert_rx
211
            self.views[i[self.get_key()]]['bytes_sent']['decoration'] = alert_tx
212
213
    def msg_curse(self, args=None, max_width=None):
214
        """Return the dict to display in the curse interface."""
215
        # Init the return message
216
        ret = []
217
218
        # Only process if stats exist and display plugin enable...
219
        if not self.stats or self.is_disabled():
220
            return ret
221
222
        # Max size for the interface name
223
        if max_width:
224
            name_max_width = max_width - 12
225
        else:
226
            # No max_width defined, return an emptu curse message
227
            logger.debug(f"No max_width defined for the {self.plugin_name} plugin, it will not be displayed.")
228
            return ret
229
230
        # Header
231
        msg = '{:{width}}'.format('NETWORK', width=name_max_width)
232
        ret.append(self.curse_add_line(msg, "TITLE"))
233
        if args.network_cumul:
234
            # Cumulative stats
235
            if args.network_sum:
236
                # Sum stats
237
                msg = '{:>14}'.format('Rx+Tx')
238
                ret.append(self.curse_add_line(msg))
239
            else:
240
                # Rx/Tx stats
241
                msg = '{:>7}'.format('Rx')
242
                ret.append(self.curse_add_line(msg))
243
                msg = '{:>7}'.format('Tx')
244
                ret.append(self.curse_add_line(msg))
245
        else:
246
            # Bitrate stats
247
            if args.network_sum:
248
                # Sum stats
249
                msg = '{:>14}'.format('Rx+Tx/s')
250
                ret.append(self.curse_add_line(msg))
251
            else:
252
                msg = '{:>7}'.format('Rx/s')
253
                ret.append(self.curse_add_line(msg))
254
                msg = '{:>7}'.format('Tx/s')
255
                ret.append(self.curse_add_line(msg))
256
        # Interface list (sorted by name)
257
        for i in self.sorted_stats():
258
            # Do not display interface in down state (issue #765)
259
            if ('is_up' in i) and (i['is_up'] is False):
260
                continue
261
            # Hide stats if never be different from 0 (issue #1787)
262
            if all(self.get_views(item=i[self.get_key()], key=f, option='hidden') for f in self.hide_zero_fields):
263
                continue
264
            # Format stats
265
            # Is there an alias for the interface name ?
266
            if i['alias'] is None:
267
                if_name = i['interface_name'].split(':')[0]
268
            else:
269
                if_name = i['alias']
270
            if len(if_name) > name_max_width:
271
                # Cut interface name if it is too long
272
                if_name = '_' + if_name[-name_max_width + 1 :]
273
274
            if args.byte:
275
                # Bytes per second (for dummy)
276
                to_bit = 1
277
                unit = ''
278
            else:
279
                # Bits per second (for real network administrator | Default)
280
                to_bit = 8
281
                unit = 'b'
282
283
            if args.network_cumul and 'bytes_recv' in i:
284
                rx = self.auto_unit(int(i['bytes_recv'] * to_bit)) + unit
285
                tx = self.auto_unit(int(i['bytes_sent'] * to_bit)) + unit
286
                ax = self.auto_unit(int(i['bytes_all'] * to_bit)) + unit
287
            elif 'bytes_recv_rate_per_sec' in i:
288
                rx = self.auto_unit(int(i['bytes_recv_rate_per_sec'] * to_bit)) + unit
289
                tx = self.auto_unit(int(i['bytes_sent_rate_per_sec'] * to_bit)) + unit
290
                ax = self.auto_unit(int(i['bytes_all_rate_per_sec'] * to_bit)) + unit
291
            else:
292
                # Avoid issue when a new interface is created on the fly
293
                # Example: start Glances, then start a new container
294
                continue
295
296
            # New line
297
            ret.append(self.curse_new_line())
298
            msg = '{:{width}}'.format(if_name, width=name_max_width)
299
            ret.append(self.curse_add_line(msg))
300
            if args.network_sum:
301
                msg = f'{ax:>14}'
302
                ret.append(self.curse_add_line(msg))
303
            else:
304
                msg = f'{rx:>7}'
305
                ret.append(
306
                    self.curse_add_line(
307
                        msg, self.get_views(item=i[self.get_key()], key='bytes_recv', option='decoration')
308
                    )
309
                )
310
                msg = f'{tx:>7}'
311
                ret.append(
312
                    self.curse_add_line(
313
                        msg, self.get_views(item=i[self.get_key()], key='bytes_sent', option='decoration')
314
                    )
315
                )
316
317
        return ret
318