1#odoo.loggers.handlers. -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4import configparser as ConfigParser
5import errno
6import logging
7import optparse
8import glob
9import os
10import sys
11import tempfile
12import warnings
13import odoo
14from os.path import expandvars, expanduser, abspath, realpath
15from .. import release, conf, loglevels
16from . import appdirs
17
18from passlib.context import CryptContext
19crypt_context = CryptContext(schemes=['pbkdf2_sha512', 'plaintext'],
20                             deprecated=['plaintext'])
21
22class MyOption (optparse.Option, object):
23    """ optparse Option with two additional attributes.
24
25    The list of command line options (getopt.Option) is used to create the
26    list of the configuration file options. When reading the file, and then
27    reading the command line arguments, we don't want optparse.parse results
28    to override the configuration file values. But if we provide default
29    values to optparse, optparse will return them and we can't know if they
30    were really provided by the user or not. A solution is to not use
31    optparse's default attribute, but use a custom one (that will be copied
32    to create the default values of the configuration file).
33
34    """
35    def __init__(self, *opts, **attrs):
36        self.my_default = attrs.pop('my_default', None)
37        super(MyOption, self).__init__(*opts, **attrs)
38
39DEFAULT_LOG_HANDLER = ':INFO'
40def _get_default_datadir():
41    home = os.path.expanduser('~')
42    if os.path.isdir(home):
43        func = appdirs.user_data_dir
44    else:
45        if sys.platform in ['win32', 'darwin']:
46            func = appdirs.site_data_dir
47        else:
48            func = lambda **kwarg: "/var/lib/%s" % kwarg['appname'].lower()
49    # No "version" kwarg as session and filestore paths are shared against series
50    return func(appname=release.product_name, appauthor=release.author)
51
52def _deduplicate_loggers(loggers):
53    """ Avoid saving multiple logging levels for the same loggers to a save
54    file, that just takes space and the list can potentially grow unbounded
55    if for some odd reason people use :option`--save`` all the time.
56    """
57    # dict(iterable) -> the last item of iterable for any given key wins,
58    # which is what we want and expect. Output order should not matter as
59    # there are no duplicates within the output sequence
60    return (
61        '{}:{}'.format(logger, level)
62        for logger, level in dict(it.split(':') for it in loggers).items()
63    )
64
65class configmanager(object):
66    def __init__(self, fname=None):
67        """Constructor.
68
69        :param fname: a shortcut allowing to instantiate :class:`configmanager`
70                      from Python code without resorting to environment
71                      variable
72        """
73        # Options not exposed on the command line. Command line options will be added
74        # from optparse's parser.
75        self.options = {
76            'admin_passwd': 'admin',
77            'csv_internal_sep': ',',
78            'publisher_warranty_url': 'http://services.openerp.com/publisher-warranty/',
79            'reportgz': False,
80            'root_path': None,
81        }
82
83        # Not exposed in the configuration file.
84        self.blacklist_for_save = set([
85            'publisher_warranty_url', 'load_language', 'root_path',
86            'init', 'save', 'config', 'update', 'stop_after_init', 'dev_mode', 'shell_interface'
87        ])
88
89        # dictionary mapping option destination (keys in self.options) to MyOptions.
90        self.casts = {}
91
92        self.misc = {}
93        self.config_file = fname
94
95        self._LOGLEVELS = dict([
96            (getattr(loglevels, 'LOG_%s' % x), getattr(logging, x))
97            for x in ('CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG', 'NOTSET')
98        ])
99
100        version = "%s %s" % (release.description, release.version)
101        self.parser = parser = optparse.OptionParser(version=version, option_class=MyOption)
102
103        # Server startup config
104        group = optparse.OptionGroup(parser, "Common options")
105        group.add_option("-c", "--config", dest="config", help="specify alternate config file")
106        group.add_option("-s", "--save", action="store_true", dest="save", default=False,
107                          help="save configuration to ~/.odoorc (or to ~/.openerp_serverrc if it exists)")
108        group.add_option("-i", "--init", dest="init", help="install one or more modules (comma-separated list, use \"all\" for all modules), requires -d")
109        group.add_option("-u", "--update", dest="update",
110                          help="update one or more modules (comma-separated list, use \"all\" for all modules). Requires -d.")
111        group.add_option("--without-demo", dest="without_demo",
112                          help="disable loading demo data for modules to be installed (comma-separated, use \"all\" for all modules). Requires -d and -i. Default is %default",
113                          my_default=False)
114        group.add_option("-P", "--import-partial", dest="import_partial", my_default='',
115                        help="Use this for big data importation, if it crashes you will be able to continue at the current state. Provide a filename to store intermediate importation states.")
116        group.add_option("--pidfile", dest="pidfile", help="file where the server pid will be stored")
117        group.add_option("--addons-path", dest="addons_path",
118                         help="specify additional addons paths (separated by commas).",
119                         action="callback", callback=self._check_addons_path, nargs=1, type="string")
120        group.add_option("--upgrade-path", dest="upgrade_path",
121                         help="specify an additional upgrade path.",
122                         action="callback", callback=self._check_upgrade_path, nargs=1, type="string")
123        group.add_option("--load", dest="server_wide_modules", help="Comma-separated list of server-wide modules.", my_default='base,web')
124
125        group.add_option("-D", "--data-dir", dest="data_dir", my_default=_get_default_datadir(),
126                         help="Directory where to store Odoo data")
127        parser.add_option_group(group)
128
129        # HTTP
130        group = optparse.OptionGroup(parser, "HTTP Service Configuration")
131        group.add_option("--http-interface", dest="http_interface", my_default='',
132                         help="Listen interface address for HTTP services. "
133                              "Keep empty to listen on all interfaces (0.0.0.0)")
134        group.add_option("-p", "--http-port", dest="http_port", my_default=8069,
135                         help="Listen port for the main HTTP service", type="int", metavar="PORT")
136        group.add_option("--longpolling-port", dest="longpolling_port", my_default=8072,
137                         help="Listen port for the longpolling HTTP service", type="int", metavar="PORT")
138        group.add_option("--no-http", dest="http_enable", action="store_false", my_default=True,
139                         help="Disable the HTTP and Longpolling services entirely")
140        group.add_option("--proxy-mode", dest="proxy_mode", action="store_true", my_default=False,
141                         help="Activate reverse proxy WSGI wrappers (headers rewriting) "
142                              "Only enable this when running behind a trusted web proxy!")
143        # HTTP: hidden backwards-compatibility for "*xmlrpc*" options
144        hidden = optparse.SUPPRESS_HELP
145        group.add_option("--xmlrpc-interface", dest="http_interface", help=hidden)
146        group.add_option("--xmlrpc-port", dest="http_port", type="int", help=hidden)
147        group.add_option("--no-xmlrpc", dest="http_enable", action="store_false", help=hidden)
148
149        parser.add_option_group(group)
150
151        # WEB
152        group = optparse.OptionGroup(parser, "Web interface Configuration")
153        group.add_option("--db-filter", dest="dbfilter", my_default='', metavar="REGEXP",
154                         help="Regular expressions for filtering available databases for Web UI. "
155                              "The expression can use %d (domain) and %h (host) placeholders.")
156        parser.add_option_group(group)
157
158        # Testing Group
159        group = optparse.OptionGroup(parser, "Testing Configuration")
160        group.add_option("--test-file", dest="test_file", my_default=False,
161                         help="Launch a python test file.")
162        group.add_option("--test-enable", action="callback", callback=self._test_enable_callback,
163                         dest='test_enable',
164                         help="Enable unit tests.")
165        group.add_option("--test-tags", dest="test_tags",
166                         help="Comma-separated list of specs to filter which tests to execute. Enable unit tests if set. "
167                         "A filter spec has the format: [-][tag][/module][:class][.method] "
168                         "The '-' specifies if we want to include or exclude tests matching this spec. "
169                         "The tag will match tags added on a class with a @tagged decorator "
170                         "(all Test classes have 'standard' and 'at_install' tags "
171                         "until explicitly removed, see the decorator documentation). "
172                         "'*' will match all tags. "
173                         "If tag is omitted on include mode, its value is 'standard'. "
174                         "If tag is omitted on exclude mode, its value is '*'. "
175                         "The module, class, and method will respectively match the module name, test class name and test method name. "
176                         "Example: --test-tags :TestClass.test_func,/test_module,external "
177
178                         "Filtering and executing the tests happens twice: right "
179                         "after each module installation/update and at the end "
180                         "of the modules loading. At each stage tests are filtered "
181                         "by --test-tags specs and additionally by dynamic specs "
182                         "'at_install' and 'post_install' correspondingly.")
183
184        group.add_option("--screencasts", dest="screencasts", action="store", my_default=None,
185                         metavar='DIR',
186                         help="Screencasts will go in DIR/{db_name}/screencasts.")
187        temp_tests_dir = os.path.join(tempfile.gettempdir(), 'odoo_tests')
188        group.add_option("--screenshots", dest="screenshots", action="store", my_default=temp_tests_dir,
189                         metavar='DIR',
190                         help="Screenshots will go in DIR/{db_name}/screenshots. Defaults to %s." % temp_tests_dir)
191        parser.add_option_group(group)
192
193        # Logging Group
194        group = optparse.OptionGroup(parser, "Logging Configuration")
195        group.add_option("--logfile", dest="logfile", help="file where the server log will be stored")
196        group.add_option("--syslog", action="store_true", dest="syslog", my_default=False, help="Send the log to the syslog server")
197        group.add_option('--log-handler', action="append", default=[], my_default=DEFAULT_LOG_HANDLER, metavar="PREFIX:LEVEL", help='setup a handler at LEVEL for a given PREFIX. An empty PREFIX indicates the root logger. This option can be repeated. Example: "odoo.orm:DEBUG" or "werkzeug:CRITICAL" (default: ":INFO")')
198        group.add_option('--log-request', action="append_const", dest="log_handler", const="odoo.http.rpc.request:DEBUG", help='shortcut for --log-handler=odoo.http.rpc.request:DEBUG')
199        group.add_option('--log-response', action="append_const", dest="log_handler", const="odoo.http.rpc.response:DEBUG", help='shortcut for --log-handler=odoo.http.rpc.response:DEBUG')
200        group.add_option('--log-web', action="append_const", dest="log_handler", const="odoo.http:DEBUG", help='shortcut for --log-handler=odoo.http:DEBUG')
201        group.add_option('--log-sql', action="append_const", dest="log_handler", const="odoo.sql_db:DEBUG", help='shortcut for --log-handler=odoo.sql_db:DEBUG')
202        group.add_option('--log-db', dest='log_db', help="Logging database", my_default=False)
203        group.add_option('--log-db-level', dest='log_db_level', my_default='warning', help="Logging database level")
204        # For backward-compatibility, map the old log levels to something
205        # quite close.
206        levels = [
207            'info', 'debug_rpc', 'warn', 'test', 'critical', 'runbot',
208            'debug_sql', 'error', 'debug', 'debug_rpc_answer', 'notset'
209        ]
210        group.add_option('--log-level', dest='log_level', type='choice',
211                         choices=levels, my_default='info',
212                         help='specify the level of the logging. Accepted values: %s.' % (levels,))
213
214        parser.add_option_group(group)
215
216        # SMTP Group
217        group = optparse.OptionGroup(parser, "SMTP Configuration")
218        group.add_option('--email-from', dest='email_from', my_default=False,
219                         help='specify the SMTP email address for sending email')
220        group.add_option('--smtp', dest='smtp_server', my_default='localhost',
221                         help='specify the SMTP server for sending email')
222        group.add_option('--smtp-port', dest='smtp_port', my_default=25,
223                         help='specify the SMTP port', type="int")
224        group.add_option('--smtp-ssl', dest='smtp_ssl', action='store_true', my_default=False,
225                         help='if passed, SMTP connections will be encrypted with SSL (STARTTLS)')
226        group.add_option('--smtp-user', dest='smtp_user', my_default=False,
227                         help='specify the SMTP username for sending email')
228        group.add_option('--smtp-password', dest='smtp_password', my_default=False,
229                         help='specify the SMTP password for sending email')
230        parser.add_option_group(group)
231
232        group = optparse.OptionGroup(parser, "Database related options")
233        group.add_option("-d", "--database", dest="db_name", my_default=False,
234                         help="specify the database name")
235        group.add_option("-r", "--db_user", dest="db_user", my_default=False,
236                         help="specify the database user name")
237        group.add_option("-w", "--db_password", dest="db_password", my_default=False,
238                         help="specify the database password")
239        group.add_option("--pg_path", dest="pg_path", help="specify the pg executable path")
240        group.add_option("--db_host", dest="db_host", my_default=False,
241                         help="specify the database host")
242        group.add_option("--db_port", dest="db_port", my_default=False,
243                         help="specify the database port", type="int")
244        group.add_option("--db_sslmode", dest="db_sslmode", type="choice", my_default='prefer',
245                         choices=['disable', 'allow', 'prefer', 'require', 'verify-ca', 'verify-full'],
246                         help="specify the database ssl connection mode (see PostgreSQL documentation)")
247        group.add_option("--db_maxconn", dest="db_maxconn", type='int', my_default=64,
248                         help="specify the maximum number of physical connections to PostgreSQL")
249        group.add_option("--db-template", dest="db_template", my_default="template0",
250                         help="specify a custom database template to create a new database")
251        parser.add_option_group(group)
252
253        group = optparse.OptionGroup(parser, "Internationalisation options. ",
254            "Use these options to translate Odoo to another language. "
255            "See i18n section of the user manual. Option '-d' is mandatory. "
256            "Option '-l' is mandatory in case of importation"
257            )
258        group.add_option('--load-language', dest="load_language",
259                         help="specifies the languages for the translations you want to be loaded")
260        group.add_option('-l', "--language", dest="language",
261                         help="specify the language of the translation file. Use it with --i18n-export or --i18n-import")
262        group.add_option("--i18n-export", dest="translate_out",
263                         help="export all sentences to be translated to a CSV file, a PO file or a TGZ archive and exit")
264        group.add_option("--i18n-import", dest="translate_in",
265                         help="import a CSV or a PO file with translations and exit. The '-l' option is required.")
266        group.add_option("--i18n-overwrite", dest="overwrite_existing_translations", action="store_true", my_default=False,
267                         help="overwrites existing translation terms on updating a module or importing a CSV or a PO file.")
268        group.add_option("--modules", dest="translate_modules",
269                         help="specify modules to export. Use in combination with --i18n-export")
270        parser.add_option_group(group)
271
272        security = optparse.OptionGroup(parser, 'Security-related options')
273        security.add_option('--no-database-list', action="store_false", dest='list_db', my_default=True,
274                            help="Disable the ability to obtain or view the list of databases. "
275                                 "Also disable access to the database manager and selector, "
276                                 "so be sure to set a proper --database parameter first")
277        parser.add_option_group(security)
278
279        # Advanced options
280        group = optparse.OptionGroup(parser, "Advanced options")
281        group.add_option('--dev', dest='dev_mode', type="string",
282                         help="Enable developer mode. Param: List of options separated by comma. "
283                              "Options : all, [pudb|wdb|ipdb|pdb], reload, qweb, werkzeug, xml")
284        group.add_option('--shell-interface', dest='shell_interface', type="string",
285                         help="Specify a preferred REPL to use in shell mode. Supported REPLs are: "
286                              "[ipython|ptpython|bpython|python]")
287        group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False,
288                          help="stop the server after its initialization")
289        group.add_option("--osv-memory-count-limit", dest="osv_memory_count_limit", my_default=False,
290                         help="Force a limit on the maximum number of records kept in the virtual "
291                              "osv_memory tables. The default is False, which means no count-based limit.",
292                         type="int")
293        group.add_option("--transient-age-limit", dest="transient_age_limit", my_default=1.0,
294                         help="Time limit (decimal value in hours) records created with a "
295                              "TransientModel (mosly wizard) are kept in the database. Default to 1 hour.",
296                         type="float")
297        group.add_option("--osv-memory-age-limit", dest="osv_memory_age_limit", my_default=False,
298                         help="Deprecated alias to the transient-age-limit option",
299                         type="float")
300        group.add_option("--max-cron-threads", dest="max_cron_threads", my_default=2,
301                         help="Maximum number of threads processing concurrently cron jobs (default 2).",
302                         type="int")
303        group.add_option("--unaccent", dest="unaccent", my_default=False, action="store_true",
304                         help="Try to enable the unaccent extension when creating new databases.")
305        group.add_option("--geoip-db", dest="geoip_database", my_default='/usr/share/GeoIP/GeoLite2-City.mmdb',
306                         help="Absolute path to the GeoIP database file.")
307        parser.add_option_group(group)
308
309        if os.name == 'posix':
310            group = optparse.OptionGroup(parser, "Multiprocessing options")
311            # TODO sensible default for the three following limits.
312            group.add_option("--workers", dest="workers", my_default=0,
313                             help="Specify the number of workers, 0 disable prefork mode.",
314                             type="int")
315            group.add_option("--limit-memory-soft", dest="limit_memory_soft", my_default=2048 * 1024 * 1024,
316                             help="Maximum allowed virtual memory per worker (in bytes), when reached the worker be "
317                             "reset after the current request (default 2048MiB).",
318                             type="int")
319            group.add_option("--limit-memory-hard", dest="limit_memory_hard", my_default=2560 * 1024 * 1024,
320                             help="Maximum allowed virtual memory per worker (in bytes), when reached, any memory "
321                             "allocation will fail (default 2560MiB).",
322                             type="int")
323            group.add_option("--limit-time-cpu", dest="limit_time_cpu", my_default=60,
324                             help="Maximum allowed CPU time per request (default 60).",
325                             type="int")
326            group.add_option("--limit-time-real", dest="limit_time_real", my_default=120,
327                             help="Maximum allowed Real time per request (default 120).",
328                             type="int")
329            group.add_option("--limit-time-real-cron", dest="limit_time_real_cron", my_default=-1,
330                             help="Maximum allowed Real time per cron job. (default: --limit-time-real). "
331                                  "Set to 0 for no limit. ",
332                             type="int")
333            group.add_option("--limit-request", dest="limit_request", my_default=8192,
334                             help="Maximum number of request to be processed per worker (default 8192).",
335                             type="int")
336            parser.add_option_group(group)
337
338        # Copy all optparse options (i.e. MyOption) into self.options.
339        for group in parser.option_groups:
340            for option in group.option_list:
341                if option.dest not in self.options:
342                    self.options[option.dest] = option.my_default
343                    self.casts[option.dest] = option
344
345        # generate default config
346        self._parse_config()
347
348    def parse_config(self, args=None):
349        """ Parse the configuration file (if any) and the command-line
350        arguments.
351
352        This method initializes odoo.tools.config and openerp.conf (the
353        former should be removed in the future) with library-wide
354        configuration values.
355
356        This method must be called before proper usage of this library can be
357        made.
358
359        Typical usage of this method:
360
361            odoo.tools.config.parse_config(sys.argv[1:])
362        """
363        opt = self._parse_config(args)
364        odoo.netsvc.init_logger()
365        self._warn_deprecated_options()
366        odoo.modules.module.initialize_sys_path()
367        return opt
368
369    def _parse_config(self, args=None):
370        if args is None:
371            args = []
372        opt, args = self.parser.parse_args(args)
373
374        def die(cond, msg):
375            if cond:
376                self.parser.error(msg)
377
378        # Ensures no illegitimate argument is silently discarded (avoids insidious "hyphen to dash" problem)
379        die(args, "unrecognized parameters: '%s'" % " ".join(args))
380
381        die(bool(opt.syslog) and bool(opt.logfile),
382            "the syslog and logfile options are exclusive")
383
384        die(opt.translate_in and (not opt.language or not opt.db_name),
385            "the i18n-import option cannot be used without the language (-l) and the database (-d) options")
386
387        die(opt.overwrite_existing_translations and not (opt.translate_in or opt.update),
388            "the i18n-overwrite option cannot be used without the i18n-import option or without the update option")
389
390        die(opt.translate_out and (not opt.db_name),
391            "the i18n-export option cannot be used without the database (-d) option")
392
393        # Check if the config file exists (-c used, but not -s)
394        die(not opt.save and opt.config and not os.access(opt.config, os.R_OK),
395            "The config file '%s' selected with -c/--config doesn't exist or is not readable, "\
396            "use -s/--save if you want to generate it"% opt.config)
397
398        die(bool(opt.osv_memory_age_limit) and bool(opt.transient_memory_age_limit),
399            "the osv-memory-count-limit option cannot be used with the "
400            "transient-age-limit option, please only use the latter.")
401
402        # place/search the config file on Win32 near the server installation
403        # (../etc from the server)
404        # if the server is run by an unprivileged user, he has to specify location of a config file where he has the rights to write,
405        # else he won't be able to save the configurations, or even to start the server...
406        # TODO use appdirs
407        if os.name == 'nt':
408            rcfilepath = os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), 'odoo.conf')
409        else:
410            rcfilepath = os.path.expanduser('~/.odoorc')
411            old_rcfilepath = os.path.expanduser('~/.openerp_serverrc')
412
413            die(os.path.isfile(rcfilepath) and os.path.isfile(old_rcfilepath),
414                "Found '.odoorc' and '.openerp_serverrc' in your path. Please keep only one of "\
415                "them, preferably '.odoorc'.")
416
417            if not os.path.isfile(rcfilepath) and os.path.isfile(old_rcfilepath):
418                rcfilepath = old_rcfilepath
419
420        self.rcfile = os.path.abspath(
421            self.config_file or opt.config or os.environ.get('ODOO_RC') or os.environ.get('OPENERP_SERVER') or rcfilepath)
422        self.load()
423
424        # Verify that we want to log or not, if not the output will go to stdout
425        if self.options['logfile'] in ('None', 'False'):
426            self.options['logfile'] = False
427        # the same for the pidfile
428        if self.options['pidfile'] in ('None', 'False'):
429            self.options['pidfile'] = False
430        # the same for the test_tags
431        if self.options['test_tags'] == 'None':
432            self.options['test_tags'] = None
433        # and the server_wide_modules
434        if self.options['server_wide_modules'] in ('', 'None', 'False'):
435            self.options['server_wide_modules'] = 'base,web'
436
437        # if defined do not take the configfile value even if the defined value is None
438        keys = ['http_interface', 'http_port', 'longpolling_port', 'http_enable',
439                'db_name', 'db_user', 'db_password', 'db_host', 'db_sslmode',
440                'db_port', 'db_template', 'logfile', 'pidfile', 'smtp_port',
441                'email_from', 'smtp_server', 'smtp_user', 'smtp_password',
442                'db_maxconn', 'import_partial', 'addons_path', 'upgrade_path',
443                'syslog', 'without_demo', 'screencasts', 'screenshots',
444                'dbfilter', 'log_level', 'log_db',
445                'log_db_level', 'geoip_database', 'dev_mode', 'shell_interface'
446        ]
447
448        for arg in keys:
449            # Copy the command-line argument (except the special case for log_handler, due to
450            # action=append requiring a real default, so we cannot use the my_default workaround)
451            if getattr(opt, arg, None) is not None:
452                self.options[arg] = getattr(opt, arg)
453            # ... or keep, but cast, the config file value.
454            elif isinstance(self.options[arg], str) and self.casts[arg].type in optparse.Option.TYPE_CHECKER:
455                self.options[arg] = optparse.Option.TYPE_CHECKER[self.casts[arg].type](self.casts[arg], arg, self.options[arg])
456
457        if isinstance(self.options['log_handler'], str):
458            self.options['log_handler'] = self.options['log_handler'].split(',')
459        self.options['log_handler'].extend(opt.log_handler)
460
461        # if defined but None take the configfile value
462        keys = [
463            'language', 'translate_out', 'translate_in', 'overwrite_existing_translations',
464            'dev_mode', 'shell_interface', 'smtp_ssl', 'load_language',
465            'stop_after_init', 'without_demo', 'http_enable', 'syslog',
466            'list_db', 'proxy_mode',
467            'test_file', 'test_tags',
468            'osv_memory_count_limit', 'osv_memory_age_limit', 'transient_age_limit', 'max_cron_threads', 'unaccent',
469            'data_dir',
470            'server_wide_modules',
471        ]
472
473        posix_keys = [
474            'workers',
475            'limit_memory_hard', 'limit_memory_soft',
476            'limit_time_cpu', 'limit_time_real', 'limit_request', 'limit_time_real_cron'
477        ]
478
479        if os.name == 'posix':
480            keys += posix_keys
481        else:
482            self.options.update(dict.fromkeys(posix_keys, None))
483
484        # Copy the command-line arguments...
485        for arg in keys:
486            if getattr(opt, arg) is not None:
487                self.options[arg] = getattr(opt, arg)
488            # ... or keep, but cast, the config file value.
489            elif isinstance(self.options[arg], str) and self.casts[arg].type in optparse.Option.TYPE_CHECKER:
490                self.options[arg] = optparse.Option.TYPE_CHECKER[self.casts[arg].type](self.casts[arg], arg, self.options[arg])
491
492        self.options['root_path'] = self._normalize(os.path.join(os.path.dirname(__file__), '..'))
493        if not self.options['addons_path'] or self.options['addons_path']=='None':
494            default_addons = []
495            base_addons = os.path.join(self.options['root_path'], 'addons')
496            if os.path.exists(base_addons):
497                default_addons.append(base_addons)
498            main_addons = os.path.abspath(os.path.join(self.options['root_path'], '../addons'))
499            if os.path.exists(main_addons):
500                default_addons.append(main_addons)
501            self.options['addons_path'] = ','.join(default_addons)
502        else:
503            self.options['addons_path'] = ",".join(
504                self._normalize(x)
505                for x in self.options['addons_path'].split(','))
506
507        self.options["upgrade_path"] = (
508            ",".join(self._normalize(x)
509                for x in self.options['upgrade_path'].split(','))
510            if self.options['upgrade_path']
511            else ""
512        )
513
514        self.options['init'] = opt.init and dict.fromkeys(opt.init.split(','), 1) or {}
515        self.options['demo'] = (dict(self.options['init'])
516                                if not self.options['without_demo'] else {})
517        self.options['update'] = opt.update and dict.fromkeys(opt.update.split(','), 1) or {}
518        self.options['translate_modules'] = opt.translate_modules and [m.strip() for m in opt.translate_modules.split(',')] or ['all']
519        self.options['translate_modules'].sort()
520
521        dev_split = opt.dev_mode and  [s.strip() for s in opt.dev_mode.split(',')] or []
522        self.options['dev_mode'] = 'all' in dev_split and dev_split + ['pdb', 'reload', 'qweb', 'werkzeug', 'xml'] or dev_split
523
524        if opt.pg_path:
525            self.options['pg_path'] = opt.pg_path
526
527        self.options['test_enable'] = bool(self.options['test_tags'])
528
529        if opt.save:
530            self.save()
531
532        # normalize path options
533        for key in ['data_dir', 'logfile', 'pidfile', 'test_file', 'screencasts', 'screenshots', 'pg_path', 'translate_out', 'translate_in', 'geoip_database']:
534            self.options[key] = self._normalize(self.options[key])
535
536        conf.addons_paths = self.options['addons_path'].split(',')
537
538        conf.server_wide_modules = [
539            m.strip() for m in self.options['server_wide_modules'].split(',') if m.strip()
540        ]
541        return opt
542
543    def _warn_deprecated_options(self):
544        if self.options['osv_memory_age_limit']:
545            warnings.warn(
546                "The osv-memory-age-limit is a deprecated alias to "
547                "the transient-age-limit option, please use the latter.",
548                DeprecationWarning)
549            self.options['transient_age_limit'] = self.options.pop('osv_memory_age_limit')
550
551    def _is_addons_path(self, path):
552        from odoo.modules.module import MANIFEST_NAMES
553        for f in os.listdir(path):
554            modpath = os.path.join(path, f)
555            if os.path.isdir(modpath):
556                def hasfile(filename):
557                    return os.path.isfile(os.path.join(modpath, filename))
558                if hasfile('__init__.py') and any(hasfile(mname) for mname in MANIFEST_NAMES):
559                    return True
560        return False
561
562    def _check_addons_path(self, option, opt, value, parser):
563        ad_paths = []
564        for path in value.split(','):
565            path = path.strip()
566            res = os.path.abspath(os.path.expanduser(path))
567            if not os.path.isdir(res):
568                raise optparse.OptionValueError("option %s: no such directory: %r" % (opt, path))
569            if not self._is_addons_path(res):
570                raise optparse.OptionValueError("option %s: the path %r is not a valid addons directory" % (opt, path))
571            ad_paths.append(res)
572
573        setattr(parser.values, option.dest, ",".join(ad_paths))
574
575    def _check_upgrade_path(self, option, opt, value, parser):
576        upgrade_path = []
577        for path in value.split(','):
578            path = path.strip()
579            res = self._normalize(path)
580            if not os.path.isdir(res):
581                raise optparse.OptionValueError("option %s: no such directory: %r" % (opt, path))
582            if not self._is_upgrades_path(res):
583                raise optparse.OptionValueError("option %s: the path %r is not a valid upgrade directory" % (opt, path))
584            if res not in upgrade_path:
585                upgrade_path.append(res)
586        setattr(parser.values, option.dest, ",".join(upgrade_path))
587
588    def _is_upgrades_path(self, res):
589        return any(
590            glob.glob(os.path.join(res, f"*/*/{prefix}-*.py"))
591            for prefix in ["pre", "post", "end"]
592        )
593
594    def _test_enable_callback(self, option, opt, value, parser):
595        if not parser.values.test_tags:
596            parser.values.test_tags = "+standard"
597
598    def load(self):
599        outdated_options_map = {
600            'xmlrpc_port': 'http_port',
601            'xmlrpc_interface': 'http_interface',
602            'xmlrpc': 'http_enable',
603        }
604        p = ConfigParser.RawConfigParser()
605        try:
606            p.read([self.rcfile])
607            for (name,value) in p.items('options'):
608                name = outdated_options_map.get(name, name)
609                if value=='True' or value=='true':
610                    value = True
611                if value=='False' or value=='false':
612                    value = False
613                self.options[name] = value
614            #parse the other sections, as well
615            for sec in p.sections():
616                if sec == 'options':
617                    continue
618                self.misc.setdefault(sec, {})
619                for (name, value) in p.items(sec):
620                    if value=='True' or value=='true':
621                        value = True
622                    if value=='False' or value=='false':
623                        value = False
624                    self.misc[sec][name] = value
625        except IOError:
626            pass
627        except ConfigParser.NoSectionError:
628            pass
629
630    def save(self):
631        p = ConfigParser.RawConfigParser()
632        loglevelnames = dict(zip(self._LOGLEVELS.values(), self._LOGLEVELS))
633        p.add_section('options')
634        for opt in sorted(self.options):
635            if opt in ('version', 'language', 'translate_out', 'translate_in', 'overwrite_existing_translations', 'init', 'update'):
636                continue
637            if opt in self.blacklist_for_save:
638                continue
639            if opt in ('log_level',):
640                p.set('options', opt, loglevelnames.get(self.options[opt], self.options[opt]))
641            elif opt == 'log_handler':
642                p.set('options', opt, ','.join(_deduplicate_loggers(self.options[opt])))
643            else:
644                p.set('options', opt, self.options[opt])
645
646        for sec in sorted(self.misc):
647            p.add_section(sec)
648            for opt in sorted(self.misc[sec]):
649                p.set(sec,opt,self.misc[sec][opt])
650
651        # try to create the directories and write the file
652        try:
653            rc_exists = os.path.exists(self.rcfile)
654            if not rc_exists and not os.path.exists(os.path.dirname(self.rcfile)):
655                os.makedirs(os.path.dirname(self.rcfile))
656            try:
657                p.write(open(self.rcfile, 'w'))
658                if not rc_exists:
659                    os.chmod(self.rcfile, 0o600)
660            except IOError:
661                sys.stderr.write("ERROR: couldn't write the config file\n")
662
663        except OSError:
664            # what to do if impossible?
665            sys.stderr.write("ERROR: couldn't create the config directory\n")
666
667    def get(self, key, default=None):
668        return self.options.get(key, default)
669
670    def pop(self, key, default=None):
671        return self.options.pop(key, default)
672
673    def get_misc(self, sect, key, default=None):
674        return self.misc.get(sect,{}).get(key, default)
675
676    def __setitem__(self, key, value):
677        self.options[key] = value
678        if key in self.options and isinstance(self.options[key], str) and \
679                key in self.casts and self.casts[key].type in optparse.Option.TYPE_CHECKER:
680            self.options[key] = optparse.Option.TYPE_CHECKER[self.casts[key].type](self.casts[key], key, self.options[key])
681
682    def __getitem__(self, key):
683        return self.options[key]
684
685    @property
686    def addons_data_dir(self):
687        add_dir = os.path.join(self['data_dir'], 'addons')
688        d = os.path.join(add_dir, release.series)
689        if not os.path.exists(d):
690            try:
691                # bootstrap parent dir +rwx
692                if not os.path.exists(add_dir):
693                    os.makedirs(add_dir, 0o700)
694                # try to make +rx placeholder dir, will need manual +w to activate it
695                os.makedirs(d, 0o500)
696            except OSError:
697                logging.getLogger(__name__).debug('Failed to create addons data dir %s', d)
698        return d
699
700    @property
701    def session_dir(self):
702        d = os.path.join(self['data_dir'], 'sessions')
703        try:
704            os.makedirs(d, 0o700)
705        except OSError as e:
706            if e.errno != errno.EEXIST:
707                raise
708            assert os.access(d, os.W_OK), \
709                "%s: directory is not writable" % d
710        return d
711
712    def filestore(self, dbname):
713        return os.path.join(self['data_dir'], 'filestore', dbname)
714
715    def set_admin_password(self, new_password):
716        hash_password = crypt_context.hash if hasattr(crypt_context, 'hash') else crypt_context.encrypt
717        self.options['admin_passwd'] = hash_password(new_password)
718
719    def verify_admin_password(self, password):
720        """Verifies the super-admin password, possibly updating the stored hash if needed"""
721        stored_hash = self.options['admin_passwd']
722        if not stored_hash:
723            # empty password/hash => authentication forbidden
724            return False
725        result, updated_hash = crypt_context.verify_and_update(password, stored_hash)
726        if result:
727            if updated_hash:
728                self.options['admin_passwd'] = updated_hash
729            return True
730
731    def _normalize(self, path):
732        if not path:
733            return ''
734        return realpath(abspath(expanduser(expandvars(path.strip()))))
735
736
737config = configmanager()
738