glances.config.Config.has_section()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 2
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""Manage the configuration file."""
10
11
import builtins
12
import multiprocessing
13
import os
14
import re
15
import sys
16
17
from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS, ConfigParser, NoOptionError, NoSectionError, system_exec
18
from glances.logger import logger
19
20
21
def user_config_dir():
22
    r"""Return a list of per-user config dir (full path).
23
24
    - Linux, *BSD, SunOS: ~/.config/glances
25
    - macOS: ~/Library/Application Support/glances
26
    - Windows: %APPDATA%\glances
27
    """
28
    paths = []
29
    if WINDOWS:
30
        paths.append(os.environ.get('APPDATA'))
31
    elif MACOS:
32
        paths.append(os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config'))
33
        paths.append(os.path.expanduser('~/Library/Application Support'))
34
    else:
35
        paths.append(os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config'))
36
37
    return [os.path.join(path, 'glances') if path is not None else '' for path in paths]
38
39
40
def user_cache_dir():
41
    r"""Return a list of per-user cache dir (full path).
42
43
    - Linux, *BSD, SunOS: ~/.cache/glances
44
    - macOS: ~/Library/Caches/glances
45
    - Windows: {%LOCALAPPDATA%,%APPDATA%}\glances\cache
46
    """
47
    if WINDOWS:
48
        path = os.path.join(os.environ.get('LOCALAPPDATA') or os.environ.get('APPDATA'), 'glances', 'cache')
49
    elif MACOS:
50
        path = os.path.expanduser('~/Library/Caches/glances')
51
    else:
52
        path = os.path.join(os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'), 'glances')
53
54
    return [path]
55
56
57
def system_config_dir():
58
    r"""Return a list of system-wide config dir (full path).
59
60
    - Linux, SunOS: /etc/glances
61
    - *BSD, macOS: /usr/local/etc/glances
62
    - Windows: %APPDATA%\glances
63
    """
64
    if LINUX or SUNOS:
65
        path = '/etc'
66
    elif BSD or MACOS:
67
        path = '/usr/local/etc'
68
    else:
69
        path = os.environ.get('APPDATA')
70
    if path is None:
71
        path = ''
72
    else:
73
        path = os.path.join(path, 'glances')
74
75
    return [path]
76
77
78
def default_config_dir():
79
    r"""Return a list of system-wide config dir (full path).
80
81
    - Linux, SunOS, *BSD, macOS: /usr/share/doc (as defined in the setup.py files)
82
    - Windows: %APPDATA%\glances
83
    """
84
    path = []
85
    # Add venv path (solve issue #2803)
86
    if in_virtualenv():
87
        path.append(os.path.join(sys.prefix, 'share', 'doc', 'glances'))
88
89
    # Add others system path
90
    if LINUX or SUNOS or BSD or MACOS:
91
        path.append('/usr/share/doc')
92
    else:
93
        path.append(os.environ.get('APPDATA'))
94
95
    return path
96
97
98
def in_virtualenv():
99
    # Source: https://stackoverflow.com/questions/1871549/how-to-determine-if-python-is-running-inside-a-virtualenv/1883251#1883251
100
    return sys.prefix != get_base_prefix_compat()
101
102
103
def get_base_prefix_compat():
104
    """Get base/real prefix, or sys.prefix if there is none."""
105
    # Source: https://stackoverflow.com/questions/1871549/how-to-determine-if-python-is-running-inside-a-virtualenv/1883251#1883251
106
    return getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
107
108
109
class Config:
110
    """This class is used to access/read config file, if it exists.
111
112
    :param config_dir: the path to search for config file
113
    :type config_dir: str or None
114
    """
115
116
    def __init__(self, config_dir=None):
117
        self.config_dir = config_dir
118
        self.config_filename = 'glances.conf'
119
        self._loaded_config_file = None
120
        self._config_file_paths = self.config_file_paths()
121
122
        # Re pattern for optimize research of `foo`
123
        self.re_pattern = re.compile(r'(\`.+?\`)')
124
125
        try:
126
            self.parser = ConfigParser(interpolation=None)
127
        except TypeError:
128
            self.parser = ConfigParser()
129
130
        self.read()
131
132
    def config_file_paths(self):
133
        r"""Get a list of config file paths.
134
135
        The list is built taking into account of the OS, priority and location.
136
137
        * custom path: /path/to/glances
138
        * Linux, SunOS: ~/.config/glances, /etc/glances
139
        * *BSD: ~/.config/glances, /usr/local/etc/glances
140
        * macOS: ~/.config/glances, ~/Library/Application Support/glances, /usr/local/etc/glances
141
        * Windows: %APPDATA%\glances
142
143
        The config file will be searched in the following order of priority:
144
            * /path/to/file (via -C flag)
145
            * user's home directory (per-user settings)
146
            * system-wide directory (system-wide settings)
147
            * default pip directory (as defined in the setup.py file)
148
        """
149
        paths = []
150
151
        # self.config_dir is the path to the config file (via -C flag)
152
        if self.config_dir:
153
            paths.append(self.config_dir)
154
155
        # user_config_dir() returns a list of paths
156
        paths.extend([os.path.join(path, self.config_filename) for path in user_config_dir()])
157
158
        # system_config_dir() returns a list of paths
159
        paths.extend([os.path.join(path, self.config_filename) for path in system_config_dir()])
160
161
        # default_config_dir() returns a list of paths
162
        paths.extend([os.path.join(path, self.config_filename) for path in default_config_dir()])
163
164
        return paths
165
166
    def read(self):
167
        """Read the config file, if it exists. Using defaults otherwise."""
168
        for config_file in self._config_file_paths:
169
            logger.debug(f'Search glances.conf file in {config_file}')
170
            if os.path.exists(config_file):
171
                try:
172
                    with builtins.open(config_file, encoding='utf-8') as f:
173
                        self.parser.read_file(f)
174
                        self.parser.read(f)
175
                    logger.info(f"Read configuration file '{config_file}'")
176
                except UnicodeDecodeError as err:
177
                    logger.error(f"Can not read configuration file '{config_file}': {err}")
178
                    sys.exit(1)
179
                # Save the loaded configuration file path (issue #374)
180
                self._loaded_config_file = config_file
181
                break
182
183
        # Set the default values for section not configured
184
        self.sections_set_default()
185
186
    def sections_set_default(self):
187
        # Globals
188
        if not self.parser.has_section('global'):
189
            self.parser.add_section('global')
190
        self.set_default('global', 'strftime_format', '')
191
        self.set_default('global', 'check_update', 'true')
192
193
        # Quicklook
194
        if not self.parser.has_section('quicklook'):
195
            self.parser.add_section('quicklook')
196
        self.set_default_cwc('quicklook', 'cpu')
197
        self.set_default_cwc('quicklook', 'mem')
198
        self.set_default_cwc('quicklook', 'swap')
199
200
        # CPU
201
        if not self.parser.has_section('cpu'):
202
            self.parser.add_section('cpu')
203
        self.set_default_cwc('cpu', 'user')
204
        self.set_default_cwc('cpu', 'system')
205
        self.set_default_cwc('cpu', 'steal')
206
        # By default I/O wait should be lower than 1/number of CPU cores
207
        iowait_bottleneck = (1.0 / multiprocessing.cpu_count()) * 100.0
208
        self.set_default_cwc(
209
            'cpu',
210
            'iowait',
211
            [
212
                str(iowait_bottleneck - (iowait_bottleneck * 0.20)),
213
                str(iowait_bottleneck - (iowait_bottleneck * 0.10)),
214
                str(iowait_bottleneck),
215
            ],
216
        )
217
        # Context switches bottleneck identification #1212
218
        ctx_switches_bottleneck = (500000 * 0.10) * multiprocessing.cpu_count()
219
        self.set_default_cwc(
220
            'cpu',
221
            'ctx_switches',
222
            [
223
                str(ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20)),
224
                str(ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10)),
225
                str(ctx_switches_bottleneck),
226
            ],
227
        )
228
229
        # Per-CPU
230
        if not self.parser.has_section('percpu'):
231
            self.parser.add_section('percpu')
232
        self.set_default_cwc('percpu', 'user')
233
        self.set_default_cwc('percpu', 'system')
234
235
        # Load
236
        if not self.parser.has_section('load'):
237
            self.parser.add_section('load')
238
        self.set_default_cwc('load', cwc=['0.7', '1.0', '5.0'])
239
240
        # Mem
241
        if not self.parser.has_section('mem'):
242
            self.parser.add_section('mem')
243
        self.set_default_cwc('mem')
244
245
        # Swap
246
        if not self.parser.has_section('memswap'):
247
            self.parser.add_section('memswap')
248
        self.set_default_cwc('memswap')
249
250
        # NETWORK
251
        if not self.parser.has_section('network'):
252
            self.parser.add_section('network')
253
        self.set_default_cwc('network', 'rx')
254
        self.set_default_cwc('network', 'tx')
255
256
        # FS
257
        if not self.parser.has_section('fs'):
258
            self.parser.add_section('fs')
259
        self.set_default_cwc('fs')
260
261
        # Sensors
262
        if not self.parser.has_section('sensors'):
263
            self.parser.add_section('sensors')
264
        self.set_default_cwc('sensors', 'temperature_core', cwc=['60', '70', '80'])
265
        self.set_default_cwc('sensors', 'temperature_hdd', cwc=['45', '52', '60'])
266
        self.set_default_cwc('sensors', 'battery', cwc=['80', '90', '95'])
267
268
        # Process list
269
        if not self.parser.has_section('processlist'):
270
            self.parser.add_section('processlist')
271
        self.set_default_cwc('processlist', 'cpu')
272
        self.set_default_cwc('processlist', 'mem')
273
274
    @property
275
    def loaded_config_file(self):
276
        """Return the loaded configuration file."""
277
        return self._loaded_config_file
278
279
    def as_dict(self):
280
        """Return the configuration as a dict"""
281
        dictionary = {}
282
        for section in self.parser.sections():
283
            dictionary[section] = {}
284
            for option in self.parser.options(section):
285
                dictionary[section][option] = self.parser.get(section, option)
286
        return dictionary
287
288
    def sections(self):
289
        """Return a list of all sections."""
290
        return self.parser.sections()
291
292
    def items(self, section):
293
        """Return the items list of a section."""
294
        return self.parser.items(section)
295
296
    def has_section(self, section):
297
        """Return info about the existence of a section."""
298
        return self.parser.has_section(section)
299
300
    def set_default_cwc(self, section, option_header=None, cwc=['50', '70', '90']):
301
        """Set default values for careful, warning and critical."""
302
        if option_header is None:
303
            header = ''
304
        else:
305
            header = option_header + '_'
306
        self.set_default(section, header + 'careful', cwc[0])
307
        self.set_default(section, header + 'warning', cwc[1])
308
        self.set_default(section, header + 'critical', cwc[2])
309
310
    def set_default(self, section, option, default):
311
        """If the option did not exist, create a default value."""
312
        if not self.parser.has_option(section, option):
313
            self.parser.set(section, option, default)
314
315
    def get_value(self, section, option, default=None):
316
        """Get the value of an option, if it exists.
317
318
        If it did not exist, then return the default value.
319
320
        It allows user to define dynamic configuration key (see issue#1204)
321
        Dynamic value should starts and end with the ` char
322
        Example: prefix=`hostname`
323
        """
324
        ret = default
325
        try:
326
            ret = self.parser.get(section, option)
327
        except (NoOptionError, NoSectionError):
328
            pass
329
330
        # Search a substring `foo` and replace it by the result of its exec
331
        if ret is not None:
332
            try:
333
                match = self.re_pattern.findall(ret)
334
                for m in match:
335
                    ret = ret.replace(m, system_exec(m[1:-1]))
336
            except TypeError:
337
                pass
338
        return ret
339
340
    def get_list_value(self, section, option, default=None, separator=','):
341
        """Get the list value of an option, if it exists."""
342
        try:
343
            return self.parser.get(section, option).split(separator)
344
        except (NoOptionError, NoSectionError):
345
            return default
346
347
    def get_int_value(self, section, option, default=0):
348
        """Get the int value of an option, if it exists."""
349
        try:
350
            return self.parser.getint(section, option)
351
        except (NoOptionError, NoSectionError):
352
            return int(default)
353
354
    def get_float_value(self, section, option, default=0.0):
355
        """Get the float value of an option, if it exists."""
356
        try:
357
            return self.parser.getfloat(section, option)
358
        except (NoOptionError, NoSectionError):
359
            return float(default)
360
361
    def get_bool_value(self, section, option, default=True):
362
        """Get the bool value of an option, if it exists."""
363
        try:
364
            return self.parser.getboolean(section, option)
365
        except (NoOptionError, NoSectionError):
366
            return bool(default)
367