Test Failed
Push — master ( 69b639...e7fa0a )
by Nicolas
04:05 queued 01:05
created

glances.plugins.glances_plugin   F

Complexity

Total Complexity 196

Size/Duplication

Total Lines 1075
Duplicated Lines 3.07 %

Importance

Changes 0
Metric Value
eloc 551
dl 33
loc 1075
rs 2
c 0
b 0
f 0
wmc 196

65 Methods

Rating   Name   Duplication   Size   Complexity  
A GlancesPlugin.reset() 0 6 1
A GlancesPlugin.__repr__() 0 3 1
A GlancesPlugin.__init__() 0 71 2
A GlancesPlugin.__str__() 0 3 1
A GlancesPlugin.get_init_value() 0 3 1
A GlancesPlugin.is_disable() 0 3 1
A GlancesPlugin.exit() 0 3 1
A GlancesPlugin.is_enable() 0 9 3
A GlancesPlugin.get_key() 0 3 1
A GlancesPlugin._json_dumps() 0 9 2
A GlancesPlugin.get_items_history_list() 0 3 1
A GlancesPlugin.short_system_name() 0 4 1
A GlancesPlugin.reset_stats_history() 0 6 2
A GlancesPlugin.get_json_history() 0 16 3
B GlancesPlugin.update_stats_history() 0 27 7
A GlancesPlugin.get_trend() 0 11 3
A GlancesPlugin.input_method() 0 4 1
A GlancesPlugin.get_export_history() 0 3 1
B GlancesPlugin.get_stats_history() 16 23 6
A GlancesPlugin.init_stats_history() 0 6 2
A GlancesPlugin.get_raw_history() 0 15 3
A GlancesPlugin.history_enable() 0 4 1
A GlancesPlugin.get_refresh_time() 0 3 1
A GlancesPlugin.get_limit_action() 0 19 3
A GlancesPlugin.get_conf_value() 0 17 4
A GlancesPlugin.manage_threshold() 0 5 1
A GlancesPlugin.is_limit() 0 6 2
B GlancesPlugin.sorted_stats() 0 13 7
A GlancesPlugin.is_show() 0 14 2
A GlancesPlugin.get_limit_log() 0 17 3
A GlancesPlugin.set_stats() 0 3 1
A GlancesPlugin.get_stat_name() 0 6 2
A GlancesPlugin.set_views() 0 3 1
A GlancesPlugin.has_alias() 0 8 2
A GlancesPlugin.trend_msg() 0 13 4
A GlancesPlugin.get_json_views() 0 3 1
A GlancesPlugin.get_stats() 0 3 1
A GlancesPlugin.get_views() 0 24 5
A GlancesPlugin.get_refresh() 0 6 2
C GlancesPlugin.get_stats_snmp() 0 54 11
A GlancesPlugin._check_decorator() 0 19 4
A GlancesPlugin.get_limits() 0 6 2
A GlancesPlugin.get_stats_action() 0 8 1
F GlancesPlugin.update_views_hidden() 0 42 15
A GlancesPlugin.limits() 0 4 1
A GlancesPlugin.get_export() 0 3 1
B GlancesPlugin.load_limits() 0 26 6
A GlancesPlugin.is_hide() 0 10 1
A GlancesPlugin.get_stats_display() 0 26 4
A GlancesPlugin.get_alert_log() 0 13 1
C GlancesPlugin.auto_unit() 0 51 9
A GlancesPlugin.set_limits() 0 3 1
A GlancesPlugin._log_result_decorator() 0 13 1
D GlancesPlugin.get_alert() 0 77 12
A GlancesPlugin.curse_new_line() 0 3 1
A GlancesPlugin.get_raw() 0 3 1
A GlancesPlugin.get_limit() 0 15 2
A GlancesPlugin.msg_curse() 0 3 1
C GlancesPlugin.update_views() 0 44 11
A GlancesPlugin.align() 0 4 1
A GlancesPlugin.get_stats_item() 17 22 5
A GlancesPlugin.get_stats_value() 0 16 4
B GlancesPlugin.manage_action() 0 35 7
A GlancesPlugin.set_refresh() 0 3 1
A GlancesPlugin.curse_add_line() 0 33 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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

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

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