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