Test Failed
Push — develop ( f1ebbc...004288 )
by Nicolas
02:12
created

GlancesPlugin.get_stats_history()   A

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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