Merge lp:~camptocamp/anybox.recipe.openerp/20121204-add-daemon into lp:anybox.recipe.openerp

Proposed by Yannick Vaucher @ Camptocamp
Status: Needs review
Proposed branch: lp:~camptocamp/anybox.recipe.openerp/20121204-add-daemon
Merge into: lp:anybox.recipe.openerp
Diff against target: 437 lines (+319/-11)
7 files modified
anybox/recipe/openerp/base.py (+4/-1)
anybox/recipe/openerp/server.py (+124/-10)
anybox/recipe/openerp/templates/__init__.py (+1/-0)
anybox/recipe/openerp/templates/initd/__init__.py (+1/-0)
anybox/recipe/openerp/templates/initd/gunicorn_openerp_server.initd.in (+109/-0)
anybox/recipe/openerp/templates/initd/openerp_server.initd.in (+79/-0)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~camptocamp/anybox.recipe.openerp/20121204-add-daemon
Reviewer Review Type Date Requested Status
Anybox Pending
Review via email: mp+137773@code.launchpad.net

Description of the change

Here is a merge proposal to insert the daemonization of openerp

It generates the following dirs and files:

in buildout dir:
- run (for pid files)
- init.d (for daemon launchers)
- log (for logs)

in init.d:
- openerp_server.initd
- gunicorn_openerp_server.initd

Those executable scripts are defined via templates in server.py of the recipe

New params in buildout config :
daemon.sys_user (used to launch daemon with a specific user if not set use current user)
daemon.current_instance (gives an instance name used to name pid file and daemon description)

Maybe this should be more optional or more generic ?
What is sure is that on our side in Camptocamp, we needs to be able to run openerp in daemon mode

To post a comment you must log in.
Revision history for this message
Alexandre Fayolle - camptocamp (alexandre-fayolle-c2c) wrote :

LGTM

Revision history for this message
Georges Racinet (gracinet) wrote :

Just did a really quick read. That looks overall good, but I (we) will need a bit more time to review/merge that one.

About the use-case itself, at Anybox, we use supervisor to handle daemonization, console logs and startup. That explains the lack of such in the recipe. We have self-contained instances, with a supervisor part with the dedicated recipe as well as mutualizing servers, where a distribution-installed supervisor takes care of several buildout-built instances.

In the future, we may switch to circus (looks promising). Some other people may use other supervision processes, so I think we should maybe introduce a class of options for that purpose, in order to avoid confusion with too many different configuration files/scripts being generated.

Revision history for this message
Georges Racinet (gracinet) wrote :

How do you plan to handle the script (cron_worker_openerp) that handles ir.cron jobs ?

This is tricky, because that script needs a (list of) database names, and we already have use cases where limitation to thoses databases belonging to the system user won't be enough.

Init/update of the database through the recipe is not far behind that one. For the time being, we can live with a simple option to declare the database, even if that's a bit odd (bin/buildout would generate a rc script that can't work until the listed databases have been created).

We had long internal discussions about these issues, with no clear consensus. c2c's input would be appreciated.

Revision history for this message
Georges Racinet (gracinet) wrote :

Finally, this will require updates to README/CHANGES and, of course, new tests. About these, in case test_server.py isn't clear enough or appropriate, I can help.

Revision history for this message
Georges Racinet (gracinet) wrote :

Thank you, it's great to see new use cases and momentum.

We don't need to solve all the concerns expressed in the above comments for the contribution to be mergeable. Reaching a consistent state with room for further developments will be enough.

I won't have much more time for this today, but will check for new inputs at least tomorrow.

Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

Hello,

My colleague Yannick will have a look on your remarks, though not sure he will have enough time today.

By the way, we may consider using a supervisor.
Is it actually a part of the buildout? I can see that the supervisor egg is in the recipes of the buildbot but do not find any configuration.

Why not to use collective.recipe.supervisor ?

Subsidiary question: Can we have more than one buildout deployed on 1 server, each one of them having their own self-contained supervisor ?

Revision history for this message
Christophe Combelles (ccomb) wrote :

Yes for a single instance we use collective.recipe.supervisor, that's perfect for quickly setting up a supervisor. For example we used it for a complex project with a libreoffice server (aeroo report), a django frontend with a cherrypy server and a Celery server for asynchronous jobs.
We use a single system-wide supervisor to manage multiple OpenERP instances from several buildouts.

Revision history for this message
Georges Racinet (gracinet) wrote :

> By the way, we may consider using a supervisor.
> Is it actually a part of the buildout? I can see that the supervisor egg is in
> the recipes of the buildbot but do not find any configuration.

That egg is indeed listed in [versions] of the buildbot/*.cfg, you are right. But it's a remnant from another configuration that had a supervisor part, it's not actually used.

Note that egg versions are shared among parts.

>
> Why not to use collective.recipe.supervisor ?

That's what we do as "local supervisors", sorry if I haven't been clear enough about that.

It wouldn't be the job of the openerp recipe to install supervisor.
A standalone, ready to go daemonized version with an example init script is interesting, especially if it needs some OpenERP specifics (like database declaration), meaning that it has a clear added value over an external approach, but there's a line to be drawn where the recipe responsibility should end, and that should be imho at the point where other recipes (supervisor) or the general OS environment (systemd/upstart/whatever) can interface themselves easily.

> Subsidiary question: Can we have more than one buildout deployed on 1 server,
> each one of them having their own self-contained supervisor ?

Yes, you probably can, if you take care to have them use separate control sockets, but I've not done this myself. I suppose that the supervisor recipe config defaults are sensible (in particular, Unix domain socket file should be local).

Then of course, you'll need an init/systemd/upstart to launch all these supervisors. The local supervisor is handy to multiplex all the needed subprocesses for that openerp installation (gunicorn, cron_worker, specific stuff) as one "service".

We prefer to use the supervisor provided by the distro with a process group per buildout, because rc script is provided out of the box. That implies that access to the web UI must be restricted to our staff (fine for us).

Revision history for this message
Guewen Baconnier @ Camptocamp (gbaconnier-c2c) wrote :

Thanks for all your explanations. I totally understand and agree.

I found an example a local supervisord setup using collective.recipe.supervisor
https://github.com/florentx/openobject-mirliton/blob/buildout/etc/buildout-base.cfg

This seems really good.

We'll do some tests and share the results. It would be nice then to have some documentation with an example of configuration. If our experiments are fine, we'll write it.

Revision history for this message
Georges Racinet (gracinet) wrote :

Hi,

we are on the verge of releasing version 1.4, mainly for the extraction-freeze features.

What are the results of your investigations ? Do you still need this ? Can it wait until, e.g, a point release or the next major release (1.5).

Regards,

Revision history for this message
Yannick Vaucher @ Camptocamp (yvaucher-c2c) wrote :

Yes, we can wait on this. Finally we created a configuration using supervisor and though it isn't used in production yet we are moving to it for our next projects.

Unmerged revisions

250. By Yannick Vaucher @ Camptocamp

[ADD] daemon launcher for standalone and gunicorn mode

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'anybox/recipe/openerp/base.py'
--- anybox/recipe/openerp/base.py 2012-11-20 10:25:02 +0000
+++ anybox/recipe/openerp/base.py 2012-12-04 08:56:24 +0000
@@ -57,6 +57,7 @@
5757
58 self.downloads_dir = self.make_absolute(58 self.downloads_dir = self.make_absolute(
59 self.b_options.get('openerp-downloads-directory', 'downloads'))59 self.b_options.get('openerp-downloads-directory', 'downloads'))
60 self.daemon_dir = self.make_absolute('init.d')
60 self.version_wanted = None # from the buildout61 self.version_wanted = None # from the buildout
61 self.version_detected = None # string from the openerp setup.py62 self.version_detected = None # string from the openerp setup.py
62 self.parts = self.buildout['buildout']['parts-directory']63 self.parts = self.buildout['buildout']['parts-directory']
@@ -81,9 +82,11 @@
81 self.openerp_installed = []82 self.openerp_installed = []
8283
83 self.etc = self.make_absolute('etc')84 self.etc = self.make_absolute('etc')
85 self.log_dir = self.make_absolute('log')
86 self.run_dir = self.make_absolute('run')
84 self.bin_dir = self.buildout['buildout']['bin-directory']87 self.bin_dir = self.buildout['buildout']['bin-directory']
85 self.config_path = join(self.etc, self.name + '.cfg')88 self.config_path = join(self.etc, self.name + '.cfg')
86 for d in self.downloads_dir, self.etc:89 for d in self.downloads_dir, self.etc, self.daemon_dir, self.log_dir, self.run_dir:
87 if not os.path.exists(d):90 if not os.path.exists(d):
88 logger.info('Created %s/ directory' % basename(d))91 logger.info('Created %s/ directory' % basename(d))
89 os.mkdir(d)92 os.mkdir(d)
9093
=== modified file 'anybox/recipe/openerp/server.py'
--- anybox/recipe/openerp/server.py 2012-11-29 17:10:20 +0000
+++ anybox/recipe/openerp/server.py 2012-12-04 08:56:24 +0000
@@ -1,14 +1,21 @@
1# coding: utf-81# coding: utf-8
2import os2import os
3import stat
4import getpass
3from os.path import join5from os.path import join
4import sys, logging6import sys
7import logging
5import subprocess8import subprocess
6import zc.buildout9import zc.buildout
10from mako.template import Template
7from anybox.recipe.openerp import devtools11from anybox.recipe.openerp import devtools
8from anybox.recipe.openerp.base import BaseRecipe12from anybox.recipe.openerp.base import BaseRecipe
913
14from pkg_resources import resource_string
15
10logger = logging.getLogger(__name__)16logger = logging.getLogger(__name__)
1117
18
12class ServerRecipe(BaseRecipe):19class ServerRecipe(BaseRecipe):
13 """Recipe for server install and config20 """Recipe for server install and config
14 """21 """
@@ -101,6 +108,112 @@
101108
102 BaseRecipe.merge_requirements(self)109 BaseRecipe.merge_requirements(self)
103110
111 def _create_daemon_launcher(self):
112 """Generate daemon laucher in a init.d directory.
113 """
114 template = resource_string(__name__, join('templates', 'initd','openerp_server.initd.in'))
115
116 daemon_options = {}
117 daemon_prefix = 'daemon.'
118 daemon_options.update((k[len(daemon_prefix):], v)
119 for k, v in self.options.items()
120 if k.startswith(daemon_prefix))
121
122 system_user = daemon_options.get('sys_user')
123 current_instance = daemon_options.get('current_instance')
124 daemon_params = {
125 'path': '/sbin:/bin:/usr/sbin:/usr/bin',
126 'pypath': '/usr/bin/python',
127 'daemon': self.script_path,
128 'config': self.config_path,
129 'name': 'openerp-server',
130 'pid': join(self.buildout_dir, 'run', current_instance + '.pid'),
131 'desc': 'openerp-server-' + current_instance,
132 'logfile': join(self.log_dir, 'server.log'),
133 'user': system_user,
134 }
135
136 template = template % daemon_params
137
138 daemon_file_name = join(self.daemon_dir, 'openerp_server.initd')
139
140 new_file = open(daemon_file_name, 'w')
141 new_file.write(template)
142 new_file.close()
143
144 os.chmod(daemon_file_name, stat.S_IRWXU)
145 self.openerp_installed.append(daemon_file_name)
146
147 def _get_gunicorn_entry_point(self, gunicorn_options):
148 # entry point in 7.0 has changed so we need it to be configurable
149 gunicorn_entry_point = gunicorn_options.get('entry_point')
150 if gunicorn_entry_point is None:
151 if self.major_version >= (6, 2):
152 # proxy vs direct now handled by an OpenERP server option
153 gunicorn_entry_point = 'openerp:service.wsgi_server.application'
154 else:
155 gunicorn_entry_point = (
156 'openerp:wsgi.%s.application' % self.gunicorn_entry)
157 return gunicorn_entry_point
158
159
160 def _create_gunicorn_daemon_launcher(self):
161 """Generate daemon laucher in a init.d directory with gunicorn support.
162 """
163 template = resource_string(__name__, join('templates', 'initd','gunicorn_openerp_server.initd.in'))
164
165 daemon_options = {}
166 gunicorn_options = {}
167 daemon_prefix = 'daemon.'
168 gunicorn_prefix = 'gunicorn.'
169
170 daemon_options.update((k[len(daemon_prefix):], v)
171 for k, v in self.options.items()
172 if k.startswith(daemon_prefix))
173
174 system_user = daemon_options.get(
175 'sys_user',
176 getpass.getuser())
177 current_instance = daemon_options.get('current_instance')
178
179 gunicorn_options.update((k[len(gunicorn_prefix):], v)
180 for k, v in self.options.items()
181 if k.startswith(gunicorn_prefix))
182 gunicorn_entry_point = self._get_gunicorn_entry_point(gunicorn_options)
183
184 database = self.options.get('options.db_name')
185 version = self.version_detected
186 daemon_params = {
187 'path': '/sbin:/bin:/usr/sbin:/usr/bin',
188 'pypath': '/usr/bin/python',
189 'daemon': join(self.bin_dir, 'gunicorn_openerp'),
190 'config': join(self.etc, 'gunicorn_openerp.conf.py'),
191 'app_module': gunicorn_entry_point,
192 'name': 'gunicorn-openerp-server',
193 'desc': 'gunicorn-openerp-server-' + current_instance,
194 'pid': join(self.buildout_dir, 'run', 'gunicorn_' + current_instance + '.pid'),
195 'logfile': join(self.log_dir, 'server.log'),
196 'user': system_user,
197
198 'cron_pid': join(self.buildout_dir, 'run', 'cron_worker_' + current_instance + '.pid'),
199 'cron_daemon': join(self.bin_dir, 'cron_worker_openerp'),
200 'cron_daemon_opts': '--database=' + database,
201 'cron_config': self.config_path,
202 'version': version,
203 }
204
205 template = Template(template).render(**daemon_params)
206
207 daemon_file_name = join(self.daemon_dir, 'gunicorn_openerp_server.initd')
208
209 new_file = open(daemon_file_name, 'w')
210 new_file.write(template)
211 new_file.close()
212
213 os.chmod(daemon_file_name, stat.S_IRWXU)
214 self.openerp_installed.append(daemon_file_name)
215
216
104 def _create_default_config(self):217 def _create_default_config(self):
105 """Have OpenERP generate its default config file.218 """Have OpenERP generate its default config file.
106 """219 """
@@ -135,11 +248,13 @@
135 if k.startswith(gunicorn_prefix))248 if k.startswith(gunicorn_prefix))
136249
137 f = open(join(self.etc, qualified_name + '.conf.py'), 'w')250 f = open(join(self.etc, qualified_name + '.conf.py'), 'w')
251 pidfile = join(self.run_dir, qualified_name + '.pid')
252 gunicorn_options.update({'pidfile': pidfile})
138 conf = """'''Gunicorn configuration script.253 conf = """'''Gunicorn configuration script.
139Generated by buildout. Do NOT edit.'''254Generated by buildout. Do NOT edit.'''
140import openerp255import openerp
141bind = %(bind)r256bind = %(bind)r
142pidfile = %(qualified_name)r + '.pid'257pidfile = %(pidfile)r
143workers = %(workers)s258workers = %(workers)s
144259
145if openerp.release.major_version == '6.1':260if openerp.release.major_version == '6.1':
@@ -191,14 +306,7 @@
191 for k, v in self.options.items()306 for k, v in self.options.items()
192 if k.startswith(gunicorn_prefix))307 if k.startswith(gunicorn_prefix))
193308
194 gunicorn_entry_point = gunicorn_options.get('entry_point')309 gunicorn_entry_point = self._get_gunicorn_entry_point(gunicorn_options)
195 if gunicorn_entry_point is None:
196 if self.major_version >= (6, 2):
197 # proxy vs direct now handled by an OpenERP server option
198 gunicorn_entry_point = 'openerp:service.wsgi_server.application'
199 else:
200 gunicorn_entry_point = (
201 'openerp:wsgi.%s.application' % self.gunicorn_entry)
202310
203 # gunicorn's main() does not take arguments, that's why we have311 # gunicorn's main() does not take arguments, that's why we have
204 # to resort on hacking sys.argv312 # to resort on hacking sys.argv
@@ -345,6 +453,9 @@
345453
346 self._install_main_startup_script()454 self._install_main_startup_script()
347455
456 if os.name == 'posix':
457 self._create_daemon_launcher()
458
348 if self.with_openerp_command:459 if self.with_openerp_command:
349 self._install_openerp_command(460 self._install_openerp_command(
350 self.options.get('openerp_command_name',461 self.options.get('openerp_command_name',
@@ -363,6 +474,9 @@
363 'cron_worker_%s' % self.name)474 'cron_worker_%s' % self.name)
364 self._install_cron_worker_startup_script(qualified_name)475 self._install_cron_worker_startup_script(qualified_name)
365476
477 if os.name == 'posix':
478 self._create_gunicorn_daemon_launcher()
479
366 def _60_fix_root_path(self):480 def _60_fix_root_path(self):
367 """Correction of root path for OpenERP 6.0 pure python install"""481 """Correction of root path for OpenERP 6.0 pure python install"""
368482
369483
=== added directory 'anybox/recipe/openerp/templates'
=== added file 'anybox/recipe/openerp/templates/__init__.py'
--- anybox/recipe/openerp/templates/__init__.py 1970-01-01 00:00:00 +0000
+++ anybox/recipe/openerp/templates/__init__.py 2012-12-04 08:56:24 +0000
@@ -0,0 +1,1 @@
1# This is a python package
02
=== added directory 'anybox/recipe/openerp/templates/initd'
=== added file 'anybox/recipe/openerp/templates/initd/__init__.py'
--- anybox/recipe/openerp/templates/initd/__init__.py 1970-01-01 00:00:00 +0000
+++ anybox/recipe/openerp/templates/initd/__init__.py 2012-12-04 08:56:24 +0000
@@ -0,0 +1,1 @@
1# This is a python package
02
=== added file 'anybox/recipe/openerp/templates/initd/gunicorn_openerp_server.initd.in'
--- anybox/recipe/openerp/templates/initd/gunicorn_openerp_server.initd.in 1970-01-01 00:00:00 +0000
+++ anybox/recipe/openerp/templates/initd/gunicorn_openerp_server.initd.in 2012-12-04 08:56:24 +0000
@@ -0,0 +1,109 @@
1#!/bin/sh
2
3### BEGIN INIT INFO
4# Provides: openerp-server
5# Required-Start: $remote_fs $syslog
6# Required-Stop: $remote_fs $syslog
7# Should-Start: $network
8# Should-Stop: $network
9# Default-Start: 2 3 4 5
10# Default-Stop: 0 1 6
11# Short-Description: Enterprise Resource Management software
12# Description: Open ERP is a complete ERP and CRM software.
13### END INIT INFO
14
15PATH=${path}
16PYTPATH=${pypath}
17DAEMON=${daemon}
18APP_MODULE=${app_module}
19NAME=${name}
20PID=${pid}
21DESC=${desc}
22CONFIG=${config}
23LOGFILE=${logfile}
24USER=${user}
25
26<%
27use_cron_daemon = version.startswith('6.1')
28%>
29
30%if use_cron_daemon:
31CRON_PID=${cron_pid}
32CRON_DAEMON=${cron_daemon}
33CRON_DAEMON_OPTS="${cron_daemon_opts}"
34CRON_CONFIG=${cron_config}
35%endif
36
37test -x ${'$'}{DAEMON} || exit 0
38
39
40%if use_cron_daemon:
41test -x ${'$'}{CRON_DAEMON} || exit 0
42%endif
43
44set -e
45
46do_start()
47{
48<%text>
49 start-stop-daemon --start --quiet --pidfile ${PID} \
50 --chuid ${USER} --background --make-pidfile \
51 --exec ${DAEMON} -- ${APP_MODULE} --config=${CONFIG}
52</%text>
53%if use_cron_daemon:
54<%text>
55 start-stop-daemon --start --quiet --pidfile ${CRON_PID} \
56 --chuid ${USER} --background --make-pidfile \
57 --exec ${PYTPATH} \
58 -- ${CRON_DAEMON} ${CRON_DAEMON_OPTS} --config=${CRON_CONFIG}
59</%text>
60%endif
61}
62
63do_stop()
64{
65 start-stop-daemon --stop --quiet --pidfile ${'$'}{PID} --retry=TERM/3/KILL/2 --oknodo
66%if use_cron_daemon:
67 start-stop-daemon --stop --quiet --pidfile ${'$'}{CRON_PID} --retry=TERM/3/KILL/2 --oknodo
68%endif
69}
70
71<%text>
72case "${1}" in
73 start)
74 echo -n "Starting ${DESC}: "
75
76 do_start
77
78 echo "${NAME}."
79 ;;
80
81 stop)
82 echo -n "Stopping ${DESC}: "
83
84 do_stop
85
86 echo "${NAME}."
87 ;;
88
89 restart|force-reload)
90 echo -n "Restarting ${DESC}: "
91
92 do_stop
93
94 sleep 1
95
96 do_start
97
98 echo "${NAME}."
99 ;;
100
101 *)
102 N=/etc/init.d/${NAME}
103 echo "Usage: ${NAME} {start|stop|restart|force-reload}" >&2
104 exit 1
105 ;;
106esac
107
108exit 0
109</%text>
0110
=== added file 'anybox/recipe/openerp/templates/initd/openerp_server.initd.in'
--- anybox/recipe/openerp/templates/initd/openerp_server.initd.in 1970-01-01 00:00:00 +0000
+++ anybox/recipe/openerp/templates/initd/openerp_server.initd.in 2012-12-04 08:56:24 +0000
@@ -0,0 +1,79 @@
1#!/bin/sh
2
3### BEGIN INIT INFO
4# Provides: openerp-server
5# Required-Start: $remote_fs $syslog
6# Required-Stop: $remote_fs $syslog
7# Should-Start: $network
8# Should-Stop: $network
9# Default-Start: 2 3 4 5
10# Default-Stop: 0 1 6
11# Short-Description: Enterprise Resource Management software
12# Description: Open ERP is a complete ERP and CRM software.
13### END INIT INFO
14
15PATH=%(path)s
16PYTPATH=%(pypath)s
17DAEMON=%(daemon)s
18NAME=%(name)s
19PID=%(pid)s
20DESC=%(desc)s
21CONFIG=%(config)s
22LOGFILE=%(logfile)s
23USER=%(user)s
24
25test -x ${DAEMON} || exit 0
26
27set -e
28
29do_start()
30{
31 start-stop-daemon --start --quiet --pidfile ${PID} \
32 --chuid ${USER} --background --make-pidfile \
33 --exec ${PYTPATH} -- ${DAEMON} --config=${CONFIG} \
34 --logfile=${LOGFILE}
35}
36
37do_stop()
38{
39 start-stop-daemon --stop --quiet --pidfile ${PID} \
40 --retry=TERM/10/KILL/5 --oknodo
41}
42
43case "${1}" in
44 start)
45 echo -n "Starting ${DESC}: "
46
47 do_start
48
49 echo "${NAME}."
50 ;;
51
52 stop)
53 echo -n "Stopping ${DESC}: "
54
55 do_stop
56
57 echo "${NAME}."
58 ;;
59
60 restart|force-reload)
61 echo -n "Restarting ${DESC}: "
62
63 do_stop
64
65 sleep 1
66
67 do_start
68
69 echo "${NAME}."
70 ;;
71
72 *)
73 N=/etc/init.d/${NAME}
74 echo "Usage: ${NAME} {start|stop|restart|force-reload}" >&2
75 exit 1
76 ;;
77esac
78
79exit 0
080
=== modified file 'setup.py'
--- setup.py 2012-11-21 14:14:51 +0000
+++ setup.py 2012-12-04 08:56:24 +0000
@@ -37,6 +37,7 @@
37 'webclient = anybox.recipe.openerp.webclient:WebClientRecipe',37 'webclient = anybox.recipe.openerp.webclient:WebClientRecipe',
38 'gtkclient = anybox.recipe.openerp.gtkclient:GtkClientRecipe',38 'gtkclient = anybox.recipe.openerp.gtkclient:GtkClientRecipe',
39 ]},39 ]},
40 package_data = { 'anybox.recipe.openerp': ['templates/initd/*.in'], },
40 )41 )
4142
4243

Subscribers

People subscribed via source and target branches