Test Failed
Push — master ( b164e3...65b0d8 )
by Nicolas
03:39
created

GlancesPlugin.is_disabled()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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