Completed
Push — master ( 26c94a...b848ef )
by Nicolas
01:11
created

glances/plugins/glances_plugin.py (2 issues)

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2017 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
from operator import itemgetter
29
30
from glances.compat import iterkeys, itervalues, listkeys, map
31
from glances.actions import GlancesActions
32
from glances.history import GlancesHistory
33
from glances.logger import logger
34
from glances.logs import glances_logs
35
36
37
class GlancesPlugin(object):
38
39
    """Main class for Glances plugin."""
40
41
    def __init__(self, args=None, items_history_list=None):
42
        """Init the plugin of plugins class."""
43
        # Plugin name (= module name without glances_)
44
        self.plugin_name = self.__class__.__module__[len('glances_'):]
45
        # logger.debug("Init plugin %s" % self.plugin_name)
46
47
        # Init the args
48
        self.args = args
49
50
        # Init the default alignement (for curses)
51
        self._align = 'left'
52
53
        # Init the input method
54
        self._input_method = 'local'
55
        self._short_system_name = None
56
57
        # Init the stats list
58
        self.stats = None
59
60
        # Init the history list
61
        self.items_history_list = items_history_list
62
        self.stats_history = self.init_stats_history()
63
64
        # Init the limits dictionnary
65
        self._limits = dict()
66
67
        # Init the actions
68
        self.actions = GlancesActions(args=args)
69
70
        # Init the views
71
        self.views = dict()
72
73
    def exit(self):
74
        """Method to be called when Glances exit"""
75
        logger.debug("Stop the {} plugin".format(self.plugin_name))
76
77
    def __repr__(self):
78
        """Return the raw stats."""
79
        return self.stats
80
81
    def __str__(self):
82
        """Return the human-readable stats."""
83
        return str(self.stats)
84
85
    def get_key(self):
86
        """Return the key of the list."""
87
        return None
88
89
    def is_enable(self):
90
        """Return true if plugin is enabled"""
91
        try:
92
            d = getattr(self.args, 'disable_' + self.plugin_name)
93
        except AttributeError:
94
            return True
95
        else:
96
            return d is False
97
98
    def is_disable(self):
99
        """Return true if plugin is disabled"""
100
        return not self.is_enable()
101
102
    def _json_dumps(self, d):
103
        """Return the object 'd' in a JSON format
104
        Manage the issue #815 for Windows OS"""
105
        try:
106
            return json.dumps(d)
107
        except UnicodeDecodeError:
108
            return json.dumps(d, ensure_ascii=False)
109
110
    def _history_enable(self):
111
        return self.args is not None and not self.args.disable_history and self.get_items_history_list() is not None
112
113
    def init_stats_history(self):
114
        """Init the stats history (dict of GlancesAttribute)."""
115
        if self._history_enable():
116
            init_list = [a['name'] for a in self.get_items_history_list()]
117
            logger.debug("Stats history activated for plugin {} (items: {})".format(self.plugin_name, init_list))
118
        return GlancesHistory()
119
120
    def reset_stats_history(self):
121
        """Reset the stats history (dict of GlancesAttribute)."""
122
        if self._history_enable():
123
            reset_list = [a['name'] for a in self.get_items_history_list()]
124
            logger.debug("Reset history for plugin {} (items: {})".format(self.plugin_name, reset_list))
125
            self.stats_history.reset()
126
127
    def update_stats_history(self):
128
        """Update stats history."""
129
        # If the plugin data is a dict, the dict's key should be used
130
        if self.get_key() is None:
131
            item_name = ''
132
        else:
133
            item_name = self.get_key()
134
        # Build the history
135
        if self.stats and self._history_enable():
136
            for i in self.get_items_history_list():
137
                if isinstance(self.stats, list):
138
                    # Stats is a list of data
139
                    # Iter throught it (for exemple, iter throught network
140
                    # interface)
141
                    for l in self.stats:
142
                        self.stats_history.add(
143
                            l[item_name] + '_' + i['name'],
144
                            l[i['name']],
145
                            description=i['description'],
146
                            history_max_size=self._limits['history_size'])
147
                else:
148
                    # Stats is not a list
149
                    # Add the item to the history directly
150
                    self.stats_history.add(i['name'],
151
                                           self.stats[i['name']],
152
                                           description=i['description'],
153
                                           history_max_size=self._limits['history_size'])
154
155
    def get_items_history_list(self):
156
        """Return the items history list."""
157
        return self.items_history_list
158
159
    def get_raw_history(self, item=None):
160
        """Return
161
        - the stats history (dict of list) if item is None
162
        - the stats history for the given item (list) instead
163
        - None if item did not exist in the history"""
164
        s = self.stats_history.get()
165
        if item is None:
166
            return s
167
        else:
168
            if item in s:
169
                return s[item]
170
            else:
171
                return None
172
173
    def get_json_history(self, item=None, nb=0):
174
        """Return:
175
        - the stats history (dict of list) if item is None
176
        - the stats history for the given item (list) instead
177
        - None if item did not exist in the history
178
        Limit to lasts nb items (all if nb=0)"""
179
        s = self.stats_history.get_json(nb=nb)
180
        if item is None:
181
            return s
182
        else:
183
            if item in s:
184
                return s[item]
185
            else:
186
                return None
187
188
    def get_export_history(self, item=None):
189
        """Return the stats history object to export.
190
        See get_raw_history for a full description"""
191
        return self.get_raw_history(item=item)
192
193
    def get_stats_history(self, item=None, nb=0):
194
        """Return the stats history as a JSON object (dict or None).
195
        Limit to lasts nb items (all if nb=0)"""
196
        s = self.get_json_history(nb=nb)
197
198
        if item is None:
199
            return self._json_dumps(s)
200
201
        if isinstance(s, dict):
202 View Code Duplication
            try:
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
203
                return self._json_dumps({item: s[item]})
204
            except KeyError as e:
205
                logger.error("Cannot get item history {} ({})".format(item, e))
206
                return None
207
        elif isinstance(s, list):
208
            try:
209
                # Source:
210
                # http://stackoverflow.com/questions/4573875/python-get-index-of-dictionary-item-in-list
211
                return self._json_dumps({item: map(itemgetter(item), s)})
212
            except (KeyError, ValueError) as e:
213
                logger.error("Cannot get item history {} ({})".format(item, e))
214
                return None
215
        else:
216
            return None
217
218
    @property
219
    def input_method(self):
220
        """Get the input method."""
221
        return self._input_method
222
223
    @input_method.setter
224
    def input_method(self, input_method):
225
        """Set the input method.
226
227
        * local: system local grab (psutil or direct access)
228
        * snmp: Client server mode via SNMP
229
        * glances: Client server mode via Glances API
230
        """
231
        self._input_method = input_method
232
233
    @property
234
    def short_system_name(self):
235
        """Get the short detected OS name (SNMP)."""
236
        return self._short_system_name
237
238
    @short_system_name.setter
239
    def short_system_name(self, short_name):
240
        """Set the short detected OS name (SNMP)."""
241
        self._short_system_name = short_name
242
243
    def set_stats(self, input_stats):
244
        """Set the stats to input_stats."""
245
        self.stats = input_stats
246
247
    def get_stats_snmp(self, bulk=False, snmp_oid=None):
248
        """Update stats using SNMP.
249
250
        If bulk=True, use a bulk request instead of a get request.
251
        """
252
        snmp_oid = snmp_oid or {}
253
254
        from glances.snmp import GlancesSNMPClient
255
256
        # Init the SNMP request
257
        clientsnmp = GlancesSNMPClient(host=self.args.client,
258
                                       port=self.args.snmp_port,
259
                                       version=self.args.snmp_version,
260
                                       community=self.args.snmp_community)
261
262
        # Process the SNMP request
263
        ret = {}
264
        if bulk:
265
            # Bulk request
266
            snmpresult = clientsnmp.getbulk_by_oid(0, 10, itervalues(*snmp_oid))
267
268
            if len(snmp_oid) == 1:
269
                # Bulk command for only one OID
270
                # Note: key is the item indexed but the OID result
271
                for item in snmpresult:
272
                    if iterkeys(item)[0].startswith(itervalues(snmp_oid)[0]):
273
                        ret[iterkeys(snmp_oid)[0] + iterkeys(item)
274
                            [0].split(itervalues(snmp_oid)[0])[1]] = itervalues(item)[0]
275
            else:
276
                # Build the internal dict with the SNMP result
277
                # Note: key is the first item in the snmp_oid
278
                index = 1
279
                for item in snmpresult:
280
                    item_stats = {}
281
                    item_key = None
282
                    for key in iterkeys(snmp_oid):
283
                        oid = snmp_oid[key] + '.' + str(index)
284
                        if oid in item:
285
                            if item_key is None:
286
                                item_key = item[oid]
287
                            else:
288
                                item_stats[key] = item[oid]
289
                    if item_stats:
290
                        ret[item_key] = item_stats
291
                    index += 1
292
        else:
293
            # Simple get request
294
            snmpresult = clientsnmp.get_by_oid(itervalues(*snmp_oid))
295
296
            # Build the internal dict with the SNMP result
297
            for key in iterkeys(snmp_oid):
298
                ret[key] = snmpresult[snmp_oid[key]]
299
300
        return ret
301
302
    def get_raw(self):
303
        """Return the stats object."""
304
        return self.stats
305
306
    def get_export(self):
307
        """Return the stats object to export."""
308
        return self.get_raw()
309
310
    def get_stats(self):
311
        """Return the stats object in JSON format."""
312
        return self._json_dumps(self.stats)
313
314
    def get_stats_item(self, item):
315
        """Return the stats object for a specific item in JSON format.
316
317
        Stats should be a list of dict (processlist, network...)
318
        """
319
        if isinstance(self.stats, dict):
320
            try:
321
                return self._json_dumps({item: self.stats[item]})
322
            except KeyError as e:
323
                logger.error("Cannot get item {} ({})".format(item, e))
324
                return None
325
        elif isinstance(self.stats, list):
326
            try:
327
                # Source:
328
                # http://stackoverflow.com/questions/4573875/python-get-index-of-dictionary-item-in-list
329
                return self._json_dumps({item: map(itemgetter(item), self.stats)})
330
            except (KeyError, ValueError) as e:
331 View Code Duplication
                logger.error("Cannot get item {} ({})".format(item, e))
0 ignored issues
show
This code seems to be duplicated in your project.
Loading history...
332
                return None
333
        else:
334
            return None
335
336
    def get_stats_value(self, item, value):
337
        """Return the stats object for a specific item=value in JSON format.
338
339
        Stats should be a list of dict (processlist, network...)
340
        """
341
        if not isinstance(self.stats, list):
342
            return None
343
        else:
344
            if value.isdigit():
345
                value = int(value)
346
            try:
347
                return self._json_dumps({value: [i for i in self.stats if i[item] == value]})
348
            except (KeyError, ValueError) as e:
349
                logger.error(
350
                    "Cannot get item({})=value({}) ({})".format(item, value, e))
351
                return None
352
353
    def update_views(self):
354
        """Default builder fo the stats views.
355
356
        The V of MVC
357
        A dict of dict with the needed information to display the stats.
358
        Example for the stat xxx:
359
        'xxx': {'decoration': 'DEFAULT',
360
                'optional': False,
361
                'additional': False,
362
                'splittable': False}
363
        """
364
        ret = {}
365
366
        if (isinstance(self.get_raw(), list) and
367
                self.get_raw() is not None and
368
                self.get_key() is not None):
369
            # Stats are stored in a list of dict (ex: NETWORK, FS...)
370
            for i in self.get_raw():
371
                ret[i[self.get_key()]] = {}
372
                for key in listkeys(i):
373
                    value = {'decoration': 'DEFAULT',
374
                             'optional': False,
375
                             'additional': False,
376
                             'splittable': False}
377
                    ret[i[self.get_key()]][key] = value
378
        elif isinstance(self.get_raw(), dict) and self.get_raw() is not None:
379
            # Stats are stored in a dict (ex: CPU, LOAD...)
380
            for key in listkeys(self.get_raw()):
381
                value = {'decoration': 'DEFAULT',
382
                         'optional': False,
383
                         'additional': False,
384
                         'splittable': False}
385
                ret[key] = value
386
387
        self.views = ret
388
389
        return self.views
390
391
    def set_views(self, input_views):
392
        """Set the views to input_views."""
393
        self.views = input_views
394
395
    def get_views(self, item=None, key=None, option=None):
396
        """Return the views object.
397
398
        If key is None, return all the view for the current plugin
399
        else if option is None return the view for the specific key (all option)
400
        else return the view fo the specific key/option
401
402
        Specify item if the stats are stored in a dict of dict (ex: NETWORK, FS...)
403
        """
404
        if item is None:
405
            item_views = self.views
406
        else:
407
            item_views = self.views[item]
408
409
        if key is None:
410
            return item_views
411
        else:
412
            if option is None:
413
                return item_views[key]
414
            else:
415
                if option in item_views[key]:
416
                    return item_views[key][option]
417
                else:
418
                    return 'DEFAULT'
419
420
    def load_limits(self, config):
421
        """Load limits from the configuration file, if it exists."""
422
423
        # By default set the history length to 3 points per second during one day
424
        self._limits['history_size'] = 28800
425
426
        if not hasattr(config, 'has_section'):
427
            return False
428
429
        # Read the global section
430
        if config.has_section('global'):
431
            self._limits['history_size'] = config.get_float_value('global', 'history_size', default=28800)
432
            logger.debug("Load configuration key: {} = {}".format('history_size', self._limits['history_size']))
433
434
        # Read the plugin specific section
435
        if config.has_section(self.plugin_name):
436
            for level, _ in config.items(self.plugin_name):
437
                # Read limits
438
                limit = '_'.join([self.plugin_name, level])
439
                try:
440
                    self._limits[limit] = config.get_float_value(self.plugin_name, level)
441
                except ValueError:
442
                    self._limits[limit] = config.get_value(self.plugin_name, level).split(",")
443
                logger.debug("Load limit: {} = {}".format(limit, self._limits[limit]))
444
445
        return True
446
447
    @property
448
    def limits(self):
449
        """Return the limits object."""
450
        return self._limits
451
452
    @limits.setter
453
    def limits(self, input_limits):
454
        """Set the limits to input_limits."""
455
        self._limits = input_limits
456
457
    def get_stats_action(self):
458
        """Return stats for the action
459
        By default return all the stats.
460
        Can be overwrite by plugins implementation.
461
        For example, Docker will return self.stats['containers']"""
462
        return self.stats
463
464
    def get_alert(self,
465
                  current=0,
466
                  minimum=0,
467
                  maximum=100,
468
                  highlight_zero=True,
469
                  is_max=False,
470
                  header="",
471
                  action_key=None,
472
                  log=False):
473
        """Return the alert status relative to a current value.
474
475
        Use this function for minor stats.
476
477
        If current < CAREFUL of max then alert = OK
478
        If current > CAREFUL of max then alert = CAREFUL
479
        If current > WARNING of max then alert = WARNING
480
        If current > CRITICAL of max then alert = CRITICAL
481
482
        If highlight=True than 0.0 is highlighted
483
484
        If defined 'header' is added between the plugin name and the status.
485
        Only useful for stats with several alert status.
486
487
        If defined, 'action_key' define the key for the actions.
488
        By default, the action_key is equal to the header.
489
490
        If log=True than add log if necessary
491
        elif log=False than do not log
492
        elif log=None than apply the config given in the conf file
493
        """
494
        # Manage 0 (0.0) value if highlight_zero is not True
495
        if not highlight_zero and current == 0:
496
            return 'DEFAULT'
497
498
        # Compute the %
499
        try:
500
            value = (current * 100) / maximum
501
        except ZeroDivisionError:
502
            return 'DEFAULT'
503
        except TypeError:
504
            return 'DEFAULT'
505
506
        # Build the stat_name = plugin_name + header
507
        if header == "":
508
            stat_name = self.plugin_name
509
        else:
510
            stat_name = self.plugin_name + '_' + header
511
512
        # Manage limits
513
        # If is_max is set then display the value in MAX
514
        ret = 'MAX' if is_max else 'OK'
515
        try:
516
            if value >= self.get_limit('critical', stat_name=stat_name):
517
                ret = 'CRITICAL'
518
            elif value >= self.get_limit('warning', stat_name=stat_name):
519
                ret = 'WARNING'
520
            elif value >= self.get_limit('careful', stat_name=stat_name):
521
                ret = 'CAREFUL'
522
            elif current < minimum:
523
                ret = 'CAREFUL'
524
        except KeyError:
525
            return 'DEFAULT'
526
527
        # Manage log
528
        log_str = ""
529
        if self.get_limit_log(stat_name=stat_name, default_action=log):
530
            # Add _LOG to the return string
531
            # So stats will be highlited with a specific color
532
            log_str = "_LOG"
533
            # Add the log to the list
534
            glances_logs.add(ret, stat_name.upper(), value)
535
536
        # Manage action
537
        self.manage_action(stat_name, ret.lower(), header, action_key)
538
539
        # Default is ok
540
        return ret + log_str
541
542
    def manage_action(self,
543
                      stat_name,
544
                      trigger,
545
                      header,
546
                      action_key):
547
        """Manage the action for the current stat"""
548
        # Here is a command line for the current trigger ?
549
        try:
550
            command = self.get_limit_action(trigger, stat_name=stat_name)
551
        except KeyError:
552
            # Reset the trigger
553
            self.actions.set(stat_name, trigger)
554
        else:
555
            # Define the action key for the stats dict
556
            # If not define, then it sets to header
557
            if action_key is None:
558
                action_key = header
559
560
            # A command line is available for the current alert
561
            # 1) Build the {{mustache}} dictionnary
562
            if isinstance(self.get_stats_action(), list):
563
                # If the stats are stored in a list of dict (fs plugin for exemple)
564
                # Return the dict for the current header
565
                mustache_dict = {}
566
                for item in self.get_stats_action():
567
                    if item[self.get_key()] == action_key:
568
                        mustache_dict = item
569
                        break
570
            else:
571
                # Use the stats dict
572
                mustache_dict = self.get_stats_action()
573
            # 2) Run the action
574
            self.actions.run(
575
                stat_name, trigger, command, mustache_dict=mustache_dict)
576
577
    def get_alert_log(self,
578
                      current=0,
579
                      minimum=0,
580
                      maximum=100,
581
                      header="",
582
                      action_key=None):
583
        """Get the alert log."""
584
        return self.get_alert(current=current,
585
                              minimum=minimum,
586
                              maximum=maximum,
587
                              header=header,
588
                              action_key=action_key,
589
                              log=True)
590
591
    def get_limit(self, criticity, stat_name=""):
592
        """Return the limit value for the alert."""
593
        # Get the limit for stat + header
594
        # Exemple: network_wlan0_rx_careful
595
        try:
596
            limit = self._limits[stat_name + '_' + criticity]
597
        except KeyError:
598
            # Try fallback to plugin default limit
599
            # Exemple: network_careful
600
            limit = self._limits[self.plugin_name + '_' + criticity]
601
602
        # logger.debug("{} {} value is {}".format(stat_name, criticity, limit))
603
604
        # Return the limit
605
        return limit
606
607
    def get_limit_action(self, criticity, stat_name=""):
608
        """Return the action for the alert."""
609
        # Get the action for stat + header
610
        # Exemple: network_wlan0_rx_careful_action
611
        try:
612
            ret = self._limits[stat_name + '_' + criticity + '_action']
613
        except KeyError:
614
            # Try fallback to plugin default limit
615
            # Exemple: network_careful_action
616
            ret = self._limits[self.plugin_name + '_' + criticity + '_action']
617
618
        # Return the action list
619
        return ret
620
621
    def get_limit_log(self, stat_name, default_action=False):
622
        """Return the log tag for the alert."""
623
        # Get the log tag for stat + header
624
        # Exemple: network_wlan0_rx_log
625
        try:
626
            log_tag = self._limits[stat_name + '_log']
627
        except KeyError:
628
            # Try fallback to plugin default log
629
            # Exemple: network_log
630
            try:
631
                log_tag = self._limits[self.plugin_name + '_log']
632
            except KeyError:
633
                # By defaukt, log are disabled
634
                return default_action
635
636
        # Return the action list
637
        return log_tag[0].lower() == 'true'
638
639
    def get_conf_value(self, value, header="", plugin_name=None):
640
        """Return the configuration (header_) value for the current plugin.
641
642
        ...or the one given by the plugin_name var.
643
        """
644
        if plugin_name is None:
645
            # If not default use the current plugin name
646
            plugin_name = self.plugin_name
647
648
        if header != "":
649
            # Add the header
650
            plugin_name = plugin_name + '_' + header
651
652
        try:
653
            return self._limits[plugin_name + '_' + value]
654
        except KeyError:
655
            return []
656
657
    def is_hide(self, value, header=""):
658
        """
659
        Return True if the value is in the hide configuration list.
660
        The hide configuration list is defined in the glances.conf file.
661
        It is a comma separed list of regexp.
662
        Example for diskio:
663
        hide=sda2,sda5,loop.*
664
        """
665
        # TODO: possible optimisation: create a re.compile list
666
        return not all(j is None for j in [re.match(i, value) for i in self.get_conf_value('hide', header=header)])
667
668
    def has_alias(self, header):
669
        """Return the alias name for the relative header or None if nonexist."""
670
        try:
671
            return self._limits[self.plugin_name + '_' + header + '_' + 'alias'][0]
672
        except (KeyError, IndexError):
673
            return None
674
675
    def msg_curse(self, args=None, max_width=None):
676
        """Return default string to display in the curse interface."""
677
        return [self.curse_add_line(str(self.stats))]
678
679
    def get_stats_display(self, args=None, max_width=None):
680
        """Return a dict with all the information needed to display the stat.
681
682
        key     | description
683
        ----------------------------
684
        display | Display the stat (True or False)
685
        msgdict | Message to display (list of dict [{ 'msg': msg, 'decoration': decoration } ... ])
686
        align   | Message position
687
        """
688
        display_curse = False
689
690
        if hasattr(self, 'display_curse'):
691
            display_curse = self.display_curse
692
        if hasattr(self, 'align'):
693
            align_curse = self._align
694
695
        if max_width is not None:
696
            ret = {'display': display_curse,
697
                   'msgdict': self.msg_curse(args, max_width=max_width),
698
                   'align': align_curse}
699
        else:
700
            ret = {'display': display_curse,
701
                   'msgdict': self.msg_curse(args),
702
                   'align': align_curse}
703
704
        return ret
705
706
    def curse_add_line(self, msg, decoration="DEFAULT",
707
                       optional=False, additional=False,
708
                       splittable=False):
709
        """Return a dict with.
710
711
        Where:
712
            msg: string
713
            decoration:
714
                DEFAULT: no decoration
715
                UNDERLINE: underline
716
                BOLD: bold
717
                TITLE: for stat title
718
                PROCESS: for process name
719
                STATUS: for process status
720
                NICE: for process niceness
721
                CPU_TIME: for process cpu time
722
                OK: Value is OK and non logged
723
                OK_LOG: Value is OK and logged
724
                CAREFUL: Value is CAREFUL and non logged
725
                CAREFUL_LOG: Value is CAREFUL and logged
726
                WARNING: Value is WARINING and non logged
727
                WARNING_LOG: Value is WARINING and logged
728
                CRITICAL: Value is CRITICAL and non logged
729
                CRITICAL_LOG: Value is CRITICAL and logged
730
            optional: True if the stat is optional (display only if space is available)
731
            additional: True if the stat is additional (display only if space is available after optional)
732
            spittable: Line can be splitted to fit on the screen (default is not)
733
        """
734
        return {'msg': msg, 'decoration': decoration, 'optional': optional, 'additional': additional, 'splittable': splittable}
735
736
    def curse_new_line(self):
737
        """Go to a new line."""
738
        return self.curse_add_line('\n')
739
740
    @property
741
    def align(self):
742
        """Get the curse align."""
743
        return self._align
744
745
    @align.setter
746
    def align(self, value):
747
        """Set the curse align.
748
749
        value: left, right, bottom.
750
        """
751
        self._align = value
752
753
    def auto_unit(self, number, low_precision=False):
754
        """Make a nice human-readable string out of number.
755
756
        Number of decimal places increases as quantity approaches 1.
757
758
        examples:
759
        CASE: 613421788        RESULT:       585M low_precision:       585M
760
        CASE: 5307033647       RESULT:      4.94G low_precision:       4.9G
761
        CASE: 44968414685      RESULT:      41.9G low_precision:      41.9G
762
        CASE: 838471403472     RESULT:       781G low_precision:       781G
763
        CASE: 9683209690677    RESULT:      8.81T low_precision:       8.8T
764
        CASE: 1073741824       RESULT:      1024M low_precision:      1024M
765
        CASE: 1181116006       RESULT:      1.10G low_precision:       1.1G
766
767
        'low_precision=True' returns less decimal places potentially
768
        sacrificing precision for more readability.
769
        """
770
        symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
771
        prefix = {
772
            'Y': 1208925819614629174706176,
773
            'Z': 1180591620717411303424,
774
            'E': 1152921504606846976,
775
            'P': 1125899906842624,
776
            'T': 1099511627776,
777
            'G': 1073741824,
778
            'M': 1048576,
779
            'K': 1024
780
        }
781
782
        for symbol in reversed(symbols):
783
            value = float(number) / prefix[symbol]
784
            if value > 1:
785
                decimal_precision = 0
786
                if value < 10:
787
                    decimal_precision = 2
788
                elif value < 100:
789
                    decimal_precision = 1
790
                if low_precision:
791
                    if symbol in 'MK':
792
                        decimal_precision = 0
793
                    else:
794
                        decimal_precision = min(1, decimal_precision)
795
                elif symbol in 'K':
796
                    decimal_precision = 0
797
                return '{:.{decimal}f}{symbol}'.format(
798
                    value, decimal=decimal_precision, symbol=symbol)
799
        return '{!s}'.format(number)
800
801
    def _check_decorator(fct):
802
        """Check if the plugin is enabled."""
803
        def wrapper(self, *args, **kw):
804
            if self.is_enable():
805
                ret = fct(self, *args, **kw)
806
            else:
807
                ret = self.stats
808
            return ret
809
        return wrapper
810
811
    def _log_result_decorator(fct):
812
        """Log (DEBUG) the result of the function fct."""
813
        def wrapper(*args, **kw):
814
            ret = fct(*args, **kw)
815
            logger.debug("%s %s %s return %s" % (
816
                args[0].__class__.__name__,
817
                args[0].__class__.__module__[len('glances_'):],
818
                fct.__name__, ret))
819
            return ret
820
        return wrapper
821
822
    # Mandatory to call the decorator in childs' classes
823
    _check_decorator = staticmethod(_check_decorator)
824
    _log_result_decorator = staticmethod(_log_result_decorator)
825