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