# # This file is part of Glances. # # SPDX-FileCopyrightText: 2022 Nicolas Hennion # # SPDX-License-Identifier: LGPL-3.0-only # """Manage the configuration file.""" import builtins import multiprocessing import os import re import sys from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS, ConfigParser, NoOptionError, NoSectionError, system_exec from glances.logger import logger def user_config_dir(): r"""Return a list of per-user config dir (full path). - Linux, *BSD, SunOS: ~/.config/glances - macOS: ~/Library/Application Support/glances - Windows: %APPDATA%\glances """ paths = [] if WINDOWS: paths.append(os.environ.get('APPDATA')) elif MACOS: paths.append(os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config')) paths.append(os.path.expanduser('~/Library/Application Support')) else: paths.append(os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config')) return [os.path.join(path, 'glances') if path is not None else '' for path in paths] def user_cache_dir(): r"""Return a list of per-user cache dir (full path). - Linux, *BSD, SunOS: ~/.cache/glances - macOS: ~/Library/Caches/glances - Windows: {%LOCALAPPDATA%,%APPDATA%}\glances\cache """ if WINDOWS: path = os.path.join(os.environ.get('LOCALAPPDATA') or os.environ.get('APPDATA'), 'glances', 'cache') elif MACOS: path = os.path.expanduser('~/Library/Caches/glances') else: path = os.path.join(os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'), 'glances') return [path] def system_config_dir(): r"""Return a list of system-wide config dir (full path). - Linux, SunOS: /etc/glances - *BSD, macOS: /usr/local/etc/glances - Windows: %APPDATA%\glances """ if LINUX or SUNOS: path = '/etc' elif BSD or MACOS: path = '/usr/local/etc' else: path = os.environ.get('APPDATA') if path is None: path = '' else: path = os.path.join(path, 'glances') return [path] def default_config_dir(): r"""Return a list of system-wide config dir (full path). - Linux, SunOS, *BSD, macOS: /usr/share/doc (as defined in the setup.py files) - Windows: %APPDATA%\glances """ if LINUX or SUNOS or BSD or MACOS: path = '/usr/share/doc' else: path = os.environ.get('APPDATA') if path is None: path = '' else: path = os.path.join(path, 'glances') return [path] class Config: """This class is used to access/read config file, if it exists. :param config_dir: the path to search for config file :type config_dir: str or None """ def __init__(self, config_dir=None): self.config_dir = config_dir self.config_filename = 'glances.conf' self._loaded_config_file = None # Re pattern for optimize research of `foo` self.re_pattern = re.compile(r'(\`.+?\`)') try: self.parser = ConfigParser(interpolation=None) except TypeError: self.parser = ConfigParser() self.read() def config_file_paths(self): r"""Get a list of config file paths. The list is built taking into account of the OS, priority and location. * custom path: /path/to/glances * Linux, SunOS: ~/.config/glances, /etc/glances * *BSD: ~/.config/glances, /usr/local/etc/glances * macOS: ~/.config/glances, ~/Library/Application Support/glances, /usr/local/etc/glances * Windows: %APPDATA%\glances The config file will be searched in the following order of priority: * /path/to/file (via -C flag) * user's home directory (per-user settings) * system-wide directory (system-wide settings) * default pip directory (as defined in the setup.py file) """ paths = [] # self.config_dir is the path to the config file (via -C flag) if self.config_dir: paths.append(self.config_dir) # user_config_dir() returns a list of paths paths.extend([os.path.join(path, self.config_filename) for path in user_config_dir()]) # system_config_dir() returns a list of paths paths.extend([os.path.join(path, self.config_filename) for path in system_config_dir()]) # default_config_dir() returns a list of paths paths.extend([os.path.join(path, self.config_filename) for path in default_config_dir()]) return paths def read(self): """Read the config file, if it exists. Using defaults otherwise.""" for config_file in self.config_file_paths(): logger.debug(f'Search glances.conf file in {config_file}') if os.path.exists(config_file): try: with builtins.open(config_file, encoding='utf-8') as f: self.parser.read_file(f) self.parser.read(f) logger.info(f"Read configuration file '{config_file}'") except UnicodeDecodeError as err: logger.error(f"Can not read configuration file '{config_file}': {err}") sys.exit(1) # Save the loaded configuration file path (issue #374) self._loaded_config_file = config_file break # Set the default values for section not configured self.sections_set_default() def sections_set_default(self): # Globals if not self.parser.has_section('global'): self.parser.add_section('global') self.set_default('global', 'strftime_format', '') self.set_default('global', 'check_update', 'true') # Quicklook if not self.parser.has_section('quicklook'): self.parser.add_section('quicklook') self.set_default_cwc('quicklook', 'cpu') self.set_default_cwc('quicklook', 'mem') self.set_default_cwc('quicklook', 'swap') # CPU if not self.parser.has_section('cpu'): self.parser.add_section('cpu') self.set_default_cwc('cpu', 'user') self.set_default_cwc('cpu', 'system') self.set_default_cwc('cpu', 'steal') # By default I/O wait should be lower than 1/number of CPU cores iowait_bottleneck = (1.0 / multiprocessing.cpu_count()) * 100.0 self.set_default_cwc( 'cpu', 'iowait', [ str(iowait_bottleneck - (iowait_bottleneck * 0.20)), str(iowait_bottleneck - (iowait_bottleneck * 0.10)), str(iowait_bottleneck), ], ) # Context switches bottleneck identification #1212 ctx_switches_bottleneck = (500000 * 0.10) * multiprocessing.cpu_count() self.set_default_cwc( 'cpu', 'ctx_switches', [ str(ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20)), str(ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10)), str(ctx_switches_bottleneck), ], ) # Per-CPU if not self.parser.has_section('percpu'): self.parser.add_section('percpu') self.set_default_cwc('percpu', 'user') self.set_default_cwc('percpu', 'system') # Load if not self.parser.has_section('load'): self.parser.add_section('load') self.set_default_cwc('load', cwc=['0.7', '1.0', '5.0']) # Mem if not self.parser.has_section('mem'): self.parser.add_section('mem') self.set_default_cwc('mem') # Swap if not self.parser.has_section('memswap'): self.parser.add_section('memswap') self.set_default_cwc('memswap') # NETWORK if not self.parser.has_section('network'): self.parser.add_section('network') self.set_default_cwc('network', 'rx') self.set_default_cwc('network', 'tx') # FS if not self.parser.has_section('fs'): self.parser.add_section('fs') self.set_default_cwc('fs') # Sensors if not self.parser.has_section('sensors'): self.parser.add_section('sensors') self.set_default_cwc('sensors', 'temperature_core', cwc=['60', '70', '80']) self.set_default_cwc('sensors', 'temperature_hdd', cwc=['45', '52', '60']) self.set_default_cwc('sensors', 'battery', cwc=['80', '90', '95']) # Process list if not self.parser.has_section('processlist'): self.parser.add_section('processlist') self.set_default_cwc('processlist', 'cpu') self.set_default_cwc('processlist', 'mem') @property def loaded_config_file(self): """Return the loaded configuration file.""" return self._loaded_config_file def as_dict(self): """Return the configuration as a dict""" dictionary = {} for section in self.parser.sections(): dictionary[section] = {} for option in self.parser.options(section): dictionary[section][option] = self.parser.get(section, option) return dictionary def sections(self): """Return a list of all sections.""" return self.parser.sections() def items(self, section): """Return the items list of a section.""" return self.parser.items(section) def has_section(self, section): """Return info about the existence of a section.""" return self.parser.has_section(section) def set_default_cwc(self, section, option_header=None, cwc=['50', '70', '90']): """Set default values for careful, warning and critical.""" if option_header is None: header = '' else: header = option_header + '_' self.set_default(section, header + 'careful', cwc[0]) self.set_default(section, header + 'warning', cwc[1]) self.set_default(section, header + 'critical', cwc[2]) def set_default(self, section, option, default): """If the option did not exist, create a default value.""" if not self.parser.has_option(section, option): self.parser.set(section, option, default) def get_value(self, section, option, default=None): """Get the value of an option, if it exists. If it did not exist, then return the default value. It allows user to define dynamic configuration key (see issue#1204) Dynamic value should starts and end with the ` char Example: prefix=`hostname` """ ret = default try: ret = self.parser.get(section, option) except (NoOptionError, NoSectionError): pass # Search a substring `foo` and replace it by the result of its exec if ret is not None: try: match = self.re_pattern.findall(ret) for m in match: ret = ret.replace(m, system_exec(m[1:-1])) except TypeError: pass return ret def get_list_value(self, section, option, default=None, separator=','): """Get the list value of an option, if it exists.""" try: return self.parser.get(section, option).split(separator) except (NoOptionError, NoSectionError): return default def get_int_value(self, section, option, default=0): """Get the int value of an option, if it exists.""" try: return self.parser.getint(section, option) except (NoOptionError, NoSectionError): return int(default) def get_float_value(self, section, option, default=0.0): """Get the float value of an option, if it exists.""" try: return self.parser.getfloat(section, option) except (NoOptionError, NoSectionError): return float(default) def get_bool_value(self, section, option, default=True): """Get the bool value of an option, if it exists.""" try: return self.parser.getboolean(section, option) except (NoOptionError, NoSectionError): return bool(default)