# -*- coding: utf-8 -*-
|
|
#
|
|
# This file is part of Glances.
|
|
#
|
|
# Copyright (C) 2018 Nicolargo <nicolas@nicolargo.com>
|
|
#
|
|
# Glances is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Glances is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
"""Manage the configuration file."""
|
|
|
|
import os
|
|
import sys
|
|
import multiprocessing
|
|
from io import open
|
|
import re
|
|
|
|
from glances.compat import ConfigParser, NoOptionError, system_exec
|
|
from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS
|
|
from glances.logger import logger
|
|
|
|
|
|
def user_config_dir():
|
|
r"""Return the per-user config dir (full path).
|
|
|
|
- Linux, *BSD, SunOS: ~/.config/glances
|
|
- macOS: ~/Library/Application Support/glances
|
|
- Windows: %APPDATA%\glances
|
|
"""
|
|
if WINDOWS:
|
|
path = os.environ.get('APPDATA')
|
|
elif MACOS:
|
|
path = os.path.expanduser('~/Library/Application Support')
|
|
else:
|
|
path = os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config')
|
|
if path is None:
|
|
path = ''
|
|
else:
|
|
path = os.path.join(path, 'glances')
|
|
|
|
return path
|
|
|
|
|
|
def user_cache_dir():
|
|
r"""Return the 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 the 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
|
|
|
|
|
|
class Config(object):
|
|
|
|
"""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 patern for optimize research of `foo`
|
|
self.re_pattern = re.compile('(\`.+?\`)')
|
|
|
|
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: ~/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)
|
|
"""
|
|
paths = []
|
|
|
|
if self.config_dir:
|
|
paths.append(self.config_dir)
|
|
|
|
paths.append(os.path.join(user_config_dir(), self.config_filename))
|
|
paths.append(os.path.join(system_config_dir(), self.config_filename))
|
|
|
|
return paths
|
|
|
|
def read(self):
|
|
"""Read the config file, if it exists. Using defaults otherwise."""
|
|
for config_file in self.config_file_paths():
|
|
logger.info('Search glances.conf file in {}'.format(config_file))
|
|
if os.path.exists(config_file):
|
|
try:
|
|
with open(config_file, encoding='utf-8') as f:
|
|
self.parser.read_file(f)
|
|
self.parser.read(f)
|
|
logger.info("Read configuration file '{}'".format(config_file))
|
|
except UnicodeDecodeError as err:
|
|
logger.error("Can not read configuration file '{}': {}".format(config_file, err))
|
|
sys.exit(1)
|
|
# Save the loaded configuration file path (issue #374)
|
|
self._loaded_config_file = config_file
|
|
break
|
|
|
|
# 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 de default value.
|
|
|
|
It allows user to define dynamic configuration key (see issue#1204)
|
|
Dynamic vlaue should starts and end with the ` char
|
|
Example: prefix=`hostname`
|
|
"""
|
|
ret = default
|
|
try:
|
|
ret = self.parser.get(section, option)
|
|
except NoOptionError:
|
|
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_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:
|
|
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:
|
|
return float(default)
|