Source code for anybox.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

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 = { '8.0': 'odoo_8.0.%s.tar.gz', 'trunk': 'odoo_9.0alpha1.%s.tar.gz' } """Name of expected nightly tarballs in base URL, by major version. """ recipe_requirements = ('babel',) requirements = ('pychart', 'anybox.recipe.odoo') soft_requirements = ('openerp-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.openerp_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
[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 openerp distribution and require it - gunicorn's related dependencies if needed Once 'openerp' 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``. """ openerp_dir = getattr(self, 'openerp_dir', None) openerp_project_name = 'openerp' if openerp_dir is not None: # happens in unit tests openerp_project_name = self.develop(openerp_dir) self.requirements.append(openerp_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 openerp_project_name
def _create_default_config(self): """Have Odoo generate its default config file. """ self.options.setdefault('options.admin_passwd', '') sys.path.append(self.openerp_dir) sys.path.extend([egg.location for egg in self.ws]) 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.''' import openerp bind = %(bind)r pidfile = %(qualified_name)r + '.pid' workers = %(workers)s timeout = %(timeout)s max_requests = %(max_requests)s openerp.multi_process = True # needed even with only one worker openerp.conf.server_wide_modules = %(server_wide_modules)r conf = openerp.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 openerp.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.""" return join(self.openerp_dir, 'openerp-server') def _parse_openerp_scripts(self): """Parse required scripts from conf.""" scripts = self.openerp_scripts if 'openerp_scripts' not in self.options: return for line in option_splitlines(self.options.get('openerp_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 = 'openerp-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['openerp_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.openerp_scripts.iteritems(): if desc['entry'] == entry: return script_name, desc if name is not None: desc = self.openerp_scripts[name] = dict(entry=entry) return name, desc def _register_main_startup_script(self, qualified_name): """Register main startup script, usually ``start_openerp`` for install. """ desc = self._get_or_create_script('openerp_starter', name=qualified_name)[1] arguments = '%r, %r, version=%r, gevent_script_path=%r' % ( self._get_server_command(), self.config_path, self.major_version, 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 anybox.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('openerp_tester', name=qualified_name)[1] arguments = '%r, %r, version=%r, just_test=True' % ( self._get_server_command(), self.config_path, self.major_version) arguments += ', gevent_script_path=%r' % self.gevent_script_path desc.update( entry='openerp_starter', initialization=os.linesep.join(( "from anybox.recipe.odoo import devtools", "devtools.load(for_tests=True)", "")), arguments=arguments ) def _register_upgrade_script(self, qualified_name): desc = self._get_or_create_script('openerp_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='openerp_upgrader', arguments='%r, %r, %r, %r' % ( script_source_path, script[1], self.config_path, 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 = ('openerp:' '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, join(self.etc, qualified_name))) def _register_gevent_script(self, qualified_name): """Register the gevent startup script """ desc = self._get_or_create_script('openerp-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 anybox.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.openerp_dir, 'openerp-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 openerp-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 openerp-cron-worker not found in " "openerp 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], 'openerp-cron-worker') desc = self._get_or_create_script('openerp_cron_worker', name=qualified_name)[1] desc.update(entry='openerp_cron_worker', arguments='%r, %r' % (script_src, 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 anybox.recipe.odoo.runtime.session import Session", "session = Session(%r, %r)" % (self.config_path, 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)\")", " from openerp import release", " from anybox.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, # TODO investigate these options: # relative_paths=self._relative_paths, ) def _install_openerp_scripts(self): """Install scripts registered in self.openerp_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 anybox.recipe.odoo.runtime.session import Session", "session = Session(%r, %r)" % (self.config_path, self.buildout_dir), )) for script_name, desc in self.openerp_scripts.items(): initialization = desc.get('initialization', common_init) log_level = desc.get('openerp_log_level') if log_level: initialization = os.linesep.join(( initialization, "import logging", "logging.getLogger('openerp').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', ''), # TODO investigate these options: extra_paths=self.extra_paths, # relative_paths=self._relative_paths, ) self.openerp_installed.append(join(self.bin_dir, script_name)) def _install_startup_scripts(self): """install startup and control scripts. """ self._parse_openerp_scripts() # provide additional needed entry points for main start/test scripts self.eggs_reqs.extend(( ('openerp_starter', 'anybox.recipe.odoo.runtime.start_openerp', 'main'), ('openerp_cron_worker', 'anybox.recipe.odoo.runtime.start_openerp', 'main'), ('openerp-gevent', 'openerp.cli', 'main'), ('openerp_upgrader', 'anybox.recipe.odoo.runtime.upgrade', 'upgrade'), )) 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_openerp_scripts()