config.py 12.2 KB
Newer Older
N
Nicolas Hennion 已提交
1 2
# -*- coding: utf-8 -*-
#
3
# This file is part of Glances.
N
Nicolas Hennion 已提交
4
#
N
nicolargo 已提交
5
# Copyright (C) 2019 Nicolargo <nicolas@nicolargo.com>
N
Nicolas Hennion 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18 19
#
# 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/>.

A
PEP 257  
Alessio Sergi 已提交
20 21
"""Manage the configuration file."""

N
Nicolas Hennion 已提交
22
import os
23
import sys
24
import multiprocessing
A
Alessio Sergi 已提交
25
from io import open
26
import re
A
Alessio Sergi 已提交
27

28
from glances.compat import ConfigParser, NoOptionError, NoSectionError, system_exec
A
Alessio Sergi 已提交
29
from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS
30
from glances.logger import logger
N
Nicolas Hennion 已提交
31 32


A
Alessio Sergi 已提交
33 34 35 36 37 38 39 40 41 42 43 44 45
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')
N
nicolargo 已提交
46 47 48 49
    if path is None:
        path = ''
    else:
        path = os.path.join(path, 'glances')
A
Alessio Sergi 已提交
50 51 52 53 54 55 56 57 58

    return path


def user_cache_dir():
    r"""Return the per-user cache dir (full path).

    - Linux, *BSD, SunOS: ~/.cache/glances
    - macOS: ~/Library/Caches/glances
59
    - Windows: {%LOCALAPPDATA%,%APPDATA%}\glances\cache
A
Alessio Sergi 已提交
60
    """
61 62 63
    if WINDOWS:
        path = os.path.join(os.environ.get('LOCALAPPDATA') or os.environ.get('APPDATA'),
                            'glances', 'cache')
A
Alessio Sergi 已提交
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
    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')
N
nicolargo 已提交
86 87 88 89
    if path is None:
        path = ''
    else:
        path = os.path.join(path, 'glances')
A
Alessio Sergi 已提交
90 91 92 93

    return path


94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
def default_config_dir():
    r"""Return the 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 BDS 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


A
Alessio Sergi 已提交
112
class Config(object):
A
PEP 257  
Alessio Sergi 已提交
113 114

    """This class is used to access/read config file, if it exists.
N
Nicolas Hennion 已提交
115

116 117
    :param config_dir: the path to search for config file
    :type config_dir: str or None
N
Nicolas Hennion 已提交
118 119
    """

120 121
    def __init__(self, config_dir=None):
        self.config_dir = config_dir
A
Alessio Sergi 已提交
122
        self.config_filename = 'glances.conf'
123
        self._loaded_config_file = None
N
Nicolas Hennion 已提交
124

125
        # Re patern for optimize research of `foo`
126
        self.re_pattern = re.compile(r'(\`.+?\`)')
127

128 129 130 131
        try:
            self.parser = ConfigParser(interpolation=None)
        except TypeError:
            self.parser = ConfigParser()
132

133
        self.read()
134

135
    def config_file_paths(self):
136
        r"""Get a list of config file paths.
A
PEP 257  
Alessio Sergi 已提交
137 138

        The list is built taking into account of the OS, priority and location.
N
Nicolas Hennion 已提交
139

140
        * custom path: /path/to/glances
A
Alessio Sergi 已提交
141
        * Linux, SunOS: ~/.config/glances, /etc/glances
A
Alessio Sergi 已提交
142 143
        * *BSD: ~/.config/glances, /usr/local/etc/glances
        * macOS: ~/Library/Application Support/glances, /usr/local/etc/glances
N
Nicolas Hennion 已提交
144 145 146 147 148
        * 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)
149
            * system-wide directory (system-wide settings)
150
            * default pip directory (as defined in the setup.py file)
N
Nicolas Hennion 已提交
151 152 153
        """
        paths = []

154 155
        if self.config_dir:
            paths.append(self.config_dir)
156

157 158
        paths.append(os.path.join(user_config_dir(), self.config_filename))
        paths.append(os.path.join(system_config_dir(), self.config_filename))
159
        paths.append(os.path.join(default_config_dir(), self.config_filename))
N
Nicolas Hennion 已提交
160 161 162

        return paths

163 164 165
    def read(self):
        """Read the config file, if it exists. Using defaults otherwise."""
        for config_file in self.config_file_paths():
N
Nicolas Hennion 已提交
166
            logger.debug('Search glances.conf file in {}'.format(config_file))
167 168
            if os.path.exists(config_file):
                try:
A
Alessio Sergi 已提交
169 170 171
                    with open(config_file, encoding='utf-8') as f:
                        self.parser.read_file(f)
                        self.parser.read(f)
172
                    logger.info("Read configuration file '{}'".format(config_file))
173
                except UnicodeDecodeError as err:
174
                    logger.error("Can not read configuration file '{}': {}".format(config_file, err))
175 176 177 178 179
                    sys.exit(1)
                # Save the loaded configuration file path (issue #374)
                self._loaded_config_file = config_file
                break

180 181 182 183
        # Set the default values for section not configured
        self.sections_set_default()

    def sections_set_default(self):
184 185 186 187 188 189 190 191 192 193
        # Globals
        if not self.parser.has_section('global'):
            self.parser.add_section('global')
        self.set_default('global', 'strftime_format', '')

        # check_update
        if not self.parser.has_section('global'):
            self.parser.add_section('global')
        self.set_default('global', 'check_update', 'false')

194 195 196
        # Quicklook
        if not self.parser.has_section('quicklook'):
            self.parser.add_section('quicklook')
N
nicolargo 已提交
197 198 199
        self.set_default_cwc('quicklook', 'cpu')
        self.set_default_cwc('quicklook', 'mem')
        self.set_default_cwc('quicklook', 'swap')
200 201 202 203

        # CPU
        if not self.parser.has_section('cpu'):
            self.parser.add_section('cpu')
N
nicolargo 已提交
204 205 206
        self.set_default_cwc('cpu', 'user')
        self.set_default_cwc('cpu', 'system')
        self.set_default_cwc('cpu', 'steal')
207 208
        # By default I/O wait should be lower than 1/number of CPU cores
        iowait_bottleneck = (1.0 / multiprocessing.cpu_count()) * 100.0
N
nicolargo 已提交
209 210 211 212
        self.set_default_cwc('cpu', 'iowait',
                             [str(iowait_bottleneck - (iowait_bottleneck * 0.20)),
                              str(iowait_bottleneck - (iowait_bottleneck * 0.10)),
                              str(iowait_bottleneck)])
213 214
        # Context switches bottleneck identification #1212
        ctx_switches_bottleneck = (500000 * 0.10) * multiprocessing.cpu_count()
N
nicolargo 已提交
215 216 217 218
        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)])
219 220 221 222

        # Per-CPU
        if not self.parser.has_section('percpu'):
            self.parser.add_section('percpu')
N
nicolargo 已提交
223 224
        self.set_default_cwc('percpu', 'user')
        self.set_default_cwc('percpu', 'system')
225 226 227 228

        # Load
        if not self.parser.has_section('load'):
            self.parser.add_section('load')
N
nicolargo 已提交
229
        self.set_default_cwc('load', cwc=['0.7', '1.0', '5.0'])
230 231 232 233

        # Mem
        if not self.parser.has_section('mem'):
            self.parser.add_section('mem')
N
nicolargo 已提交
234
        self.set_default_cwc('mem')
235 236 237 238

        # Swap
        if not self.parser.has_section('memswap'):
            self.parser.add_section('memswap')
N
nicolargo 已提交
239
        self.set_default_cwc('memswap')
240

241 242 243
        # NETWORK
        if not self.parser.has_section('network'):
            self.parser.add_section('network')
N
nicolargo 已提交
244 245
        self.set_default_cwc('network', 'rx')
        self.set_default_cwc('network', 'tx')
246

247 248 249
        # FS
        if not self.parser.has_section('fs'):
            self.parser.add_section('fs')
N
nicolargo 已提交
250
        self.set_default_cwc('fs')
251 252 253 254

        # Sensors
        if not self.parser.has_section('sensors'):
            self.parser.add_section('sensors')
A
Alessio Sergi 已提交
255 256 257
        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'])
258 259 260 261

        # Process list
        if not self.parser.has_section('processlist'):
            self.parser.add_section('processlist')
N
nicolargo 已提交
262 263
        self.set_default_cwc('processlist', 'cpu')
        self.set_default_cwc('processlist', 'mem')
264 265 266 267 268 269

    @property
    def loaded_config_file(self):
        """Return the loaded configuration file."""
        return self._loaded_config_file

270 271 272 273 274 275 276 277 278
    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

279 280 281 282
    def sections(self):
        """Return a list of all sections."""
        return self.parser.sections()

283
    def items(self, section):
A
PEP 257  
Alessio Sergi 已提交
284
        """Return the items list of a section."""
285 286
        return self.parser.items(section)

N
Nicolas Hennion 已提交
287
    def has_section(self, section):
A
PEP 257  
Alessio Sergi 已提交
288
        """Return info about the existence of a section."""
N
Nicolas Hennion 已提交
289 290
        return self.parser.has_section(section)

N
nicolargo 已提交
291 292 293 294 295 296 297 298 299 300 301 302
    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])

303 304
    def set_default(self, section, option,
                    default):
305 306 307 308
        """If the option did not exist, create a default value."""
        if not self.parser.has_option(section, option):
            self.parser.set(section, option, default)

309 310 311 312
    def get_value(self, section, option,
                  default=None):
        """Get the value of an option, if it exists.

313
        If it did not exist, then return the default value.
314 315

        It allows user to define dynamic configuration key (see issue#1204)
N
nicolargo 已提交
316
        Dynamic value should starts and end with the ` char
317 318 319
        Example: prefix=`hostname`
        """
        ret = default
N
Nicolas Hennion 已提交
320
        try:
321
            ret = self.parser.get(section, option)
322
        except (NoOptionError, NoSectionError):
323 324 325 326
            pass

        # Search a substring `foo` and replace it by the result of its exec
        if ret is not None:
327 328 329 330 331 332
            try:
                match = self.re_pattern.findall(ret)
                for m in match:
                    ret = ret.replace(m, system_exec(m[1:-1]))
            except TypeError:
                pass
333
        return ret
N
Nicolas Hennion 已提交
334

335 336 337 338
    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)
339
        except (NoOptionError, NoSectionError):
340 341
            return int(default)

342 343
    def get_float_value(self, section, option, default=0.0):
        """Get the float value of an option, if it exists."""
N
Nicolas Hennion 已提交
344
        try:
345
            return self.parser.getfloat(section, option)
346
        except (NoOptionError, NoSectionError):
347
            return float(default)
348 349 350 351 352

    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)
353
        except (NoOptionError, NoSectionError):
354
            return bool(default)