Test Failed
Push — develop ( a9e382...15d993 )
by Nicolas
02:28
created

GlancesPluginModel.get_limits()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nop 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""
10
I am your father...
11
12
...of all Glances model plugins.
13
"""
14
15
import copy
16
import re
17
18
from glances.actions import GlancesActions
19
from glances.events_list import glances_events
20
from glances.globals import dictlist, iterkeys, itervalues, json_dumps, json_dumps_dictlist, listkeys, mean, nativestr
21
from glances.history import GlancesHistory
22
from glances.logger import logger
23
from glances.outputs.glances_unicode import unicode_message
24
from glances.thresholds import glances_thresholds
25
from glances.timer import Counter, Timer, getTimeSinceLastUpdate
26
27
fields_unit_short = {'percent': '%'}
28
29
fields_unit_type = {
30
    'percent': 'float',
31
    'percents': 'float',
32
    'number': 'int',
33
    'numbers': 'int',
34
    'int': 'int',
35
    'ints': 'int',
36
    'float': 'float',
37
    'floats': 'float',
38
    'second': 'int',
39
    'seconds': 'int',
40
    'byte': 'int',
41
    'bytes': 'int',
42
}
43
44
45
class GlancesPluginModel:
46
    """Main class for Glances plugin model."""
47
48
    def __init__(self, args=None, config=None, items_history_list=None, stats_init_value={}, fields_description=None):
49
        """Init the plugin of plugins model class.
50
51
        All Glances' plugins model should inherit from this class. Most of the
52
        methods are already implemented in the father classes.
53
54
        Your plugin should return a dict or a list of dicts (stored in the
55
        self.stats). As an example, you can have a look on the mem plugin
56
        (for dict) or network (for list of dicts).
57
58
        From version 4 of the API, the plugin should return a dict.
59
60
        A plugin should implement:
61
        - the reset method: to set your self.stats variable to {} or []
62
        - the update method: where your self.stats variable is set
63
        and optionally:
64
        - the get_key method: set the key of the dict (only for list of dict)
65
        - all others methods you want to overwrite
66
67
        :args: args parameters
68
        :config: configuration parameters
69
        :items_history_list: list of items to store in the history
70
        :stats_init_value: Default value for a stats item
71
        """
72
        # Build the plugin name
73
        # Internal or external module (former prefixed by 'glances.plugins')
74
        _mod = self.__class__.__module__.replace('glances.plugins.', '')
75
        self.plugin_name = _mod.split('.')[0]
76
77
        if self.plugin_name.startswith('glances_'):
78
            self.plugin_name = self.plugin_name.split('glances_')[1]
79
        logger.debug(f"Init {self.plugin_name} plugin")
80
81
        # Init the args
82
        self.args = args
83
84
        # Init the default alignment (for curses)
85
        self._align = 'left'
86
87
        # Init the input method
88
        self._input_method = 'local'
89
        self._short_system_name = None
90
91
        # Init the history list
92
        self.items_history_list = items_history_list
93
        self.stats_history = self.init_stats_history()
94
95
        # Init the limits (configuration keys) dictionary
96
        self._limits = {}
97
        if config is not None:
98
            logger.debug(f'Load section {self.plugin_name} in Glances configuration file')
99
            self.load_limits(config=config)
100
101
        # Init the alias (dictionary)
102
        self.alias = self.read_alias()
103
104
        # Init the actions
105
        self.actions = GlancesActions(args=args)
106
107
        # Init the views
108
        self.views = {}
109
110
        # Hide stats if all the hide_zero_fields has never been != 0
111
        # Default is False, always display stats
112
        self.hide_zero = False
113
        # The threshold needed to display a value if hide_zero is true.
114
        # Only hide a value if it is less than hide_threshold_bytes.
115
        self.hide_threshold_bytes = 0
116
        self.hide_zero_fields = []
117
118
        # Set the initial refresh time to display stats the first time
119
        self.refresh_timer = Timer(0)
120
121
        # Init stats description
122
        self.fields_description = fields_description
123
124
        # Init the stats
125
        self.stats_init_value = stats_init_value
126
        self.time_since_last_update = None
127
        self.stats = None
128
        self.stats_previous = None
129
        self.reset()
130
131
    def __repr__(self):
132
        """Return the raw stats."""
133
        return str(self.stats)
134
135
    def __str__(self):
136
        """Return the human-readable stats."""
137
        return str(self.stats)
138
139
    def get_init_value(self):
140
        """Return a copy of the init value."""
141
        return copy.copy(self.stats_init_value)
142
143
    def reset(self):
144
        """Reset the stats.
145
146
        This method should be overwritten by child classes.
147
        """
148
        self.stats = self.get_init_value()
149
150
    def exit(self):
151
        """Just log an event when Glances exit."""
152
        logger.debug(f"Stop the {self.plugin_name} plugin")
153
154
    def get_key(self):
155
        """Return the key of the list."""
156
        return
157
158
    def is_enabled(self, plugin_name=None):
159
        """Return true if plugin is enabled."""
160
        if not plugin_name:
161
            plugin_name = self.plugin_name
162
        try:
163
            d = getattr(self.args, 'disable_' + plugin_name)
164
        except AttributeError:
165
            d = getattr(self.args, 'enable_' + plugin_name, True)
166
        return d is False
167
168
    def is_disabled(self, plugin_name=None):
169
        """Return true if plugin is disabled."""
170
        return not self.is_enabled(plugin_name=plugin_name)
171
172
    def history_enable(self):
173
        return self.args is not None and not self.args.disable_history and self.get_items_history_list() is not None
174
175
    def init_stats_history(self):
176
        """Init the stats history (dict of GlancesAttribute)."""
177
        if self.history_enable():
178
            init_list = [a['name'] for a in self.get_items_history_list()]
179
            logger.debug(f"Stats history activated for plugin {self.plugin_name} (items: {init_list})")
180
        return GlancesHistory()
181
182
    def reset_stats_history(self):
183
        """Reset the stats history (dict of GlancesAttribute)."""
184
        if self.history_enable():
185
            reset_list = [a['name'] for a in self.get_items_history_list()]
186
            logger.debug(f"Reset history for plugin {self.plugin_name} (items: {reset_list})")
187
            self.stats_history.reset()
188
189
    def update_stats_history(self):
190
        """Update stats history."""
191
        # Build the history
192
        if not (self.get_export() and self.history_enable()):
193
            return
194
        # Itern through items history
195
        item_name = '' if self.get_key() is None else self.get_key()
196
        for i in self.get_items_history_list():
197
            if isinstance(self.get_export(), list):
198
                # Stats is a list of data
199
                # Iter through stats (for example, iter through network interface)
200
                for l_export in self.get_export():
201
                    if i['name'] in l_export:
202
                        self.stats_history.add(
203
                            nativestr(l_export[item_name]) + '_' + nativestr(i['name']),
204
                            l_export[i['name']],
205
                            description=i['description'],
206
                            history_max_size=self._limits['history_size'],
207
                        )
208
            else:
209
                # Stats is not a list
210
                # Add the item to the history directly
211
                self.stats_history.add(
212
                    nativestr(i['name']),
213
                    self.get_export()[i['name']],
214
                    description=i['description'],
215
                    history_max_size=self._limits['history_size'],
216
                )
217
218
    def get_items_history_list(self):
219
        """Return the items history list."""
220
        return self.items_history_list
221
222
    def get_raw_history(self, item=None, nb=0):
223
        """Return the history (RAW format).
224
225
        - the stats history (dict of list) if item is None
226
        - the stats history for the given item (list) instead
227
        - None if item did not exist in the history
228
        """
229
        s = self.stats_history.get(nb=nb)
230
        if item is None:
231
            return s
232
        if item in s:
233
            return s[item]
234
        return None
235
236
    def get_export_history(self, item=None):
237
        """Return the stats history object to export."""
238
        return self.get_raw_history(item=item)
239
240
    def get_stats_history(self, item=None, nb=0):
241
        """Return the stats history (JSON format)."""
242
        s = self.stats_history.get_json(nb=nb)
243
244
        if item is None:
245
            return json_dumps(s)
246
247
        return json_dumps_dictlist(s, item)
248
249
    def get_trend(self, item, nb=30):
250
        """Get the trend regarding to the last nb values.
251
252
        The trend is the diffirence between the mean of the last 0 to nb / 2
253
        and nb / 2 to nb values.
254
        """
255
        raw_history = self.get_raw_history(item=item, nb=nb)
256
        if raw_history is None or len(raw_history) < nb:
257
            return None
258
        last_nb = [v[1] for v in raw_history]
259
        return mean(last_nb[nb // 2 :]) - mean(last_nb[: nb // 2])
260
261
    @property
262
    def input_method(self):
263
        """Get the input method."""
264
        return self._input_method
265
266
    @input_method.setter
267
    def input_method(self, input_method):
268
        """Set the input method.
269
270
        * local: system local grab (psutil or direct access)
271
        * snmp: Client server mode via SNMP
272
        * glances: Client server mode via Glances API
273
        """
274
        self._input_method = input_method
275
276
    @property
277
    def short_system_name(self):
278
        """Get the short detected OS name (SNMP)."""
279
        return self._short_system_name
280
281
    def sorted_stats(self):
282
        """Get the stats sorted by an alias (if present) or key."""
283
        key = self.get_key()
284
        if key is None:
285
            return self.stats
286
        try:
287
            return sorted(
288
                self.stats,
289
                key=lambda stat: tuple(
290
                    int(part) if part.isdigit() else part.lower()
291
                    for part in re.split(r"(\d+|\D+)", self.has_alias(stat[key]) or stat[key])
292
                ),
293
            )
294
        except TypeError:
295
            # Correct "Starting an alias with a number causes a crash #1885"
296
            return sorted(
297
                self.stats,
298
                key=lambda stat: tuple(
299
                    part.lower() for part in re.split(r"(\d+|\D+)", self.has_alias(stat[key]) or stat[key])
300
                ),
301
            )
302
303
    @short_system_name.setter
304
    def short_system_name(self, short_name):
305
        """Set the short detected OS name (SNMP)."""
306
        self._short_system_name = short_name
307
308
    def set_stats(self, input_stats):
309
        """Set the stats to input_stats."""
310
        self.stats = input_stats
311
312
    def get_stats_snmp(self, bulk=False, snmp_oid=None):
313
        """Update stats using SNMP.
314
315
        If bulk=True, use a bulk request instead of a get request.
316
        """
317
        snmp_oid = snmp_oid or {}
318
319
        from glances.snmp import GlancesSNMPClient
320
321
        # Init the SNMP request
322
        snmp_client = GlancesSNMPClient(
323
            host=self.args.client,
324
            port=self.args.snmp_port,
325
            version=self.args.snmp_version,
326
            community=self.args.snmp_community,
327
        )
328
329
        # Process the SNMP request
330
        ret = {}
331
        if bulk:
332
            # Bulk request
333
            snmp_result = snmp_client.getbulk_by_oid(0, 10, *list(itervalues(snmp_oid)))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable itervalues does not seem to be defined.
Loading history...
334
            logger.info(snmp_result)
335
            if len(snmp_oid) == 1:
336
                # Bulk command for only one OID
337
                # Note: key is the item indexed but the OID result
338
                for item in snmp_result:
339
                    if iterkeys(item)[0].startswith(itervalues(snmp_oid)[0]):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable iterkeys does not seem to be defined.
Loading history...
340
                        ret[iterkeys(snmp_oid)[0] + iterkeys(item)[0].split(itervalues(snmp_oid)[0])[1]] = itervalues(
341
                            item
342
                        )[0]
343
            else:
344
                # Build the internal dict with the SNMP result
345
                # Note: key is the first item in the snmp_oid
346
                index = 1
347
                for item in snmp_result:
348
                    item_stats = {}
349
                    item_key = None
350
                    for key in iterkeys(snmp_oid):
351
                        oid = snmp_oid[key] + '.' + str(index)
352
                        if oid in item:
353
                            if item_key is None:
354
                                item_key = item[oid]
355
                            else:
356
                                item_stats[key] = item[oid]
357
                    if item_stats:
358
                        ret[item_key] = item_stats
359
                    index += 1
360
        else:
361
            # Simple get request
362
            snmp_result = snmp_client.get_by_oid(*list(itervalues(snmp_oid)))
363
364
            # Build the internal dict with the SNMP result
365
            for key in iterkeys(snmp_oid):
366
                ret[key] = snmp_result[snmp_oid[key]]
367
368
        return ret
369
370
    def get_raw(self):
371
        """Return the stats object."""
372
        return self.stats
373
374
    def get_export(self):
375
        """Return the stats object to export.
376
        By default, return the raw stats.
377
        Note: this method could be overwritten by the plugin if a specific format is needed (ex: processlist)
378
        """
379
        return self.get_raw()
380
381
    def get_stats(self):
382
        """Return the stats object in JSON format."""
383
        return json_dumps(self.get_raw())
384
385
    def get_json(self):
386
        """Return the stats object in JSON format."""
387
        return self.get_stats()
388
389
    def get_raw_stats_item(self, item):
390
        """Return the stats object for a specific item in RAW format.
391
392
        Stats should be a list of dict (processlist, network...)
393
        """
394
        return dictlist(self.get_raw(), item)
395
396
    def get_raw_stats_key(self, item, key):
397
        """Return the stats object for a specific item in RAW format.
398
399
        Stats should be a list of dict (processlist, network...)
400
        """
401
        try:
402
            return {item: [i for i in self.get_raw() if 'key' in i and i[i['key']] == key][0].get(item)}
403
        except (KeyError, ValueError) as e:
404
            logger.error(f"Cannot get item ({item}) for key ({key}) ({e})")
405
            return None
406
407
    def get_stats_item(self, item):
408
        """Return the stats object for a specific item in JSON format.
409
410
        Stats should be a list of dict (processlist, network...)
411
        """
412
        return json_dumps_dictlist(self.get_raw(), item)
413
414
    def get_raw_stats_value(self, item, value):
415
        """Return the stats object for a specific item=value.
416
417
        Return None if the item=value does not exist
418
        Return None if the item is not a list of dict
419
        """
420
        if not isinstance(self.get_raw(), list):
421
            return None
422
423
        if (not isinstance(value, int) and not isinstance(value, float)) and value.isdigit():
424
            value = int(value)
425
        try:
426
            return {value: [i for i in self.get_raw() if i[item] == value]}
427
        except (KeyError, ValueError) as e:
428
            logger.error(f"Cannot get item({item})=value({value}) ({e})")
429
            return None
430
431
    def get_stats_value(self, item, value):
432
        """Return the stats object for a specific item=value in JSON format.
433
434
        Stats should be a list of dict (processlist, network...)
435
        """
436
        rsv = self.get_raw_stats_value(item, value)
437
        if rsv is None:
438
            return None
439
        return json_dumps(rsv)
440
441
    def get_item_info(self, item, key, default=None):
442
        """Return the item info grabbed into self.fields_description."""
443
        if self.fields_description is None or item not in self.fields_description:
444
            return default
445
        return self.fields_description[item].get(key, default)
446
447
    def update_views(self):
448
        """Update the stats views.
449
450
        The V of MVC
451
        A dict of dict with the needed information to display the stats.
452
        Example for the stat xxx:
453
        'xxx': {'decoration': 'DEFAULT',  >>> The decoration of the stats
454
                'optional': False,        >>> Is the stat optional
455
                'additional': False,      >>> Is the stat provide additional information
456
                'splittable': False,      >>> Is the stat can be cut (like process lon name)
457
                'hidden': False}          >>> Is the stats should be hidden in the UI
458
        """
459
        ret = {}
460
461
        if isinstance(self.get_raw(), list) and self.get_raw() is not None and self.get_key() is not None:
462
            # Stats are stored in a list of dict (ex: DISKIO, NETWORK, FS...)
463
            for i in self.get_raw():
464
                key = i[self.get_key()]
465
                ret[key] = {}
466
                for field in listkeys(i):
467
                    value = {
468
                        'decoration': 'DEFAULT',
469
                        'optional': False,
470
                        'additional': False,
471
                        'splittable': False,
472
                    }
473
                    # Manage the hidden feature
474
                    # Allow to automatically hide fields when values is never different than 0
475
                    # Refactoring done for #2929
476
                    if not self.hide_zero:
477
                        value['hidden'] = False
478
                    elif key in self.views and field in self.views[key] and 'hidden' in self.views[key][field]:
479
                        value['hidden'] = self.views[key][field]['hidden']
480
                        if field in self.hide_zero_fields and i[field] > self.hide_threshold_bytes:
481
                            value['hidden'] = False
482
                    else:
483
                        value['hidden'] = field in self.hide_zero_fields
484
                    ret[key][field] = value
485
        elif isinstance(self.get_raw(), dict) and self.get_raw() is not None:
486
            # Stats are stored in a dict (ex: CPU, LOAD...)
487
            for field in listkeys(self.get_raw()):
488
                value = {
489
                    'decoration': 'DEFAULT',
490
                    'optional': False,
491
                    'additional': False,
492
                    'splittable': False,
493
                    'hidden': False,
494
                }
495
                # Manage the hidden feature
496
                # Allow to automatically hide fields when values is never different than 0
497
                # Refactoring done for #2929
498
                if not self.hide_zero:
499
                    value['hidden'] = False
500
                elif field in self.views and 'hidden' in self.views[field]:
501
                    value['hidden'] = self.views[field]['hidden']
502
                    if field in self.hide_zero_fields and self.get_raw()[field] >= self.hide_threshold_bytes:
503
                        value['hidden'] = False
504
                else:
505
                    value['hidden'] = field in self.hide_zero_fields
506
                ret[field] = value
507
508
        self.views = ret
509
510
        return self.views
511
512
    def set_views(self, input_views):
513
        """Set the views to input_views."""
514
        self.views = input_views
515
516
    def reset_views(self):
517
        """Reset the views to input_views."""
518
        self.views = {}
519
520
    def get_views(self, item=None, key=None, option=None):
521
        """Return the views object.
522
523
        If key is None, return all the view for the current plugin
524
        else if option is None return the view for the specific key (all option)
525
        else return the view of the specific key/option
526
527
        Specify item if the stats are stored in a dict of dict (ex: NETWORK, FS...)
528
        """
529
        if item is None:
530
            item_views = self.views
531
        else:
532
            item_views = self.views[item]
533
534
        if key is None or key not in item_views:
535
            return item_views
536
        if option is None:
537
            return item_views[key]
538
        if option in item_views[key]:
539
            return item_views[key][option]
540
        return 'DEFAULT'
541
542
    def get_json_views(self, item=None, key=None, option=None):
543
        """Return the views (in JSON)."""
544
        return json_dumps(self.get_views(item, key, option))
545
546
    def load_limits(self, config):
547
        """Load limits from the configuration file, if it exists."""
548
        # By default set the history length to 3 points per second during one day
549
        self._limits['history_size'] = 28800
550
551
        if not hasattr(config, 'has_section'):
552
            return False
553
554
        # Read the global section
555
        # TODO: not optimized because this section is loaded for each plugin...
556
        if config.has_section('global'):
557
            self._limits['history_size'] = config.get_float_value('global', 'history_size', default=28800)
558
            logger.debug("Load configuration key: {} = {}".format('history_size', self._limits['history_size']))
559
560
        # Read the plugin specific section
561
        if config.has_section(self.plugin_name):
562
            for level, _ in config.items(self.plugin_name):
563
                # Read limits
564
                limit = '_'.join([self.plugin_name, level])
565
                try:
566
                    self._limits[limit] = config.get_float_value(self.plugin_name, level)
567
                except ValueError:
568
                    self._limits[limit] = config.get_value(self.plugin_name, level).split(",")
569
                logger.debug(f"Load limit: {limit} = {self._limits[limit]}")
570
571
        return True
572
573
    @property
574
    def limits(self):
575
        """Return the limits object."""
576
        return self._limits
577
578
    @limits.setter
579
    def limits(self, input_limits):
580
        """Set the limits to input_limits."""
581
        self._limits = input_limits
582
583
    def set_refresh(self, value):
584
        """Set the plugin refresh rate"""
585
        self.set_limits('refresh', value)
586
587
    def get_refresh(self):
588
        """Return the plugin refresh time"""
589
        ret = self.get_limits(item='refresh')
590
        if ret is None:
591
            ret = self.args.time if hasattr(self.args, 'time') else 2
592
        return ret
593
594
    def get_refresh_time(self):
595
        """Return the plugin refresh time"""
596
        return self.get_refresh()
597
598
    def set_limits(self, item, value):
599
        """Set the limits object."""
600
        self._limits[f'{self.plugin_name}_{item}'] = value
601
602
    def get_limits(self, item=None):
603
        """Return the limits object."""
604
        if item is None:
605
            return self._limits
606
        return self._limits.get(f'{self.plugin_name}_{item}', None)
607
608
    def get_stats_action(self):
609
        """Return stats for the action.
610
611
        By default return all the stats.
612
        Can be overwrite by plugins implementation.
613
        For example, Docker will return self.stats['containers']
614
        """
615
        return self.stats
616
617
    def get_stat_name(self, header=""):
618
        """Return the stat name with an optional header"""
619
        ret = self.plugin_name
620
        if header != "":
621
            ret += '_' + header
622
        return ret
623
624
    def get_alert(
625
        self,
626
        current=0,
627
        minimum=0,
628
        maximum=100,
629
        highlight_zero=True,
630
        is_max=False,
631
        header="",
632
        action_key=None,
633
        log=False,
634
    ):
635
        """Return the alert status relative to a current value.
636
637
        Use this function for minor stats.
638
639
        If current < CAREFUL of max then alert = OK
640
        If current > CAREFUL of max then alert = CAREFUL
641
        If current > WARNING of max then alert = WARNING
642
        If current > CRITICAL of max then alert = CRITICAL
643
644
        If highlight=True than 0.0 is highlighted
645
646
        If defined 'header' is added between the plugin name and the status.
647
        Only useful for stats with several alert status.
648
649
        If defined, 'action_key' define the key for the actions.
650
        By default, the action_key is equal to the header.
651
652
        If log=True than add log if necessary
653
        elif log=False than do not log
654
        elif log=None than apply the config given in the conf file
655
        """
656
        # Manage 0 (0.0) value if highlight_zero is not True
657
        if not highlight_zero and current == 0:
658
            return 'DEFAULT'
659
660
        # Compute the %
661
        try:
662
            value = (current * 100) / maximum
663
        except ZeroDivisionError:
664
            return 'DEFAULT'
665
        except TypeError:
666
            return 'DEFAULT'
667
668
        # Build the stat_name
669
        stat_name = self.get_stat_name(header=header)
670
671
        # Manage limits
672
        # If is_max is set then default style is set to MAX else default is set to OK
673
        ret = 'MAX' if is_max else 'OK'
674
675
        # Iter through limits
676
        try:
677
            limit = self.get_limit('critical', stat_name=stat_name)
678
        except KeyError:
679
            try:
680
                limit = self.get_limit('warning', stat_name=stat_name)
681
            except KeyError:
682
                try:
683
                    limit = self.get_limit('careful', stat_name=stat_name)
684
                except KeyError:
685
                    return 'DEFAULT'
686
                else:
687
                    if value >= limit:
688
                        ret = 'CAREFUL'
689
            else:
690
                if value >= limit:
691
                    ret = 'WARNING'
692
        else:
693
            if value >= limit:
694
                ret = 'CRITICAL'
695
696
        if current < minimum:
697
            ret = 'CAREFUL'
698
699
        # Manage log
700
        log_str = ""
701
        if self.get_limit_log(stat_name=stat_name, default_action=log):
702
            # Add _LOG to the return string
703
            # So stats will be highlighted with a specific color
704
            log_str = "_LOG"
705
            # Add the log to the events list
706
            glances_events.add(ret, stat_name.upper(), value)
707
708
        # Manage threshold
709
        self.manage_threshold(stat_name, ret)
710
711
        # Manage action
712
        self.manage_action(stat_name, ret.lower(), header, action_key)
713
714
        # Default is 'OK'
715
        return ret + log_str
716
717
    def filter_stats(self, stats):
718
        """Filter the stats to keep only the fields we want (the one defined in fields_description)."""
719
        if hasattr(stats, '_asdict'):
720
            return {k: v for k, v in stats._asdict().items() if k in self.fields_description}
721
        if isinstance(stats, dict):
722
            return {k: v for k, v in stats.items() if k in self.fields_description}
723
        if isinstance(stats, list):
724
            return [self.filter_stats(s) for s in stats]
725
        return stats
726
727
    def manage_threshold(self, stat_name, trigger):
728
        """Manage the threshold for the current stat."""
729
        glances_thresholds.add(stat_name, trigger)
730
731
    def manage_action(self, stat_name, trigger, header, action_key):
732
        """Manage the action for the current stat."""
733
        # Here is a command line for the current trigger ?
734
        try:
735
            command, repeat = self.get_limit_action(trigger, stat_name=stat_name)
736
        except KeyError:
737
            # Reset the trigger
738
            self.actions.set(stat_name, trigger)
739
        else:
740
            # Define the action key for the stats dict
741
            # If not define, then it sets to header
742
            if action_key is None:
743
                action_key = header
744
745
            # A command line is available for the current alert
746
            # 1) Build the {{mustache}} dictionary
747
            if isinstance(self.get_stats_action(), list):
748
                # If the stats are stored in a list of dict (fs plugin for example)
749
                # Return the dict for the current header
750
                mustache_dict = {}
751
                for item in self.get_stats_action():
752
                    if item[self.get_key()] == action_key:
753
                        mustache_dict = item
754
                        break
755
            else:
756
                # Use the stats dict
757
                mustache_dict = self.get_stats_action()
758
            # 2) Run the action
759
            self.actions.run(stat_name, trigger, command, repeat, mustache_dict=mustache_dict)
760
761
    def get_alert_log(self, current=0, minimum=0, maximum=100, header="", action_key=None):
762
        """Get the alert log."""
763
        return self.get_alert(
764
            current=current, minimum=minimum, maximum=maximum, header=header, action_key=action_key, log=True
765
        )
766
767
    def is_limit(self, criticality, stat_name=""):
768
        """Return true if the criticality limit exist for the given stat_name"""
769
        if stat_name == "":
770
            return self.plugin_name + '_' + criticality in self._limits
771
        return stat_name + '_' + criticality in self._limits
772
773
    def get_limit(self, criticality=None, stat_name=""):
774
        """Return the limit value for the given criticality.
775
        If criticality is None, return the dict of all the limits."""
776
        if criticality is None:
777
            return self._limits
778
779
        # Get the limit for stat + header
780
        # Example: network_wlan0_rx_careful
781
        if stat_name + '_' + criticality in self._limits:
782
            return self._limits[stat_name + '_' + criticality]
783
        if self.plugin_name + '_' + criticality in self._limits:
784
            return self._limits[self.plugin_name + '_' + criticality]
785
786
        # No key found, the raise an error
787
        raise KeyError
788
789
    def get_limit_action(self, criticality, stat_name=""):
790
        """Return the tuple (action, repeat) for the alert.
791
792
        - action is a command line
793
        - repeat is a bool
794
        """
795
        # Get the action for stat + header
796
        # Example: network_wlan0_rx_careful_action
797
        # Action key available ?
798
        ret = [
799
            (stat_name + '_' + criticality + '_action', False),
800
            (stat_name + '_' + criticality + '_action_repeat', True),
801
            (self.plugin_name + '_' + criticality + '_action', False),
802
            (self.plugin_name + '_' + criticality + '_action_repeat', True),
803
        ]
804
        for r in ret:
805
            if r[0] in self._limits:
806
                return self._limits[r[0]], r[1]
807
808
        # No key found, the raise an error
809
        raise KeyError
810
811
    def get_limit_log(self, stat_name, default_action=False):
812
        """Return the log tag for the alert."""
813
        # Get the log tag for stat + header
814
        # Example: network_wlan0_rx_log
815
        if stat_name + '_log' in self._limits:
816
            return self._limits[stat_name + '_log'][0].lower() == 'true'
817
        if self.plugin_name + '_log' in self._limits:
818
            return self._limits[self.plugin_name + '_log'][0].lower() == 'true'
819
        return default_action
820
821
    def get_conf_value(self, value, header="", plugin_name=None, default=[]):
822
        """Return the configuration (header_) value for the current plugin.
823
824
        ...or the one given by the plugin_name var.
825
        """
826
        if plugin_name is None:
827
            # If not default use the current plugin name
828
            plugin_name = self.plugin_name
829
830
        if header != "":
831
            # Add the header
832
            plugin_name = plugin_name + '_' + header
833
834
        try:
835
            return self._limits[plugin_name + '_' + value]
836
        except KeyError:
837
            return default
838
839
    def is_show(self, value, header=""):
840
        """Return True if the value is in the show configuration list.
841
842
        If the show value is empty, return True (show by default)
843
844
        The show configuration list is defined in the glances.conf file.
845
        It is a comma-separated list of regexp.
846
        Example for diskio:
847
        show=sda.*
848
        """
849
        # TODO: possible optimisation: create a re.compile list
850
        return any(
851
            j for j in [re.fullmatch(i.lower(), value.lower()) for i in self.get_conf_value('show', header=header)]
852
        )
853
854
    def is_hide(self, value, header=""):
855
        """Return True if the value is in the hide configuration list.
856
857
        The hide configuration list is defined in the glances.conf file.
858
        It is a comma-separated list of regexp.
859
        Example for diskio:
860
        hide=sda2,sda5,loop.*
861
        """
862
        # TODO: possible optimisation: create a re.compile list
863
        return any(
864
            j for j in [re.fullmatch(i.lower(), value.lower()) for i in self.get_conf_value('hide', header=header)]
865
        )
866
867
    def is_display(self, value, header=""):
868
        """Return True if the value should be displayed in the UI"""
869
        if self.get_conf_value('show', header=header) != []:
870
            return self.is_show(value, header=header)
871
        return not self.is_hide(value, header=header)
872
873
    def read_alias(self):
874
        if self.plugin_name + '_' + 'alias' in self._limits:
875
            return {i.split(':')[0].lower(): i.split(':')[1] for i in self._limits[self.plugin_name + '_' + 'alias']}
876
        return {}
877
878
    def has_alias(self, header):
879
        """Return the alias name for the relative header it it exists otherwise None."""
880
        return self.alias.get(header, None)
881
882
    def msg_curse(self, args=None, max_width=None):
883
        """Return default string to display in the curse interface."""
884
        return [self.curse_add_line(str(self.stats))]
885
886
    def get_stats_display(self, args=None, max_width=None):
887
        """Return a dict with all the information needed to display the stat.
888
889
        key     | description
890
        ----------------------------
891
        display | Display the stat (True or False)
892
        msgdict | Message to display (list of dict [{ 'msg': msg, 'decoration': decoration } ... ])
893
        align   | Message position
894
        """
895
        display_curse = False
896
897
        if hasattr(self, 'display_curse'):
898
            display_curse = self.display_curse
899
        if hasattr(self, 'align'):
900
            align_curse = self._align
901
902
        if max_width is not None:
903
            ret = {'display': display_curse, 'msgdict': self.msg_curse(args, max_width=max_width), 'align': align_curse}
0 ignored issues
show
introduced by
The variable align_curse does not seem to be defined in case hasattr(self, 'align') on line 899 is False. Are you sure this can never be the case?
Loading history...
904
        else:
905
            ret = {'display': display_curse, 'msgdict': self.msg_curse(args), 'align': align_curse}
906
907
        return ret
908
909
    def curse_add_line(self, msg, decoration="DEFAULT", optional=False, additional=False, splittable=False):
910
        """Return a dict with.
911
912
        Where:
913
            msg: string
914
            decoration:
915
                DEFAULT: no decoration
916
                UNDERLINE: underline
917
                BOLD: bold
918
                TITLE: for stat title
919
                PROCESS: for process name
920
                STATUS: for process status
921
                NICE: for process niceness
922
                CPU_TIME: for process cpu time
923
                OK: Value is OK and non logged
924
                OK_LOG: Value is OK and logged
925
                CAREFUL: Value is CAREFUL and non logged
926
                CAREFUL_LOG: Value is CAREFUL and logged
927
                WARNING: Value is WARNING and non logged
928
                WARNING_LOG: Value is WARNING and logged
929
                CRITICAL: Value is CRITICAL and non logged
930
                CRITICAL_LOG: Value is CRITICAL and logged
931
            optional: True if the stat is optional (display only if space is available)
932
            additional: True if the stat is additional (display only if space is available after optional)
933
            spittable: Line can be split to fit on the screen (default is not)
934
        """
935
        return {
936
            'msg': msg,
937
            'decoration': decoration,
938
            'optional': optional,
939
            'additional': additional,
940
            'splittable': splittable,
941
        }
942
943
    def curse_new_line(self):
944
        """Go to a new line."""
945
        return self.curse_add_line('\n')
946
947
    def curse_add_stat(self, key, width=None, header='', display_key=True, separator='', trailer=''):
948
        """Return a list of dict messages with the 'key: value' result
949
950
          <=== width ===>
951
        __key     : 80.5%__
952
        | |       | |    |_ trailer
953
        | |       | |_ self.stats[key]
954
        | |       |_ separator
955
        | |_ 'short_name' description or key or nothing if display_key is True
956
        |_ header
957
958
        Instead of:
959
            msg = '  {:8}'.format('idle:')
960
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
961
            msg = '{:5.1f}%'.format(self.stats['idle'])
962
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
963
964
        Use:
965
            ret.extend(self.curse_add_stat('idle', width=15, header='  '))
966
967
        """
968
        if key not in self.stats:
969
            return []
970
971
        # Check if a shortname is defined
972
        if not display_key:
973
            key_name = ''
974
        elif key in self.fields_description and 'short_name' in self.fields_description[key]:
975
            key_name = self.fields_description[key]['short_name']
976
        else:
977
            key_name = key
978
979
        # Check if unit is defined and get the short unit char in the unit_sort dict
980
        if (
981
            key in self.fields_description
982
            and 'unit' in self.fields_description[key]
983
            and self.fields_description[key]['unit'] in fields_unit_short
984
        ):
985
            # Get the shortname
986
            unit_short = fields_unit_short[self.fields_description[key]['unit']]
987
        else:
988
            unit_short = ''
989
990
        # Check if unit is defined and get the unit type unit_type dict
991
        if (
992
            key in self.fields_description
993
            and 'unit' in self.fields_description[key]
994
            and self.fields_description[key]['unit'] in fields_unit_type
995
        ):
996
            # Get the shortname
997
            unit_type = fields_unit_type[self.fields_description[key]['unit']]
998
        else:
999
            unit_type = 'float'
1000
1001
        # Is it a rate ? Yes, get the pre-computed rate value
1002
        if (
1003
            key in self.fields_description
1004
            and 'rate' in self.fields_description[key]
1005
            and self.fields_description[key]['rate'] is True
1006
        ):
1007
            value = self.stats.get(key + '_rate_per_sec', None)
1008
        else:
1009
            value = self.stats.get(key, None)
1010
1011
        if width is None:
1012
            msg_item = header + f'{key_name}' + separator
1013
            msg_template_float = '{:.1f}{}'
1014
            msg_template = '{}{}'
1015
        else:
1016
            # Define the size of the message
1017
            # item will be on the left
1018
            # value will be on the right
1019
            msg_item = header + '{:{width}}'.format(key_name, width=width - 7) + separator
1020
            msg_template_float = '{:5.1f}{}'
1021
            msg_template = '{:>5}{}'
1022
1023
        if value is None:
1024
            msg_value = msg_template.format('-', '')
1025
        elif unit_type == 'float':
1026
            msg_value = msg_template_float.format(value, unit_short)
1027
        elif 'min_symbol' in self.fields_description[key]:
1028
            msg_value = msg_template.format(
1029
                self.auto_unit(int(value), min_symbol=self.fields_description[key]['min_symbol']), unit_short
1030
            )
1031
        else:
1032
            msg_value = msg_template.format(int(value), unit_short)
1033
1034
        # Add the trailer
1035
        msg_value = msg_value + trailer
1036
1037
        decoration = self.get_views(key=key, option='decoration') if value is not None else 'DEFAULT'
1038
        optional = self.get_views(key=key, option='optional')
1039
1040
        return [
1041
            self.curse_add_line(msg_item, optional=optional),
1042
            self.curse_add_line(msg_value, decoration=decoration, optional=optional),
1043
        ]
1044
1045
    @property
1046
    def align(self):
1047
        """Get the curse align."""
1048
        return self._align
1049
1050
    @align.setter
1051
    def align(self, value):
1052
        """Set the curse align.
1053
1054
        value: left, right, bottom.
1055
        """
1056
        self._align = value
1057
1058
    def auto_unit(self, number, low_precision=False, min_symbol='K', none_symbol='-'):
1059
        """Make a nice human-readable string out of number.
1060
1061
        Number of decimal places increases as quantity approaches 1.
1062
        CASE: 613421788        RESULT:       585M low_precision:       585M
1063
        CASE: 5307033647       RESULT:      4.94G low_precision:       4.9G
1064
        CASE: 44968414685      RESULT:      41.9G low_precision:      41.9G
1065
        CASE: 838471403472     RESULT:       781G low_precision:       781G
1066
        CASE: 9683209690677    RESULT:      8.81T low_precision:       8.8T
1067
        CASE: 1073741824       RESULT:      1024M low_precision:      1024M
1068
        CASE: 1181116006       RESULT:      1.10G low_precision:       1.1G
1069
1070
        :low_precision: returns less decimal places potentially (default is False)
1071
                        sacrificing precision for more readability.
1072
        :min_symbol: Do not approach if number < min_symbol (default is K)
1073
        :decimal_count: if set, force the number of decimal number (default is None)
1074
        """
1075
        if number is None:
1076
            return none_symbol
1077
        symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
1078
        if min_symbol in symbols:
1079
            symbols = symbols[symbols.index(min_symbol) :]
1080
        prefix = {
1081
            'Y': 1208925819614629174706176,
1082
            'Z': 1180591620717411303424,
1083
            'E': 1152921504606846976,
1084
            'P': 1125899906842624,
1085
            'T': 1099511627776,
1086
            'G': 1073741824,
1087
            'M': 1048576,
1088
            'K': 1024,
1089
        }
1090
1091
        if number == 0:
1092
            # Avoid 0.0
1093
            return '0'
1094
1095
        for symbol in reversed(symbols):
1096
            value = float(number) / prefix[symbol]
1097
            if value > 1:
1098
                decimal_precision = 0
1099
                if value < 10:
1100
                    decimal_precision = 2
1101
                elif value < 100:
1102
                    decimal_precision = 1
1103
                if low_precision:
1104
                    if symbol in 'MK':
1105
                        decimal_precision = 0
1106
                    else:
1107
                        decimal_precision = min(1, decimal_precision)
1108
                elif symbol in 'K':
1109
                    decimal_precision = 0
1110
                return '{:.{decimal}f}{symbol}'.format(value, decimal=decimal_precision, symbol=symbol)
1111
        return f'{number!s}'
1112
1113
    def trend_msg(self, trend, significant=1):
1114
        """Return the trend message.
1115
1116
        Do not take into account if trend < significant
1117
        """
1118
        ret = '-'
1119
        if trend is None:
1120
            ret = ' '
1121
        elif trend > significant:
1122
            ret = unicode_message('ARROW_UP', self.args)
1123
        elif trend < -significant:
1124
            ret = unicode_message('ARROW_DOWN', self.args)
1125
        return ret
1126
1127
    def _check_decorator(fct):
1128
        """Check decorator for update method.
1129
1130
        It checks:
1131
        - if the plugin is enabled.
1132
        - if the refresh_timer is finished
1133
        """
1134
1135
        def wrapper(self, *args, **kw):
1136
            if self.is_enabled() and (self.refresh_timer.finished() or self.stats == self.get_init_value):
1137
                # Run the method
1138
                ret = fct(self, *args, **kw)
1139
                # Reset the timer
1140
                self.refresh_timer.set(self.get_refresh())
1141
                self.refresh_timer.reset()
1142
            else:
1143
                # No need to call the method
1144
                # Return the last result available
1145
                ret = self.stats
1146
            return ret
1147
1148
        return wrapper
1149
1150 View Code Duplication
    def _log_result_decorator(fct):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
1151
        """Log (DEBUG) the result of the function fct."""
1152
1153
        def wrapper(*args, **kw):
1154
            counter = Counter()
1155
            ret = fct(*args, **kw)
1156
            duration = counter.get()
1157
            class_name = args[0].__class__.__name__
1158
            class_module = args[0].__class__.__module__
1159
            logger.debug(f"{class_name} {class_module} {fct.__name__} return {ret} in {duration} seconds")
1160
            return ret
1161
1162
        return wrapper
1163
1164
    def _manage_rate(fct):
1165
        """Manage rate decorator for update method."""
1166
1167
        def compute_rate(self, stat, stat_previous):
1168
            if stat_previous is None:
1169
                return stat
1170
1171
            # 1) set _gauge for all the rate fields
1172
            # 2) compute the _rate_per_sec
1173
            # 3) set the original field to the delta between the current and the previous value
1174
            for field in self.fields_description:
1175
                # For all the field with the rate=True flag
1176
                if 'rate' in self.fields_description[field] and self.fields_description[field]['rate'] is True:
1177
                    # Create a new metadata with the gauge
1178
                    stat['time_since_update'] = self.time_since_last_update
1179
                    stat[field + '_gauge'] = stat[field]
1180
                    if field + '_gauge' in stat_previous and stat[field] and stat_previous[field + '_gauge']:
1181
                        # The stat becomes the delta between the current and the previous value
1182
                        stat[field] = stat[field] - stat_previous[field + '_gauge']
1183
                        # Compute the rate
1184
                        if self.time_since_last_update > 0:
1185
                            stat[field + '_rate_per_sec'] = stat[field] // self.time_since_last_update
1186
                        else:
1187
                            stat[field] = 0
1188
                    else:
1189
                        # Avoid strange rate at the first run
1190
                        stat[field] = 0
1191
            return stat
1192
1193
        def compute_rate_on_list(self, stats, stats_previous):
1194
            if stats_previous is None:
1195
                return stats
1196
1197
            for stat in stats:
1198
                olds = [i for i in stats_previous if i[self.get_key()] == stat[self.get_key()]]
1199
                if len(olds) == 1:
1200
                    compute_rate(self, stat, olds[0])
1201
            return stats
1202
1203
        def wrapper(self, *args, **kw):
1204
            # Call the father method
1205
            stats = fct(self, *args, **kw)
1206
1207
            # Get the time since the last update
1208
            self.time_since_last_update = getTimeSinceLastUpdate(self.plugin_name)
1209
1210
            # Compute the rate
1211
            if isinstance(stats, dict):
1212
                # Stats is a dict
1213
                compute_rate(self, stats, self.stats_previous)
1214
            elif isinstance(stats, list):
1215
                # Stats is a list
1216
                compute_rate_on_list(self, stats, self.stats_previous)
1217
1218
            # Memorized the current stats for next run
1219
            self.stats_previous = copy.deepcopy(stats)
1220
1221
            return stats
1222
1223
        return wrapper
1224
1225
    # Mandatory to call the decorator in child classes
1226
    _check_decorator = staticmethod(_check_decorator)
1227
    _log_result_decorator = staticmethod(_log_result_decorator)
1228
    _manage_rate = staticmethod(_manage_rate)
1229