glances.config   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 345
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 193
dl 0
loc 345
rs 3.44
c 0
b 0
f 0
wmc 62

15 Methods

Rating   Name   Duplication   Size   Complexity  
A Config.config_file_paths() 0 27 2
A Config.__init__() 0 14 2
A Config.read() 0 19 5
A Config.get_value() 0 24 5
D Config.sections_set_default() 0 87 12
A Config.items() 0 3 1
A Config.set_default_cwc() 0 9 2
A Config.get_bool_value() 0 6 2
A Config.loaded_config_file() 0 4 1
A Config.get_float_value() 0 6 2
A Config.get_int_value() 0 6 2
A Config.as_dict() 0 8 3
A Config.has_section() 0 3 1
A Config.set_default() 0 4 2
A Config.sections() 0 3 1

4 Functions

Rating   Name   Duplication   Size   Complexity  
B default_config_dir() 0 16 6
A user_cache_dir() 0 15 3
A user_config_dir() 0 19 4
B system_config_dir() 0 19 6

How to fix   Complexity   

Complexity

Complex classes like glances.config often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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