GlancesPlugin.update_views_hidden()   F
last analyzed

Complexity

Conditions 15

Size

Total Lines 39
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 18
nop 1
dl 0
loc 39
rs 2.9998
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like glances.plugins.glances_plugin.GlancesPlugin.update_views_hidden() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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