Issues (48)

glances/plugins/plugin/model.py (2 issues)

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