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