Test Failed
Push — master ( ee826a...d9056e )
by Nicolas
03:09
created

GlancesPluginModel.get_stats_item()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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