]> kaliko git repositories - mpd-sima.git/blob - sima/utils/config.py
Catches SIGHUP/SIGUSR1 to trigger conf reload
[mpd-sima.git] / sima / utils / config.py
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2009, 2010, 2011, 2013 Jack Kaliko <kaliko@azylum.org>
4 #
5 #  This file is part of sima
6 #
7 #  sima is free software: you can redistribute it and/or modify
8 #  it under the terms of the GNU 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 #  sima 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 General Public License for more details.
16 #
17 #  You should have received a copy of the GNU General Public License
18 #  along with sima.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 #
21
22 """
23 Deal with configuration and data files.
24 Parse configuration file and set defaults for missing options.
25 """
26
27 # IMPORTS
28 import configparser
29 import sys
30
31 from configparser import Error
32 from os import (makedirs, environ, stat, chmod)
33 from os.path import (join, isdir, isfile)
34 from stat import (S_IMODE, ST_MODE, S_IRWXO, S_IRWXG)
35
36 from . import utils
37
38 # DEFAULTS
39 DIRNAME = 'sima'
40 CONF_FILE = 'sima.cfg'
41
42 DEFAULT_CONF = {
43         'MPD': {
44             'host': "localhost",
45             #'password': "",
46             'port': "6600",
47             },
48         'sima': {
49             'internal': "Crop, Lastfm, RandomFallBack",
50             'contrib': "",
51             'user_db': "false",
52             'history_duration': "8",
53             'queue_length': "1",
54             },
55         'daemon':{
56             'daemon': "false",
57             'pidfile': "",
58             },
59         'log': {
60             'verbosity': "info",
61             'logfile': "",
62             },
63         'echonest': {
64             },
65         'lastfm': {
66             'dynamic': "10",
67             'similarity': "15",
68             'queue_mode': "track", #TODO control values
69             'single_album': "false",
70             'track_to_add': "1",
71             'album_to_add': "1",
72             'depth': "1",
73             },
74         'randomfallback': {
75             'flavour': "sensible", # in pure, sensible, genre
76             'track_to_add': "1",
77             }
78         }
79 #
80
81
82 class ConfMan(object):  # CONFIG MANAGER CLASS
83     """
84     Configuration manager.
85     Default configuration is stored in DEFAULT_CONF dictionnary.
86     First init_config() run to get config from file.
87     Then control_conf() is run and retrieve configuration from defaults if not
88     set in conf files.
89     These settings are then updated with command line options with
90     supersedes_config_with_cmd_line_options().
91
92     Order of priority for the origin of an option is then (lowest to highest):
93         * DEFAULT_CONF
94         * Env. Var for MPD host, port and password
95         * configuration file (overrides previous)
96         * command line options (overrides previous)
97     """
98
99     def __init__(self, logger, options=None):
100         # options settings priority:
101         # defauts < conf. file < command line
102         self.conf_file = options.get('conf_file')
103         self.config = None
104         self.defaults = dict(DEFAULT_CONF)
105         self.startopt = options
106         ## Sima sqlite DB
107         self.db_file = None
108
109         self.log = logger
110         ## INIT CALLS
111         self.use_envar()
112         self.init_config()
113         self.control_conf()
114         self.supersedes_config_with_cmd_line_options()
115         self.config['sima']['db_file'] = self.db_file
116
117     def get_pw(self):
118         try:
119             self.config.getboolean('MPD', 'password')
120             self.log.debug('No password set, proceeding without ' +
121                            'authentication...')
122             return None
123         except ValueError:
124             # ValueError if password not a boolean, hence an actual password.
125             pwd = self.config.get('MPD', 'password')
126             if not pwd:
127                 self.log.debug('Password set as an empty string.')
128                 return None
129             return pwd
130
131     def control_mod(self):
132         """
133         Controls conf file permissions.
134         """
135         mode = S_IMODE(stat(self.conf_file)[ST_MODE])
136         self.log.debug('file permission is: %o' % mode)
137         if mode & S_IRWXO or mode & S_IRWXG:
138             self.log.warning('File is readable by "other" and/or' +
139                              ' "group" (actual permission %o octal).' %
140                              mode)
141             self.log.warning('Consider setting permissions' +
142                              ' to 600 octal.')
143
144     def supersedes_config_with_cmd_line_options(self):
145         """Updates defaults settings with command line options"""
146         for sec in self.config.sections():
147             for opt in self.config.options(sec):
148                 if opt in list(self.startopt.keys()):
149                     self.config.set(sec, opt, str(self.startopt.get(opt)))
150
151     def use_envar(self):
152         """Use MPD en.var. to set defaults"""
153         mpd_host, mpd_port, passwd = utils.get_mpd_environ()
154         if mpd_host:
155             self.log.info('Env. variable MPD_HOST set to "%s"' % mpd_host)
156             self.defaults['MPD']['host'] = mpd_host
157         if passwd:
158             self.log.info('Env. variable MPD_HOST contains password.')
159             self.defaults['MPD']['password'] = passwd
160         if mpd_port:
161             self.log.info('Env. variable MPD_PORT set to "%s".'
162                                   % mpd_port)
163             self.defaults['MPD']['port'] = mpd_port
164
165     def control_conf(self):
166         """Get through options/values and set defaults if not in conf file."""
167         # Control presence of obsolete settings
168         for option in ['history', 'history_length', 'top_tracks']:
169             if self.config.has_option('sima', option):
170                 self.log.warning('Obsolete setting found in conf file: "%s"'
171                         % option)
172         # Setting default if not specified
173         for section in DEFAULT_CONF.keys():
174             if section not in self.config.sections():
175                 self.log.debug('[%s] NOT in conf file' % section)
176                 self.config.add_section(section)
177                 for option in self.defaults[section]:
178                     self.config.set(section,
179                             option,
180                             self.defaults[section][option])
181                     self.log.debug(
182                             'Setting option with default value: %s = %s' %
183                             (option, self.defaults[section][option]))
184             elif section in self.config.sections():
185                 self.log.debug('[%s] present in conf file' % section)
186                 for option in self.defaults[section]:
187                     if self.config.has_option(section, option):
188                         #self.log.debug(u'option "%s" set to "%s" in conf. file' %
189                         #              (option, self.config.get(section, option)))
190                         pass
191                     else:
192                         self.log.debug(
193                                 'Option "%s" missing in section "%s"' %
194                                 (option, section))
195                         self.log.debug('=> setting default "%s" (may not suit you…)' %
196                                        self.defaults[section][option])
197                         self.config.set(section, option,
198                                         self.defaults[section][option])
199
200     def init_config(self):
201         """
202         Use XDG directory standard if exists
203         else use "HOME/(.config|.local/share)/sima/"
204         http://standards.freedesktop.org/basedir-spec/basedir-spec-0.6.html
205         """
206
207         homedir = environ.get('HOME')
208
209         if environ.get('XDG_DATA_HOME'):
210             data_dir = join(environ.get('XDG_DATA_HOME'), DIRNAME)
211         elif self.startopt.get('var_dir'):
212             # If var folder is provided via CLI set data_dir accordingly
213             data_dir = join(self.startopt.get('var_dir'))
214         elif (homedir and isdir(homedir) and homedir not in ['/']):
215             data_dir = join(homedir, '.local', 'share', DIRNAME)
216         else:
217             self.log.error('Can\'t find a suitable location for data folder (XDG_DATA_HOME)')
218             self.log.error('Please use "--var_dir" to set a proper location')
219             sys.exit(1)
220
221         if not isdir(data_dir):
222             makedirs(data_dir)
223             chmod(data_dir, 0o700)
224
225         if self.startopt.get('conf_file'):
226             # No need to handle conf file location
227             pass
228         elif environ.get('XDG_CONFIG_HOME'):
229             conf_dir = join(environ.get('XDG_CONFIG_HOME'), DIRNAME)
230         elif (homedir and isdir(homedir) and homedir not in ['/']):
231             conf_dir = join(homedir, '.config', DIRNAME)
232             # Create conf_dir if necessary
233             if not isdir(conf_dir):
234                 makedirs(conf_dir)
235                 chmod(conf_dir, 0o700)
236             self.conf_file = join(conf_dir, CONF_FILE)
237         else:
238             self.log.error('Can\'t find a suitable location for config folder (XDG_CONFIG_HOME)')
239             self.log.error('Please use "--config" to locate the conf file')
240             sys.exit(1)
241
242         self.db_file = join(data_dir, 'sima.db')
243
244         config = configparser.SafeConfigParser()
245         # If no conf file present, uses defaults
246         if not isfile(self.conf_file):
247             self.config = config
248             return
249
250         self.log.info('Loading configuration from:  %s' % self.conf_file)
251         self.control_mod()
252
253         try:
254             config.read(self.conf_file)
255         except Error as err:
256             self.log.error(err)
257             sys.exit(1)
258
259         self.config = config
260
261 # VIM MODLINE
262 # vim: ai ts=4 sw=4 sts=4 expandtab