Test Failed
Push — develop ( 131d0a...172661 )
by Nicolas
03:33
created

GlancesPluginModel.get_api()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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