Test Failed
Push — master ( 128504...ccd56b )
by Nicolas
03:24
created

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