Source code for oca.recipe.odoo.server

# coding: utf-8
import os
from os.path import join
import sys
import shutil
import logging
import zc.buildout
from zc.buildout import UserError
from .base import BaseRecipe
from . import devtools
from .utils import option_splitlines, option_strip, major_version

logger = logging.getLogger(__name__)

SERVER_COMMA_LIST_OPTIONS = ('log_handler', )


[docs]class ServerRecipe(BaseRecipe): """Recipe for server install and config """ release_filenames = { # no release since OpenERP 6.1, only nightlies } nightly_filenames = { '10.0': 'odoo_10.0.%s.tar.gz', 'trunk': 'odoo_10.0alpha1.%s.tar.gz' } """Name of expected nightly tarballs in base URL, by major version. """ recipe_requirements = ('babel',) requirements = ('oca.recipe.odoo',) soft_requirements = ('odoo-command',) with_gunicorn = False with_upgrade = True ws = None template_upgrade_script = os.path.join(os.path.dirname(__file__), 'upgrade.py.tmpl') server_wide_modules = () def __init__(self, *a, **kw): super(ServerRecipe, self).__init__(*a, **kw) opt = self.options self.with_devtools = ( opt.get('with_devtools', 'false').lower() == 'true') self.with_upgrade = self.options.get('upgrade_script') != '' # discarding, because we have a special behaviour with custom # interpreters opt.pop('interpreter', None) self.odoo_scripts = {} sw_modules = option_splitlines(opt.get('server_wide_modules')) if sw_modules and 'web' not in sw_modules: sw_modules = ('web', ) + sw_modules self.server_wide_modules = sw_modules if self.python_scripts_executable: # Monkeypatch the script headers to replace the python # executable by the one configured by the user new_header = '#!%s' % self.python_scripts_executable zc.buildout.easy_install.script_template = ( zc.buildout.easy_install.script_template.replace( zc.buildout.easy_install.script_header, new_header)) zc.buildout.easy_install.py_script_template = ( zc.buildout.easy_install.py_script_template.replace( zc.buildout.easy_install.script_header, new_header))
[docs] def apply_version_dependent_decisions(self): """Store some booleans depending on detected version. Also does some options normalization accordingly. Currently, there is only one Odoo version, this method will be really useful again in a while. """ gunicorn = self.options.get('gunicorn', '').strip().lower() self.with_gunicorn = bool(gunicorn) if gunicorn == 'proxied': self.options['options.proxy_mode'] = 'True' logger.warn("'gunicorn = proxied' now superseded since " "OpenERP 7 by the 'proxy_mode' Odoo server option ")
[docs] def merge_requirements(self, reqs=None): """Prepare for installation by zc.recipe.egg - develop the odoo distribution and require it - gunicorn's related dependencies if needed Once 'odoo' is required, zc.recipe.egg will take it into account and put it in needed scripts, interpreters etc. Historically, in ``anybox.recipe.openerp`` this used to take care of adding Pillow, which is now in Odoo's ``setup.py``. """ odoo_dir = getattr(self, 'odoo_dir', None) odoo_project_name = 'odoo' if odoo_dir is not None: # happens in unit tests odoo_project_name = self.develop(odoo_dir) self.requirements.append(odoo_project_name) if self.with_gunicorn: self.requirements.extend(('psutil', 'gunicorn')) if self.with_devtools: self.requirements.extend(devtools.requirements) BaseRecipe.merge_requirements(self, reqs=reqs) return odoo_project_name
def _create_default_config(self): """Have Odoo generate its default config file. """ self.options.setdefault('options.admin_passwd', '') sys.path.append(self.odoo_dir) sys.path.extend([egg.location for egg in self.ws]) try: from odoo.tools.config import configmanager except ImportError: from openerp.tools.config import configmanager configmanager(self.config_path).save() def _create_gunicorn_conf(self, qualified_name): """Put a gunicorn_PART.conf.py script in /etc. Derived from the standard gunicorn.conf.py shipping with Odoo. """ gunicorn_options = dict( workers='4', timeout='240', max_requests='2000', qualified_name=qualified_name, bind='%s:%s' % ( self.options.get('options.xmlrpc_interface', '0.0.0.0'), self.options.get('options.xmlrpc_port', '8069') )) gunicorn_prefix = 'gunicorn.' gunicorn_options.update((k[len(gunicorn_prefix):], v) for k, v in self.options.items() if k.startswith(gunicorn_prefix)) gunicorn_options['server_wide_modules'] = list( self.server_wide_modules) if self.server_wide_modules else ['web'] f = open(join(self.etc, qualified_name + '.conf.py'), 'w') conf = """'''Gunicorn configuration script. Generated by buildout. Do NOT edit.''' try: import openerp as odoo except ImportError: import odoo bind = %(bind)r pidfile = %(qualified_name)r + '.pid' workers = %(workers)s timeout = %(timeout)s max_requests = %(max_requests)s odoo.multi_process = True # needed even with only one worker odoo.conf.server_wide_modules = %(server_wide_modules)r conf = odoo.tools.config """ % gunicorn_options # forwarding specified options prefix = 'options.' for opt, val in self.options.items(): if not opt.startswith(prefix): continue opt = opt[len(prefix):] if opt == 'log_level': # blindly following the sample script val = dict(DEBUG=10, DEBUG_RPC=8, DEBUG_RPC_ANSWER=6, DEBUG_SQL=5, INFO=20, WARNING=30, ERROR=40, CRITICAL=50).get(val.strip().upper(), 30) if opt in SERVER_COMMA_LIST_OPTIONS: val = [i.strip() for i in val.split(',')] conf += 'conf[%r] = %r' % (opt, val) + os.linesep preload_dbs = option_splitlines(self.options.get( 'gunicorn.preload_databases')) if preload_dbs: conf += os.linesep.join(( "", "def post_fork(server, worker):", " '''Preload databases specified in buildout conf.'''", " from odoo.modules.registry import RegistryManager", " preload_dbs = %r" % (preload_dbs,), " for db_name in preload_dbs:", " server.log.info('Worker loading database %r',", " db_name)", " RegistryManager.get(db_name)", " server.log.info('Odoo databases %r loaded, '", " 'worker ready '", " 'to serve requests', preload_dbs)", )) f.write(conf) f.close() def _get_server_command(self): """Return a full path to the main Odoo server command.""" if major_version(self.version_detected)[0] >= 10: base_name = 'odoo-bin' else: base_name = 'openerp-server' return join(self.odoo_dir, base_name) def _parse_odoo_scripts(self): """Parse required scripts from conf.""" scripts = self.odoo_scripts if 'odoo_scripts' not in self.options: return for line in option_splitlines(self.options.get('odoo_scripts')): line = line.split() naming = line[0].split('=') if not naming or len(naming) > 2: raise UserError("Invalid script specification %r" % line[0]) elif len(naming) == 1: name = '_'.join((naming[0], self.name)) else: name = naming[1] cl_options = [] desc = scripts[name] = dict(entry=naming[0], command_line_options=cl_options) opt_prefix = 'command-line-options=' arg_prefix = 'arguments=' log_prefix = 'odoo-log-level=' for token in line[1:]: if token.startswith(opt_prefix): cl_options.extend(token[len(opt_prefix):].split(',')) elif token.startswith(arg_prefix): desc['arguments'] = token[len(arg_prefix):] elif token.startswith(log_prefix): level = token[len(log_prefix):].upper() if level not in dir(logging): raise UserError("In script %r, improper logging " "level %r" % (name, level)) desc['odoo_log_level'] = level else: raise UserError( "Invalid token for script %r: %r" % (name, token)) def _get_or_create_script(self, entry, name=None): """Retrieve or create a registered script by its entry point. If create_name is not given, no creation will occur, will return None if not found. In all other cases, return return (script_name, desc). """ for script_name, desc in self.odoo_scripts.items(): if desc['entry'] == entry: return script_name, desc if name is not None: desc = self.odoo_scripts[name] = dict(entry=entry) return name, desc def _relativitize(self, path): if self._relative_paths: return "join(base, %r)" % os.path.relpath( path, self._relative_paths) return "%r" % path def _register_main_startup_script(self, qualified_name): """Register main startup script, usually ``start_odoo`` for install. """ desc = self._get_or_create_script('odoo_starter', name=qualified_name)[1] arguments = '%s, %s, version=%r, gevent_script_path=%s' % ( self._relativitize(self._get_server_command()), self._relativitize(self.config_path), self.major_version, self._relativitize(self.gevent_script_path)) if self.server_wide_modules: arguments += ', server_wide_modules=%r' % ( self.server_wide_modules,) desc.update(arguments=arguments) startup_delay = float(self.options.get('startup_delay', 0)) initialization = [''] if self.with_devtools: initialization.extend(( 'from oca.recipe.odoo import devtools', 'devtools.load(for_tests=False)', '')) if startup_delay: initialization.extend( ('print("sleeping %s seconds...")' % startup_delay, 'import time', 'time.sleep(%f)' % startup_delay)) desc['initialization'] = os.linesep.join((initialization)) def _register_test_script(self, qualified_name): """Register the main test script for installation. """ desc = self._get_or_create_script('odoo_tester', name=qualified_name)[1] arguments = '%s, %s, version=%r, just_test=True' % ( self._relativitize(self._get_server_command()), self._relativitize(self.config_path), self.major_version) arguments += ', gevent_script_path=%s' % self._relativitize( self.gevent_script_path) desc.update( entry='odoo_starter', initialization=os.linesep.join(( "from oca.recipe.odoo import devtools", "devtools.load(for_tests=True)", "")), arguments=arguments ) def _register_upgrade_script(self, qualified_name): desc = self._get_or_create_script('odoo_upgrader', name=qualified_name)[1] script_opt = option_strip(self.options.get('upgrade_script', 'upgrade.py run')) script = script_opt.split() if len(script) != 2: # TODO add console script entry point support raise zc.buildout.UserError( ("upgrade_script option must take the form " "SOURCE_FILE CALLABLE (got '%r')" % script)) script_source_path = self.make_absolute(script[0]) desc.update( entry='odoo_upgrader', arguments='%s, %r, %s, %s' % ( self._relativitize(script_source_path), script[1], self._relativitize(self.config_path), self._relativitize(self.buildout_dir)), ) if not os.path.exists(script_source_path): logger.warning("Ugrade script source %s does not exist." "Initializing it for you", script_source_path) shutil.copy(self.template_upgrade_script, script_source_path) def _register_gunicorn_startup_script(self, qualified_name): """Register a gunicorn foreground start script for installation. The produced script is suitable for external process management, such as provided by supervisor. """ desc = self._get_or_create_script('gunicorn', name=qualified_name)[1] gunicorn_options = {} gunicorn_prefix = 'gunicorn.' gunicorn_options.update((k[len(gunicorn_prefix):], v) for k, v in self.options.items() if k.startswith(gunicorn_prefix)) gunicorn_entry_point = gunicorn_options.get('entry_point') if gunicorn_entry_point is None: gunicorn_entry_point = ('odoo:' 'service.wsgi_server.application') # gunicorn's main() does not take arguments, that's why we have # to resort on hacking sys.argv desc['initialization'] = ( "from sys import argv; argv[1:] = ['%s', '-c', '%s.conf.py']" % ( gunicorn_entry_point, self._relativitize(join(self.etc, qualified_name)))) def _register_gevent_script(self, qualified_name): """Register the gevent startup script """ desc = self._get_or_create_script('odoo-gevent', name=qualified_name)[1] initialization = [ "import gevent.monkey", "gevent.monkey.patch_all()", "import psycogreen.gevent", "psycogreen.gevent.patch_psycopg()", ""] if self.with_devtools: initialization.extend([ 'from oca.recipe.odoo import devtools', 'devtools.load(for_tests=False)', '']) desc['initialization'] = os.linesep.join(initialization) def _register_cron_worker_startup_script(self, qualified_name): """Register the cron worker script for installation. This worker script has been introduced in openobject-server, rev 4184 together with changes in the main code that it requires. These changes appeared in nightly build 6.1-20120530-233414. The worker script itself does not appear in nightly builds. """ script_src = join(self.odoo_dir, 'odoo-cron-worker') if not os.path.isfile(script_src): version = self.version_detected if self.version_wanted == '6.1-1' or ( version.startswith('6.1-2012') and version[4:12] < '20120530'): logger.warn( "Can't use odoo-cron-worker with version %s " "You have to run a separate regular Odoo process " "for cron jobs to be launched.", version) return logger.info("Cron launcher odoo-cron-worker not found in " "odoo source tree (version %s). " "This is expected with some nightly builds. " "Using the launcher script distributed " "with the recipe.", version) script_src = join(os.path.split(__file__)[0], 'odoo-cron-worker') desc = self._get_or_create_script('odoo_cron_worker', name=qualified_name)[1] desc.update(entry='odoo_cron_worker', arguments='%s, %s' % ( self._relativitize(script_src), self._relativitize(self.config_path)), initialization='', ) def _install_interpreter(self): """Install a python interpreter with a ready-made session object.""" int_name = self.options.get('interpreter_name', None) if int_name == '': # conf requires not to build an interpreter return elif int_name is None: int_name = 'python_' + self.name initialization = os.linesep.join(( "", "from oca.recipe.odoo.runtime.session import Session", "session = Session(%s, %s)" % ( self._relativitize(self.config_path), self._relativitize(self.buildout_dir), ), "if len(sys.argv) <= 1:", " print('To start the Odoo working session, just do:')", " print(' session.open(db=DATABASE_NAME)')", " print('or, to use the database from the buildout " "part config:')", " print(' session.open()')", " print('All other options from buildout part config " "do apply.')", "" " print('Then you can issue commands such as:')", " print(\"" " session.registry('res.users').browse(session.cr, 1, 1)\")", " try:", " from openerp import release", " except ImportError:", " from odoo import release", " from oca.recipe.odoo.utils import major_version", " if major_version(release.version)[0] >= 8:", " print('Or using new api:')", " print(\" session.env['res.users'].browse(1)\")" "")) reqs, ws = self.eggs_reqs, self.eggs_ws return zc.buildout.easy_install.scripts( reqs, ws, sys.executable, self.options['bin-directory'], scripts={}, interpreter=int_name, initialization=initialization, arguments=self.options.get('arguments', ''), extra_paths=self.extra_paths, relative_paths=self._relative_paths, ) def _install_odoo_scripts(self): """Install scripts registered in self.odoo_scripts. If initialization string is not passed, one will be cooked for - session initialization - treatment of Odoo options specific to this script, as required in the 'options' key of the scripts descrition (typically to add a database opening option to the provided script). """ reqs, ws = self.eggs_reqs, self.eggs_ws common_init = os.linesep.join(( "", "from oca.recipe.odoo.runtime.session import Session", "session = Session(%s, %s)" % ( self._relativitize(self.config_path), self._relativitize(self.buildout_dir)), )) for script_name, desc in self.odoo_scripts.items(): initialization = desc.get('initialization', common_init) log_level = desc.get('odoo_log_level') if log_level: initialization = os.linesep.join(( initialization, "import logging", "logging.getLogger('odoo').setLevel" "(logging.%s)" % log_level)) options = desc.get('command_line_options') if options: initialization = os.linesep.join(( initialization, "session.handle_command_line_options(%r)" % options)) zc.buildout.easy_install.scripts( reqs, ws, sys.executable, self.bin_dir, scripts={desc['entry']: script_name}, interpreter='', initialization=initialization, arguments=desc.get('arguments', ''), extra_paths=self.extra_paths, relative_paths=self._relative_paths, ) self.odoo_installed.append(join(self.bin_dir, script_name)) def _install_startup_scripts(self): """install startup and control scripts. """ self._parse_odoo_scripts() # provide additional needed entry points for main start/test scripts self.eggs_reqs.extend(( ('odoo_starter', 'oca.recipe.odoo.runtime.start_odoo', 'main'), ('odoo_cron_worker', 'oca.recipe.odoo.runtime.start_odooo', 'main'), ('odoo_upgrader', 'oca.recipe.odoo.runtime.upgrade', 'upgrade'), )) if major_version(self.version_detected)[0] >= 10: self.eggs_reqs.append( ('odoo-gevent', 'odoo.cli', 'main'), ) else: self.eggs_reqs.append( ('odoo-gevent', 'openerp.cli', 'main'), ) self._install_interpreter() main_script = self.options.get('script_name', 'start_' + self.name) gevent_script_name = self.options.get('gevent_script_name', 'gevent_%s' % self.name) self._register_gevent_script(gevent_script_name) self.gevent_script_path = join(self.bin_dir, gevent_script_name) self._register_main_startup_script(main_script) self.script_path = join(self.bin_dir, main_script) if self.with_devtools: self._register_test_script( self.options.get('test_script_name', 'test_' + self.name)) if self.with_gunicorn: qualified_name = self.options.get('gunicorn_script_name', 'gunicorn_%s' % self.name) self._create_gunicorn_conf(qualified_name) self._register_gunicorn_startup_script(qualified_name) qualified_name = self.options.get('cron_worker_script_name', 'cron_worker_%s' % self.name) self._register_cron_worker_startup_script(qualified_name) if self.with_upgrade: qualified_name = self.options.get('upgrade_script_name', 'upgrade_%s' % self.name) self._register_upgrade_script(qualified_name) self._install_odoo_scripts()