GlancesPluginModel.get_limit_log()   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nop 3
dl 0
loc 9
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, dictlist_json_dumps, iterkeys, itervalues, json_dumps, 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 dictlist_json_dumps(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
        return {item: [i for i in self.get_raw() if 'key' in i and i[i['key']] == key][0].get(item)}
402
403
    def get_stats_item(self, item):
404
        """Return the stats object for a specific item in JSON format.
405
406
        Stats should be a list of dict (processlist, network...)
407
        """
408
        return dictlist_json_dumps(self.get_raw(), item)
409
410
    def get_raw_stats_value(self, item, value):
411
        """Return the stats object for a specific item=value.
412
413
        Return None if the item=value does not exist
414
        Return None if the item is not a list of dict
415
        """
416
        if not isinstance(self.get_raw(), list):
417
            return None
418
419
        if (not isinstance(value, int) and not isinstance(value, float)) and value.isdigit():
420
            value = int(value)
421
        try:
422
            return {value: [i for i in self.get_raw() if i[item] == value]}
423
        except (KeyError, ValueError) as e:
424
            logger.error(f"Cannot get item({item})=value({value}) ({e})")
425
            return None
426
427
    def get_stats_value(self, item, value):
428
        """Return the stats object for a specific item=value in JSON format.
429
430
        Stats should be a list of dict (processlist, network...)
431
        """
432
        rsv = self.get_raw_stats_value(item, value)
433
        if rsv is None:
434
            return None
435
        return json_dumps(rsv)
436
437
    def get_item_info(self, item, key, default=None):
438
        """Return the item info grabbed into self.fields_description."""
439
        if self.fields_description is None or item not in self.fields_description:
440
            return default
441
        return self.fields_description[item].get(key, default)
442
443
    def _build_field_decoration(self, field):
444
        """Return the field decoration.
445
446
        The decoration is used to display the field in the UI.
447
        """
448
        # Manage the decoration
449
        if self.fields_description and field in self.fields_description:
450
            if (
451
                self.fields_description[field].get('rate') is True
452
                and isinstance(self.stats, dict)
453
                and self.stats.get('time_since_update', 0) == 0
454
            ):
455
                return 'DEFAULT'
456
            if self.fields_description[field].get('log') is True:
457
                return self.get_alert_log(self.stats[field], header=field)
458
            if self.fields_description[field].get('alert') is True:
459
                return self.get_alert(self.stats[field], header=field)
460
        return 'DEFAULT'
461
462
    def _build_field_optional(self, field):
463
        """Return true if the field is optional."""
464
        if self.fields_description and field in self.fields_description:
465
            return self.fields_description[field].get('optional', False)
466
        return False
467
468
    def _build_view_for_field(self, field):
469
        value = {
470
            'decoration': self._build_field_decoration(field),
471
            'optional': self._build_field_optional(field),
472
            'additional': False,
473
            'splittable': False,
474
            'hidden': False,
475
        }
476
477
        # Manage the hidden feature
478
        # Allow to automatically hide fields when values is never different than 0
479
        # Refactoring done for #2929
480
        if not self.hide_zero:
481
            value['hidden'] = False
482
        elif field in self.views and 'hidden' in self.views[field]:
483
            value['hidden'] = self.views[field]['hidden']
484
            if field in self.hide_zero_fields and self.get_raw()[field] >= self.hide_threshold_bytes:
485
                value['hidden'] = False
486
        else:
487
            value['hidden'] = field in self.hide_zero_fields
488
489
        return value
490
491
    def update_views(self):
492
        """Update the stats views.
493
494
        The V of MVC
495
        A dict of dict with the needed information to display the stats.
496
        Example for the stat xxx:
497
        'xxx': {'decoration': 'DEFAULT',  >>> The decoration of the stats
498
                'optional': False,        >>> Is the stat optional
499
                'additional': False,      >>> Is the stat provide additional information
500
                'splittable': False,      >>> Is the stat can be cut (like process lon name)
501
                'hidden': False}          >>> Is the stats should be hidden in the UI
502
        """
503
        ret = {}
504
505
        if self.get_raw() is not None and isinstance(self.get_raw(), list) and self.get_key() is not None:
506
            # Stats are stored in a list of dict (ex: DISKIO, NETWORK, FS...)
507
            for i in self.get_raw():
508
                key = i[self.get_key()]
509
                ret[key] = {}
510
                for field in listkeys(i):
511
                    ret[key][field] = self._build_view_for_field(field)
512
        elif isinstance(self.get_raw(), dict) and self.get_raw() is not None:
513
            # Stats are stored in a dict (ex: CPU, LOAD...)
514
            for field in listkeys(self.get_raw()):
515
                ret[field] = self._build_view_for_field(field)
516
517
        self.views = ret
518
519
        return self.views
520
521
    def set_views(self, input_views):
522
        """Set the views to input_views."""
523
        self.views = input_views
524
525
    def reset_views(self):
526
        """Reset the views to input_views."""
527
        self.views = {}
528
529
    def get_views(self, item=None, key=None, option=None):
530
        """Return the views object.
531
532
        If key is None, return all the view for the current plugin
533
        else if option is None return the view for the specific key (all option)
534
        else return the view of the specific key/option
535
536
        Specify item if the stats are stored in a dict of dict (ex: NETWORK, FS...)
537
        """
538
        if item is None:
539
            item_views = self.views
540
        else:
541
            item_views = self.views[item]
542
543
        if key is None:
544
            return item_views
545
        if key not in item_views:
546
            return 'DEFAULT'
547
        if option is None:
548
            return item_views[key]
549
        if option in item_views[key]:
550
            return item_views[key][option]
551
        return 'DEFAULT'
552
553
    def get_json_views(self, item=None, key=None, option=None):
554
        """Return the views (in JSON)."""
555
        return json_dumps(self.get_views(item, key, option))
556
557
    def load_limits(self, config):
558
        """Load limits from the configuration file, if it exists."""
559
        # By default set the history length to 3 points per second during one day
560
        self._limits['history_size'] = 28800
561
562
        if not hasattr(config, 'has_section'):
563
            return False
564
565
        # Read the global section
566
        # TODO: not optimized because this section is loaded for each plugin...
567
        if config.has_section('global'):
568
            self._limits['history_size'] = config.get_float_value('global', 'history_size', default=28800)
569
            logger.debug("Load configuration key: {} = {}".format('history_size', self._limits['history_size']))
570
571
        # Read the plugin specific section
572
        if config.has_section(self.plugin_name):
573
            for level, _ in config.items(self.plugin_name):
574
                # Read limits
575
                limit = '_'.join([self.plugin_name, level])
576
                try:
577
                    self._limits[limit] = config.get_float_value(self.plugin_name, level)
578
                except ValueError:
579
                    self._limits[limit] = config.get_value(self.plugin_name, level).split(",")
580
                logger.debug(f"Load limit: {limit} = {self._limits[limit]}")
581
582
        return True
583
584
    @property
585
    def limits(self):
586
        """Return the limits object."""
587
        return self._limits
588
589
    @limits.setter
590
    def limits(self, input_limits):
591
        """Set the limits to input_limits."""
592
        self._limits = input_limits
593
594
    def set_refresh(self, value):
595
        """Set the plugin refresh rate"""
596
        self.set_limits('refresh', value)
597
598
    def get_refresh(self):
599
        """Return the plugin refresh time"""
600
        ret = self.get_limits(item='refresh')
601
        if ret is None:
602
            ret = self.args.time if hasattr(self.args, 'time') else 2
603
        return ret
604
605
    def get_refresh_time(self):
606
        """Return the plugin refresh time"""
607
        return self.get_refresh()
608
609
    def set_limits(self, item, value):
610
        """Set the limits object."""
611
        self._limits[f'{self.plugin_name}_{item}'] = value
612
613
    def get_limits(self, item=None):
614
        """Return the limits object."""
615
        if item is None:
616
            return self._limits
617
        return self._limits.get(f'{self.plugin_name}_{item}', None)
618
619
    def get_stats_action(self):
620
        """Return stats for the action.
621
622
        By default return all the stats.
623
        Can be overwrite by plugins implementation.
624
        For example, Docker will return self.stats['containers']
625
        """
626
        return self.stats
627
628
    def get_stat_name(self, header=""):
629
        """Return the stat name with an optional header"""
630
        ret = self.plugin_name
631
        if header != '':
632
            ret += '_' + header
633
        return ret
634
635
    def get_alert(
636
        self,
637
        current=0,
638
        minimum=0,
639
        maximum=100,
640
        highlight_zero=True,
641
        is_max=False,
642
        header="",
643
        action_key=None,
644
        log=False,
645
    ):
646
        """Return the alert status relative to a current value.
647
648
        Use this function for minor stats.
649
650
        If current < CAREFUL of max then alert = OK
651
        If current > CAREFUL of max then alert = CAREFUL
652
        If current > WARNING of max then alert = WARNING
653
        If current > CRITICAL of max then alert = CRITICAL
654
655
        If highlight=True than 0.0 is highlighted
656
657
        If defined 'header' is added between the plugin name and the status.
658
        Only useful for stats with several alert status.
659
660
        If defined, 'action_key' define the key for the actions.
661
        By default, the action_key is equal to the header.
662
663
        If log=True than add log if necessary
664
        elif log=False than do not log
665
        elif log=None than apply the config given in the conf file
666
        """
667
        # Manage 0 (0.0) value if highlight_zero is not True
668
        if not highlight_zero and current == 0:
669
            return 'DEFAULT'
670
671
        # Compute the %
672
        try:
673
            value = (current * 100) / maximum
674
        except ZeroDivisionError:
675
            return 'DEFAULT'
676
        except TypeError:
677
            return 'DEFAULT'
678
679
        # Build the stat_name
680
        stat_name = self.get_stat_name(header=header).lower()
681
682
        # Manage limits
683
        # If is_max is set then default style is set to MAX else default is set to OK
684
        ret = 'MAX' if is_max else 'OK'
685
686
        # Iter through limits
687
        critical = self.get_limit('critical', stat_name=stat_name)
688
        warning = self.get_limit('warning', stat_name=stat_name)
689
        careful = self.get_limit('careful', stat_name=stat_name)
690
        if critical and value >= critical:
691
            ret = 'CRITICAL'
692
        elif warning and value >= warning:
693
            ret = 'WARNING'
694
        elif careful and value >= careful:
695
            ret = 'CAREFUL'
696
        elif not careful and not warning and not critical:
697
            ret = 'DEFAULT'
698
        else:
699
            ret = 'OK'
700
701
        if current < minimum:
702
            ret = 'CAREFUL'
703
704
        # Manage log
705
        log_str = ""
706
        if self.get_limit_log(stat_name=stat_name, default_action=log):
707
            # Add _LOG to the return string
708
            # So stats will be highlighted with a specific color
709
            log_str = "_LOG"
710
            # Add the log to the events list
711
            glances_events.add(ret, stat_name.upper(), value)
712
713
        # Manage threshold
714
        self.manage_threshold(stat_name, ret)
715
716
        # Manage action
717
        self.manage_action(stat_name, ret.lower(), header, action_key)
718
719
        # Default is 'OK'
720
        return ret + log_str
721
722
    def filter_stats(self, stats):
723
        """Filter the stats to keep only the fields we want (the one defined in fields_description)."""
724
        if hasattr(stats, '_asdict'):
725
            return {k: v for k, v in stats._asdict().items() if k in self.fields_description}
726
        if isinstance(stats, dict):
727
            return {k: v for k, v in stats.items() if k in self.fields_description}
728
        if isinstance(stats, list):
729
            return [self.filter_stats(s) for s in stats]
730
        return stats
731
732
    def manage_threshold(self, stat_name, trigger):
733
        """Manage the threshold for the current stat."""
734
        glances_thresholds.add(stat_name, trigger)
735
736
    def manage_action(self, stat_name, trigger, header, action_key):
737
        """Manage the action for the current stat."""
738
        # Here is a command line for the current trigger ?
739
        command, repeat = self.get_limit_action(trigger, stat_name=stat_name)
740
        if not command and not repeat:
741
            # Reset the trigger
742
            self.actions.set(stat_name, trigger)
743
        else:
744
            # Define the action key for the stats dict
745
            # If not define, then it sets to header
746
            if action_key is None:
747
                action_key = header
748
749
            # A command line is available for the current alert
750
            # 1) Build the {{mustache}} dictionary
751
            if isinstance(self.get_stats_action(), list):
752
                # If the stats are stored in a list of dict (fs plugin for example)
753
                # Return the dict for the current header
754
                mustache_dict = {}
755
                for item in self.get_stats_action():
756
                    if item[self.get_key()] == action_key:
757
                        mustache_dict = item
758
                        break
759
            else:
760
                # Use the stats dict
761
                mustache_dict = self.get_stats_action()
762
            # 2) Run the action
763
            self.actions.run(stat_name, trigger, command, repeat, mustache_dict=mustache_dict)
764
765
    def get_alert_log(self, current=0, minimum=0, maximum=100, header="", action_key=None):
766
        """Get the alert log."""
767
        return self.get_alert(
768
            current=current, minimum=minimum, maximum=maximum, header=header, action_key=action_key, log=True
769
        )
770
771
    def is_limit(self, criticality, stat_name=""):
772
        """Return true if the criticality limit exist for the given stat_name"""
773
        return self.get_stat_name(stat_name).lower() + '_' + criticality in self._limits
774
775
    def get_limit(self, criticality=None, stat_name=""):
776
        """Return the limit value for the given criticality.
777
        If criticality is None, return the dict of all the limits."""
778
        if criticality is None:
779
            return self._limits
780
781
        # Get the limit for stat + header
782
        # Example: network_wlan0_rx_careful
783
        stat_name = stat_name.lower()
784
        if stat_name + '_' + criticality in self._limits:
785
            return self._limits[stat_name + '_' + criticality]
786
        if self.plugin_name + '_' + criticality in self._limits:
787
            return self._limits[self.plugin_name + '_' + criticality]
788
789
        return None
790
791
    def get_limit_action(self, criticality, stat_name=""):
792
        """Return the tuple (action, repeat) for the alert.
793
794
        - action is a command line
795
        - repeat is a bool
796
        """
797
        # Get the action for stat + header
798
        # Example: network_wlan0_rx_careful_action
799
        # Action key available ?
800
        ret = [
801
            (stat_name + '_' + criticality + '_action', False),
802
            (stat_name + '_' + criticality + '_action_repeat', True),
803
            (self.plugin_name + '_' + criticality + '_action', False),
804
            (self.plugin_name + '_' + criticality + '_action_repeat', True),
805
        ]
806
        for r in ret:
807
            if r[0] in self._limits:
808
                return self._limits[r[0]], r[1]
809
810
        # No key found, return None
811
        return None, None
812
813
    def get_limit_log(self, stat_name, default_action=False):
814
        """Return the log tag for the alert."""
815
        # Get the log tag for stat + header
816
        # Example: network_wlan0_rx_log
817
        if stat_name + '_log' in self._limits:
818
            return self._limits[stat_name + '_log'][0].lower() == 'true'
819
        if self.plugin_name + '_log' in self._limits:
820
            return self._limits[self.plugin_name + '_log'][0].lower() == 'true'
821
        return default_action
822
823
    def get_conf_value(self, value, header="", plugin_name=None, default=[]):
824
        """Return the configuration (header_) value for the current plugin.
825
826
        ...or the one given by the plugin_name var.
827
        """
828
        if plugin_name is None:
829
            # If not default use the current plugin name
830
            plugin_name = self.plugin_name
831
832
        if header != "":
833
            # Add the header
834
            plugin_name = plugin_name + '_' + header
835
836
        try:
837
            return self._limits[plugin_name + '_' + value]
838
        except KeyError:
839
            return default
840
841
    def is_show(self, value, header=""):
842
        """Return True if the value is in the show configuration list.
843
844
        If the show value is empty, return True (show by default)
845
846
        The show configuration list is defined in the glances.conf file.
847
        It is a comma-separated list of regexp.
848
        Example for diskio:
849
        show=sda.*
850
        """
851
        # TODO: possible optimisation: create a re.compile list
852
        # nguuuquaaa: no need a compile list, the re module caches the compiling itself
853
        return any(re.fullmatch(i, value, re.I) for i in self.get_conf_value('show', header=header))
854
855
    def is_hide(self, value, header=""):
856
        """Return True if the value is in the hide configuration list.
857
858
        The hide configuration list is defined in the glances.conf file.
859
        It is a comma-separated list of regexp.
860
        Example for diskio:
861
        hide=sda2,sda5,loop.*
862
        """
863
        # TODO: possible optimisation: create a re.compile list
864
        # nguuuquaaa: no need a compile list, the re module caches the compiling itself
865
        return any(re.fullmatch(i, value, re.I) for i in self.get_conf_value('hide', header=header))
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 is_display_any(self, *values, header=""):
874
        """Return True if any of the values should be displayed in the UI"""
875
        if self.get_conf_value('show', header=header) != []:
876
            return any(self.is_show(value, header=header) for value in values)
877
        return not any(self.is_hide(value, header=header) for value in values)
878
879
    def read_alias(self):
880
        if self.plugin_name + '_' + 'alias' in self._limits:
881
            return {i.split(':')[0].lower(): i.split(':')[1] for i in self._limits[self.plugin_name + '_' + 'alias']}
882
        return {}
883
884
    def has_alias(self, header):
885
        """Return the alias name for the relative header it it exists otherwise None."""
886
        return self.alias.get(header, None)
887
888
    def msg_curse(self, args=None, max_width=None):
889
        """Return default string to display in the curse interface."""
890
        return [self.curse_add_line(str(self.stats))]
891
892
    def get_stats_display(self, args=None, max_width=None):
893
        """Return a dict with all the information needed to display the stat.
894
895
        key     | description
896
        ----------------------------
897
        display | Display the stat (True or False)
898
        msgdict | Message to display (list of dict [{ 'msg': msg, 'decoration': decoration } ... ])
899
        align   | Message position
900
        """
901
        display_curse = False
902
903
        if hasattr(self, 'display_curse'):
904
            display_curse = self.display_curse
905
        if hasattr(self, 'align'):
906
            align_curse = self._align
907
908
        if max_width is not None:
909
            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 905 is False. Are you sure this can never be the case?
Loading history...
910
        else:
911
            ret = {'display': display_curse, 'msgdict': self.msg_curse(args), 'align': align_curse}
912
913
        return ret
914
915
    def curse_add_line(self, msg, decoration="DEFAULT", optional=False, additional=False, splittable=False):
916
        """Return a dict with.
917
918
        Where:
919
            msg: string
920
            decoration:
921
                DEFAULT: no decoration
922
                UNDERLINE: underline
923
                BOLD: bold
924
                TITLE: for stat title
925
                PROCESS: for process name
926
                STATUS: for process status
927
                CPU_TIME: for process cpu time
928
                OK: Value is OK and non logged
929
                OK_LOG: Value is OK and logged
930
                CAREFUL: Value is CAREFUL and non logged
931
                CAREFUL_LOG: Value is CAREFUL and logged
932
                WARNING: Value is WARNING and non logged
933
                WARNING_LOG: Value is WARNING and logged
934
                CRITICAL: Value is CRITICAL and non logged
935
                CRITICAL_LOG: Value is CRITICAL and logged
936
            optional: True if the stat is optional (display only if space is available)
937
            additional: True if the stat is additional (display only if space is available after optional)
938
            spittable: Line can be split to fit on the screen (default is not)
939
        """
940
        return {
941
            'msg': msg,
942
            'decoration': decoration,
943
            'optional': optional,
944
            'additional': additional,
945
            'splittable': splittable,
946
        }
947
948
    def curse_new_line(self):
949
        """Go to a new line."""
950
        return self.curse_add_line('\n')
951
952
    def curse_add_stat(self, key, width=None, header='', display_key=True, separator='', trailer=''):
953
        """Return a list of dict messages with the 'key: value' result
954
955
          <=== width ===>
956
        __key     : 80.5%__
957
        | |       | |    |_ trailer
958
        | |       | |_ self.stats[key]
959
        | |       |_ separator
960
        | |_ 'short_name' description or key or nothing if display_key is True
961
        |_ header
962
963
        Instead of:
964
            msg = '  {:8}'.format('idle:')
965
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
966
            msg = '{:5.1f}%'.format(self.stats['idle'])
967
            ret.append(self.curse_add_line(msg, optional=self.get_views(key='idle', option='optional')))
968
969
        Use:
970
            ret.extend(self.curse_add_stat('idle', width=15, header='  '))
971
972
        """
973
        if key not in self.stats:
974
            return []
975
976
        # Check if a shortname is defined
977
        if not display_key:
978
            key_name = ''
979
        elif key in self.fields_description and 'short_name' in self.fields_description[key]:
980
            key_name = self.fields_description[key]['short_name']
981
        else:
982
            key_name = key
983
984
        # Check if unit is defined and get the short unit char in the unit_sort dict
985
        if (
986
            key in self.fields_description
987
            and 'unit' in self.fields_description[key]
988
            and self.fields_description[key]['unit'] in fields_unit_short
989
        ):
990
            # Get the shortname
991
            unit_short = fields_unit_short[self.fields_description[key]['unit']]
992
        else:
993
            unit_short = ''
994
995
        # Check if unit is defined and get the unit type unit_type dict
996
        if (
997
            key in self.fields_description
998
            and 'unit' in self.fields_description[key]
999
            and self.fields_description[key]['unit'] in fields_unit_type
1000
        ):
1001
            # Get the shortname
1002
            unit_type = fields_unit_type[self.fields_description[key]['unit']]
1003
        else:
1004
            unit_type = 'float'
1005
1006
        # Is it a rate ? Yes, get the pre-computed rate value
1007
        if key in self.fields_description and self.fields_description[key].get('rate', False) is True:
1008
            value = self.stats.get(key + '_rate_per_sec', None)
1009
        else:
1010
            value = self.stats.get(key, None)
1011
1012
        if width is None:
1013
            msg_item = header + f'{key_name}' + separator
1014
            msg_template_float = '{:.1f}{}'
1015
            msg_template = '{}{}'
1016
        else:
1017
            # Define the size of the message
1018
            # item will be on the left
1019
            # value will be on the right
1020
            msg_item = header + '{:{width}}'.format(key_name, width=width - 7) + separator
1021
            msg_template_float = '{:5.1f}{}'
1022
            msg_template = '{:>5}{}'
1023
1024
        if value is None:
1025
            msg_value = msg_template.format('-', '')
1026
        elif unit_type == 'float':
1027
            msg_value = msg_template_float.format(value, unit_short)
1028
        elif 'min_symbol' in self.fields_description[key]:
1029
            msg_value = msg_template.format(
1030
                self.auto_unit(int(value), min_symbol=self.fields_description[key]['min_symbol']), unit_short
1031
            )
1032
        else:
1033
            msg_value = msg_template.format(int(value), unit_short)
1034
1035
        # Add the trailer
1036
        msg_value = msg_value + trailer
1037
1038
        decoration = self.get_views(key=key, option='decoration') if value is not None else 'DEFAULT'
1039
        optional = self.get_views(key=key, option='optional')
1040
1041
        return [
1042
            self.curse_add_line(msg_item, optional=optional),
1043
            self.curse_add_line(msg_value, decoration=decoration, optional=optional),
1044
        ]
1045
1046
    @property
1047
    def align(self):
1048
        """Get the curse align."""
1049
        return self._align
1050
1051
    @align.setter
1052
    def align(self, value):
1053
        """Set the curse align.
1054
1055
        value: left, right, bottom.
1056
        """
1057
        self._align = value
1058
1059
    def auto_unit(self, number, low_precision=False, min_symbol='K', none_symbol='-'):
1060
        """Make a nice human-readable string out of number.
1061
1062
        Number of decimal places increases as quantity approaches 1.
1063
        CASE: 613421788        RESULT:       585M low_precision:       585M
1064
        CASE: 5307033647       RESULT:      4.94G low_precision:       4.9G
1065
        CASE: 44968414685      RESULT:      41.9G low_precision:      41.9G
1066
        CASE: 838471403472     RESULT:       781G low_precision:       781G
1067
        CASE: 9683209690677    RESULT:      8.81T low_precision:       8.8T
1068
        CASE: 1073741824       RESULT:      1024M low_precision:      1024M
1069
        CASE: 1181116006       RESULT:      1.10G low_precision:       1.1G
1070
1071
        :low_precision: returns less decimal places potentially (default is False)
1072
                        sacrificing precision for more readability.
1073
        :min_symbol: Do not approach if number < min_symbol (default is K)
1074
        :decimal_count: if set, force the number of decimal number (default is None)
1075
        """
1076
        if number is None:
1077
            return none_symbol
1078
        symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
1079
        if min_symbol in symbols:
1080
            symbols = symbols[symbols.index(min_symbol) :]
1081
        prefix = {
1082
            'Y': 1208925819614629174706176,
1083
            'Z': 1180591620717411303424,
1084
            'E': 1152921504606846976,
1085
            'P': 1125899906842624,
1086
            'T': 1099511627776,
1087
            'G': 1073741824,
1088
            'M': 1048576,
1089
            'K': 1024,
1090
        }
1091
1092
        if number == 0:
1093
            # Avoid 0.0
1094
            return '0'
1095
1096
        for symbol in reversed(symbols):
1097
            value = float(number) / prefix[symbol]
1098
            if value > 1:
1099
                decimal_precision = 0
1100
                if value < 10:
1101
                    decimal_precision = 2
1102
                elif value < 100:
1103
                    decimal_precision = 1
1104
                if low_precision:
1105
                    if symbol in 'MK':
1106
                        decimal_precision = 0
1107
                    else:
1108
                        decimal_precision = min(1, decimal_precision)
1109
                elif symbol in 'K':
1110
                    decimal_precision = 0
1111
                return '{:.{decimal}f}{symbol}'.format(value, decimal=decimal_precision, symbol=symbol)
1112
        return f'{number!s}'
1113
1114
    def trend_msg(self, trend, significant=1):
1115
        """Return the trend message.
1116
1117
        Do not take into account if trend < significant
1118
        """
1119
        ret = '-'
1120
        if trend is None:
1121
            ret = ' '
1122
        elif trend > significant:
1123
            ret = unicode_message('ARROW_UP', self.args)
1124
        elif trend < -significant:
1125
            ret = unicode_message('ARROW_DOWN', self.args)
1126
        return ret
1127
1128
    def _check_decorator(fct):
1129
        """Check decorator for update method.
1130
1131
        It checks:
1132
        - if the plugin is enabled.
1133
        - if the refresh_timer is finished
1134
        """
1135
1136
        def wrapper(self, *args, **kw):
1137
            if self.is_enabled() and (self.refresh_timer.finished() or self.stats == self.get_init_value):
1138
                # Run the method
1139
                ret = fct(self, *args, **kw)
1140
                # Reset the timer
1141
                self.refresh_timer.set(self.get_refresh())
1142
                self.refresh_timer.reset()
1143
            else:
1144
                # No need to call the method
1145
                # Return the last result available
1146
                ret = self.stats
1147
            return ret
1148
1149
        return wrapper
1150
1151 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...
1152
        """Log (DEBUG) the result of the function fct."""
1153
1154
        def wrapper(*args, **kw):
1155
            counter = Counter()
1156
            ret = fct(*args, **kw)
1157
            duration = counter.get()
1158
            class_name = args[0].__class__.__name__
1159
            class_module = args[0].__class__.__module__
1160
            logger.debug(f"{class_name} {class_module} {fct.__name__} return {ret} in {duration} seconds")
1161
            return ret
1162
1163
        return wrapper
1164
1165
    def _manage_rate(fct):
1166
        """Manage rate decorator for update method."""
1167
1168
        def compute_rate(self, stat, stat_previous):
1169
            if stat_previous is None:
1170
                return stat
1171
1172
            # 1) set _gauge for all the rate fields
1173
            # 2) compute the _rate_per_sec
1174
            # 3) set the original field to the delta between the current and the previous value
1175
            for field in self.fields_description:
1176
                # For all the field with the rate=True flag
1177
                # if 'rate' in self.fields_description[field] and self.fields_description[field]['rate'] is True:
1178
                if self.fields_description[field].get('rate', False):
1179
                    # Create a new metadata with the gauge
1180
                    stat['time_since_update'] = self.time_since_last_update
1181
                    stat[field + '_gauge'] = stat[field]
1182
                    if field + '_gauge' in stat_previous and stat[field] and stat_previous[field + '_gauge']:
1183
                        # The stat becomes the delta between the current and the previous value
1184
                        stat[field] = stat[field] - stat_previous[field + '_gauge']
1185
                        # Compute the rate
1186
                        if self.time_since_last_update > 0:
1187
                            stat[field + '_rate_per_sec'] = stat[field] // self.time_since_last_update
1188
                        else:
1189
                            stat[field] = None
1190
                            stat[field + '_rate_per_sec'] = None
1191
                    else:
1192
                        # Avoid strange rate at the first run
1193
                        stat[field] = None
1194
                        stat[field + '_rate_per_sec'] = None
1195
            return stat
1196
1197
        def compute_rate_on_list(self, stats, stats_previous):
1198
            if stats_previous is None:
1199
                return stats
1200
1201
            for stat in stats:
1202
                olds = [i for i in stats_previous if i[self.get_key()] == stat[self.get_key()]]
1203
                if len(olds) == 1:
1204
                    compute_rate(self, stat, olds[0])
1205
            return stats
1206
1207
        def wrapper(self, *args, **kw):
1208
            # Call the father method
1209
            stats = fct(self, *args, **kw)
1210
1211
            # Get the time since the last update
1212
            self.time_since_last_update = getTimeSinceLastUpdate(self.plugin_name)
1213
1214
            # Compute the rate
1215
            if isinstance(stats, dict):
1216
                # Stats is a dict
1217
                compute_rate(self, stats, self.stats_previous)
1218
            elif isinstance(stats, list):
1219
                # Stats is a list
1220
                compute_rate_on_list(self, stats, self.stats_previous)
1221
1222
            # Memorized the current stats for next run
1223
            self.stats_previous = copy.deepcopy(stats)
1224
1225
            return stats
1226
1227
        return wrapper
1228
1229
    # Mandatory to call the decorator in child classes
1230
    _check_decorator = staticmethod(_check_decorator)
1231
    _log_result_decorator = staticmethod(_log_result_decorator)
1232
    _manage_rate = staticmethod(_manage_rate)
1233