You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

309 lines
11 KiB

4 years ago
  1. # -*- coding: utf-8 -*-
  2. #
  3. # This file is part of Glances.
  4. #
  5. # Copyright (C) 2018 Nicolargo <nicolas@nicolargo.com>
  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. """Manage the configuration file."""
  20. import os
  21. import sys
  22. import multiprocessing
  23. from io import open
  24. import re
  25. from glances.compat import ConfigParser, NoOptionError, system_exec
  26. from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS
  27. from glances.logger import logger
  28. def user_config_dir():
  29. r"""Return the per-user config dir (full path).
  30. - Linux, *BSD, SunOS: ~/.config/glances
  31. - macOS: ~/Library/Application Support/glances
  32. - Windows: %APPDATA%\glances
  33. """
  34. if WINDOWS:
  35. path = os.environ.get('APPDATA')
  36. elif MACOS:
  37. path = os.path.expanduser('~/Library/Application Support')
  38. else:
  39. path = os.environ.get('XDG_CONFIG_HOME') or os.path.expanduser('~/.config')
  40. if path is None:
  41. path = ''
  42. else:
  43. path = os.path.join(path, 'glances')
  44. return path
  45. def user_cache_dir():
  46. r"""Return the per-user cache dir (full path).
  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'),
  53. 'glances', 'cache')
  54. elif MACOS:
  55. path = os.path.expanduser('~/Library/Caches/glances')
  56. else:
  57. path = os.path.join(os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),
  58. 'glances')
  59. return path
  60. def system_config_dir():
  61. r"""Return the system-wide config dir (full path).
  62. - Linux, SunOS: /etc/glances
  63. - *BSD, macOS: /usr/local/etc/glances
  64. - Windows: %APPDATA%\glances
  65. """
  66. if LINUX or SUNOS:
  67. path = '/etc'
  68. elif BSD or MACOS:
  69. path = '/usr/local/etc'
  70. else:
  71. path = os.environ.get('APPDATA')
  72. if path is None:
  73. path = ''
  74. else:
  75. path = os.path.join(path, 'glances')
  76. return path
  77. class Config(object):
  78. """This class is used to access/read config file, if it exists.
  79. :param config_dir: the path to search for config file
  80. :type config_dir: str or None
  81. """
  82. def __init__(self, config_dir=None):
  83. self.config_dir = config_dir
  84. self.config_filename = 'glances.conf'
  85. self._loaded_config_file = None
  86. # Re patern for optimize research of `foo`
  87. self.re_pattern = re.compile('(\`.+?\`)')
  88. self.parser = ConfigParser()
  89. self.read()
  90. def config_file_paths(self):
  91. r"""Get a list of config file paths.
  92. The list is built taking into account of the OS, priority and location.
  93. * custom path: /path/to/glances
  94. * Linux, SunOS: ~/.config/glances, /etc/glances
  95. * *BSD: ~/.config/glances, /usr/local/etc/glances
  96. * macOS: ~/Library/Application Support/glances, /usr/local/etc/glances
  97. * Windows: %APPDATA%\glances
  98. The config file will be searched in the following order of priority:
  99. * /path/to/file (via -C flag)
  100. * user's home directory (per-user settings)
  101. * system-wide directory (system-wide settings)
  102. """
  103. paths = []
  104. if self.config_dir:
  105. paths.append(self.config_dir)
  106. paths.append(os.path.join(user_config_dir(), self.config_filename))
  107. paths.append(os.path.join(system_config_dir(), self.config_filename))
  108. return paths
  109. def read(self):
  110. """Read the config file, if it exists. Using defaults otherwise."""
  111. for config_file in self.config_file_paths():
  112. logger.info('Search glances.conf file in {}'.format(config_file))
  113. if os.path.exists(config_file):
  114. try:
  115. with open(config_file, encoding='utf-8') as f:
  116. self.parser.read_file(f)
  117. self.parser.read(f)
  118. logger.info("Read configuration file '{}'".format(config_file))
  119. except UnicodeDecodeError as err:
  120. logger.error("Can not read configuration file '{}': {}".format(config_file, err))
  121. sys.exit(1)
  122. # Save the loaded configuration file path (issue #374)
  123. self._loaded_config_file = config_file
  124. break
  125. # Quicklook
  126. if not self.parser.has_section('quicklook'):
  127. self.parser.add_section('quicklook')
  128. self.set_default_cwc('quicklook', 'cpu')
  129. self.set_default_cwc('quicklook', 'mem')
  130. self.set_default_cwc('quicklook', 'swap')
  131. # CPU
  132. if not self.parser.has_section('cpu'):
  133. self.parser.add_section('cpu')
  134. self.set_default_cwc('cpu', 'user')
  135. self.set_default_cwc('cpu', 'system')
  136. self.set_default_cwc('cpu', 'steal')
  137. # By default I/O wait should be lower than 1/number of CPU cores
  138. iowait_bottleneck = (1.0 / multiprocessing.cpu_count()) * 100.0
  139. self.set_default_cwc('cpu', 'iowait',
  140. [str(iowait_bottleneck - (iowait_bottleneck * 0.20)),
  141. str(iowait_bottleneck - (iowait_bottleneck * 0.10)),
  142. str(iowait_bottleneck)])
  143. # Context switches bottleneck identification #1212
  144. ctx_switches_bottleneck = (500000 * 0.10) * multiprocessing.cpu_count()
  145. self.set_default_cwc('cpu', 'ctx_switches',
  146. [str(ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.20)),
  147. str(ctx_switches_bottleneck - (ctx_switches_bottleneck * 0.10)),
  148. str(ctx_switches_bottleneck)])
  149. # Per-CPU
  150. if not self.parser.has_section('percpu'):
  151. self.parser.add_section('percpu')
  152. self.set_default_cwc('percpu', 'user')
  153. self.set_default_cwc('percpu', 'system')
  154. # Load
  155. if not self.parser.has_section('load'):
  156. self.parser.add_section('load')
  157. self.set_default_cwc('load', cwc=['0.7', '1.0', '5.0'])
  158. # Mem
  159. if not self.parser.has_section('mem'):
  160. self.parser.add_section('mem')
  161. self.set_default_cwc('mem')
  162. # Swap
  163. if not self.parser.has_section('memswap'):
  164. self.parser.add_section('memswap')
  165. self.set_default_cwc('memswap')
  166. # NETWORK
  167. if not self.parser.has_section('network'):
  168. self.parser.add_section('network')
  169. self.set_default_cwc('network', 'rx')
  170. self.set_default_cwc('network', 'tx')
  171. # FS
  172. if not self.parser.has_section('fs'):
  173. self.parser.add_section('fs')
  174. self.set_default_cwc('fs')
  175. # Sensors
  176. if not self.parser.has_section('sensors'):
  177. self.parser.add_section('sensors')
  178. self.set_default_cwc('sensors', 'temperature_core', cwc=['60', '70', '80'])
  179. self.set_default_cwc('sensors', 'temperature_hdd', cwc=['45', '52', '60'])
  180. self.set_default_cwc('sensors', 'battery', cwc=['80', '90', '95'])
  181. # Process list
  182. if not self.parser.has_section('processlist'):
  183. self.parser.add_section('processlist')
  184. self.set_default_cwc('processlist', 'cpu')
  185. self.set_default_cwc('processlist', 'mem')
  186. @property
  187. def loaded_config_file(self):
  188. """Return the loaded configuration file."""
  189. return self._loaded_config_file
  190. def as_dict(self):
  191. """Return the configuration as a dict"""
  192. dictionary = {}
  193. for section in self.parser.sections():
  194. dictionary[section] = {}
  195. for option in self.parser.options(section):
  196. dictionary[section][option] = self.parser.get(section, option)
  197. return dictionary
  198. def sections(self):
  199. """Return a list of all sections."""
  200. return self.parser.sections()
  201. def items(self, section):
  202. """Return the items list of a section."""
  203. return self.parser.items(section)
  204. def has_section(self, section):
  205. """Return info about the existence of a section."""
  206. return self.parser.has_section(section)
  207. def set_default_cwc(self, section,
  208. option_header=None,
  209. cwc=['50', '70', '90']):
  210. """Set default values for careful, warning and critical."""
  211. if option_header is None:
  212. header = ''
  213. else:
  214. header = option_header + '_'
  215. self.set_default(section, header + 'careful', cwc[0])
  216. self.set_default(section, header + 'warning', cwc[1])
  217. self.set_default(section, header + 'critical', cwc[2])
  218. def set_default(self, section, option,
  219. default):
  220. """If the option did not exist, create a default value."""
  221. if not self.parser.has_option(section, option):
  222. self.parser.set(section, option, default)
  223. def get_value(self, section, option,
  224. default=None):
  225. """Get the value of an option, if it exists.
  226. If it did not exist, then return de default value.
  227. It allows user to define dynamic configuration key (see issue#1204)
  228. Dynamic vlaue should starts and end with the ` char
  229. Example: prefix=`hostname`
  230. """
  231. ret = default
  232. try:
  233. ret = self.parser.get(section, option)
  234. except NoOptionError:
  235. pass
  236. # Search a substring `foo` and replace it by the result of its exec
  237. if ret is not None:
  238. try:
  239. match = self.re_pattern.findall(ret)
  240. for m in match:
  241. ret = ret.replace(m, system_exec(m[1:-1]))
  242. except TypeError:
  243. pass
  244. return ret
  245. def get_int_value(self, section, option, default=0):
  246. """Get the int value of an option, if it exists."""
  247. try:
  248. return self.parser.getint(section, option)
  249. except NoOptionError:
  250. return int(default)
  251. def get_float_value(self, section, option, default=0.0):
  252. """Get the float value of an option, if it exists."""
  253. try:
  254. return self.parser.getfloat(section, option)
  255. except NoOptionError:
  256. return float(default)