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