16 min read

Releasing Odoo-based projects as Debian packages

Releasing Odoo-based projects as Debian packages

Here is a report about something I've been playing with lately, namely a way to install a full blown custom Odoo-based project entirely with Debian packages. In this report, we'll work on a Debian system, but keep in mind that this would apply readily to any derivative distribution, such as Ubuntu.

It's been a funny experiment, about something that has been on my mind at least since the introduction of packaging options in the OpenERP/Odoo buildout recipe.

At this point I don't know if it's going to become the way Anybox deploys its custom Odoo-based projects, but time will show.

In this blog post, I will

  1. try and summarize what are those needs that zc.buildout alone does not address,
  2. then proceed with a commented shell session of a pure Debian-style installation of a real-life project.
  3. The third part explains how it's been brought and present the whole architecture that backs this up.
  4. The final part will be about the limitations of this approach.

Buildout shortcomings

As we've always said, zc.buildout is a great tool to provide uniformity of installation along several axes:

  • uniformity accross projects: always the same bin/buildout command
  • uniformity accross installations of a given project
  • means to set apart those parameters that must depend on each installation (system wide or per install).

All of this while allowing each project to have more dependencies than the upstream Odoo, or to use newer features and bugfixes from those libraries. Given that these features and bugfixes may well be critical for a precise project while being minor to the ecosystem as a whole, we need flexibility. In turn, full flexibility without a reproducibility warranty would just bring hostile chaos upon us. Buildout gives us such a warranty, albeit with shortcomings.

Namely, installation can fail. This means that the reproducibility promise is actually to be understood as "if it worked, then it is what has been required".

Bootstrap issues

The bootstrap phase is the typical chicken-and-egg problem: one needs to have a local installation of zc.buildout with a compatible enough version to actually run bin/buildout. If the configuration file requires a different version, then the actual first execution will also install it and switch to it before proceeding [1].

There are several ways to bootstrap [2] and I would need several pages to write on the details of how each can break. The important thing, though, is that making one work reliably at a given point in time is far from being a guarantee that it won't stop working in an unpredictable manner. Usually, developers won't notice immediately, and that increases the probability of embarassment.

Considering that this process actually involves setuptools, on which zc.buildout also depends for its regular operation actually makes me think that a reliable procedure is quite impossible, at least without the a priori knowledge of versions of both and some assumptions on the environment.

When the bootstrap breaks, the errors are quite obscure, but there's nothing that couldn't be overcome easily by someone accustomed to them in manual mode. On the other hand, we want to let developers focus on their precise project, and give something that works to the one doing the installation. Also consider that the latter one might not even be a person, and part of a wider automated process. Automated processes are great, but sometimes don't let operators switch to manual mode easily.[1]This first execution of bin/buildout is not part of the bootstrap, but one can hardly call the bootstrap successful if the first execution fails.[2]

A very common practice is to include the bootstrap.py script directly in the project. This has the obvious advantage of making the project self-contained, with instructions as simple as: "just do python bootstrap.py && bin/buildout. But it also implies that this precise bootstrap must work in all contexts and without time limitations, if one really wish to ensure reproducibility of a precise build of the project.

Another way is to shortcut the bootstrap.py script and instead install directly zc.buildout in a virtualenv with pip. It can be tightened by installing directly the target version.

Downloads

Shipping an archived buildout such as one the recipe's extract-downloads-to option can help you build is great: the precise revision of Odoo and all the modules that the project uses are already extracted and packaged.

Still, without further efforts, the installation on a given system will perform these steps:

  • download of all needed Python libraries
  • compilation of those Python libraries that need it (extensions)

Of these, the most problematic is the download. First, it's not frequent, but versions can disappear. It's not frequent anymore, but the Python Package Index can go offline.

I personnally never had to install Odoo/OpenERP on a machine that could not reach the internet at all, but I've lost some valuable time and energy in the frustration of having to battle with proxy configurations. Your fellow operators might just not be ready to provide you with access codes.

In general, if a step that you're used to perform on your own in 5 minutes suddenly needs assistance from a third party (proxy admin) and overall 1-2 hours of tinkering, you're very likely to have a schedule problem.

A full Debian backed installation

Before explaining how it works, let's demonstrate.

I'm on a fairly barebones Debian 7 system (actually it's a fresh install from the OpenVZ template, of which I removed some stuff for fun). It has only 209 installed packages, and doesn't take much disk space:root@aohdeb:~# dpkg -l | grep '^ii' | wc -l 209 root@aohdeb:~# df -h / Filesystem      Size  Used Avail Use% Mounted on /dev/simfs       20G  448M   20G   3% /

I have gone through the instructions to hook the system to Anybox package repositories, installed anybox-keyring and additionally registered a special source:root@aohdeb:~# cat /etc/apt/sources.list.d/anybox.list deb http://apt.anybox.fr/openerp common main # anybox-odoo-host is in testing only for now deb http://apt.anybox.fr/openerp anybox-testing contrib  # special source for Python libs expected by buildout: deb http://pylib.apt.anybox.fr wheezy-pythonlib contrib

I have a local package of my project that I managed to put on this server. I'll explain how it's been built later, but let's say for now that it has all the needed dependencies, including anybox-odoo-host.root@aohdeb:~# ls *.deb project_name_1.16.9.9.2_all.deb

This is actually a complex project, with lots of dependencies. In particular, it makes use of Aeroo reports, which means a full headless LibreOffice stack.

For the demonstration, we'll use gdebi, a tool that's able to download the dependencies from the repository for a locally available package (see also below for discussion of downloads):root@aohdeb:~# aptitude install gdebi-core

Okay, let's dive in:root@aohdeb:~# time gdebi project_name_1.16.9.9.2_all.deb Reading package lists... Done Building dependency tree Reading state information... Done Building data structures... Done Building data structures... Done  Requires the installation of the following packages: acl  anybox-multipip anybox-odoo-host  anybox-python2.7-aeroolib-1.2.0  anybox-python2.7-anybox.recipe.openerp-1.8.6  anybox-python2.7-anybox.scripts.openerp-0.2  anybox-python2.7-anybox.testing.datetime-0.4.2  anybox-python2.7-anybox.testing.openerp-1.3.1  anybox-python2.7-argparse-1.2.2  anybox-python2.7-babel-1.3  anybox-python2.7-beautifulsoup-3.2.1  anybox-python2.7-bzr-2.6.0  anybox-python2.7-coverage-3.7.1  anybox-python2.7-docutils-0.9  anybox-python2.7-ecdsa-0.11  anybox-python2.7-fabric-1.10.0  anybox-python2.7-feedparser-5.1.3  anybox-python2.7-gdata-2.0.18  anybox-python2.7-genshi-0.6  anybox-python2.7-gp.vcsdevelop-2.2.3  anybox-python2.7-jinja2-2.7.3  anybox-python2.7-lxml-2.3.3  anybox-python2.7-mako-1.0.0  anybox-python2.7-markupsafe-0.23  anybox-python2.7-mock-1.0.1  anybox-python2.7-nose-1.3.0  anybox-python2.7-odfpy-0.9.6  anybox-python2.7-paramiko-1.15.1  anybox-python2.7-pillow-2.6.1  anybox-python2.7-pip-1.5.6  anybox-python2.7-psutil-2.1.3  anybox-python2.7-psycopg2-2.5.4  anybox-python2.7-pychart-1.39  anybox-python2.7-pycrypto-2.6.1  anybox-python2.7-pydot-1.0.28  anybox-python2.7-pyparsing-1.5.6  anybox-python2.7-python-dateutil-1.5  anybox-python2.7-python-ldap-2.4.18  anybox-python2.7-python-openid-2.2.5  anybox-python2.7-python-stdnum-1.0  anybox-python2.7-pytz-2014.9  anybox-python2.7-pywebdav-0.9.4.1  anybox-python2.7-pyyaml-3.11  anybox-python2.7-reportlab-3.1.8  anybox-python2.7-setuptools-7.0  anybox-python2.7-simplejson-3.6.5  anybox-python2.7-six-1.8.0  anybox-python2.7-unidecode-0.04.16  anybox-python2.7-unittest2-0.8.0  anybox-python2.7-vatnumber-1.2  anybox-python2.7-vobject-0.6.6  anybox-python2.7-werkzeug-0.8.3  anybox-python2.7-xlwt-0.7.5  anybox-python2.7-zc.buildout-2.2.5  anybox-python2.7-zc.recipe.egg-2.0.1  binutils  build-essential  bzr  ca-certificates  comerr-dev  cpp  cpp-4.7  curl  dbus  dictionaries-common  dpkg-dev  fakeroot  fontconfig  fontconfig-config  fonts-droid  fonts-liberation  fonts-lyx  fonts-opensymbol  fonts-stix  g++  g++-4.7  gcc  gcc-4.7  ghostscript  git  git-core  git-man  graphviz  gsfonts  hunspell-en-us  iso-codes  javascript-common  krb5-locales  krb5-multidev  lftp  libalgorithm-diff-perl  libalgorithm-diff-xs-perl  libalgorithm-merge-perl  libapr1  libaprutil1  libaudio2  libaudit0  libavahi-client3  libavahi-common-data  libavahi-common3  libc-dev-bin  libc6-dev  libcairo2  libcdt4  libcgraph5  libcmis-0.2-0  libcroco3  libcups2  libcupsimage2  libcurl3  libcurl3-gnutls  libdatrie1  libdbus-1-3  libdpkg-perl  liberror-perl  libevent-core-2.0-5  libevent-dev  libevent-extra-2.0-5  libevent-openssl-2.0-5  libevent-pthreads-2.0-5  libexpat1-dev  libexttextcat-data  libexttextcat0  libffi5  libfile-fcntllock-perl  libfontconfig1  libfontenc1  libfreetype6  libfreetype6-dev  libgd2-noxpm  libgdk-pixbuf2.0-0  libgdk-pixbuf2.0-common  libglib2.0-0  libglib2.0-data  libgomp1  libgraph4  libgraphite2-2.0.0  libgs9  libgs9-common  libgssapi-krb5-2  libgssrpc4  libgstreamer-plugins-base0.10-0  libgstreamer0.10-0  libgvc5  libgvpr1  libhunspell-1.3-0  libhyphen0  libice6  libicu48  libijs-0.35  libitm1  libjasper1  libjbig0  libjbig2dec0  libjpeg62  libjpeg8  libjpeg8-dev  libjs-jquery  libk5crypto3  libkadm5clnt-mit8  libkadm5srv-mit8  libkdb5-6  libkrb5-3  libkrb5-dev  libkrb5support0  liblcms1  liblcms1-dev  liblcms2-2  libldap2-dev  libltdl7  libmhash2  libmng1  libmythes-1.2-0  libneon27-gnutls  libnspr4  libnss3  libopenjpeg2  liborc-0.4-0  libpango1.0-0  libpaper-utils  libpaper1  libpathplan4  libpixman-1-0  libpoppler19  libpq-dev  libpq5  libpython2.7  libpython3.2  libqt4-network  libqt4-xml  libqtcore4  libqtdbus4  libqtgui4  libqtwebkit4  libquadmath0  libraptor2-0  librasqal3  librdf0  libreoffice-common  libreoffice-core  libreoffice-style-galaxy  librsvg2-2  librsvg2-common  librtmp0  libsasl2-dev  libsasl2-modules  libsm6  libssh2-1  libssl-dev  libssl-doc  libstdc++6-4.7-dev  libsvn1  libsystemd-login0  libthai-data  libthai0  libtiff4  libtimedate-perl  libx11-6  libx11-data  libxaw7  libxcb-render0  libxcb-shm0  libxcb1  libxdot4  libxext6  libxfont1  libxft2  libxinerama1  libxkbfile1  libxml2-dev  libxmu6  libxmuu1  libxpm4  libxrandr2  libxrender1  libxslt1-dev  libxslt1.1  libxt6  libyajl2  libyaml-0-2  libyaml-dev  linux-libc-dev  lsb-release  make  manpages-dev  mercurial  mercurial-common  nginx-common  nginx-full  openerp-server-system-dev-deps  openerp-server-system-run-deps  openssh-blacklist  openssh-blacklist-extra  openssh-client  poppler-data  poppler-utils  postgresql  postgresql-9.1  postgresql-client  postgresql-client-9.1  postgresql-client-common  postgresql-common  python-anybox.hosting  python-bzrlib  python-configobj  python-crypto  python-dev  python-gpgme  python-httplib2  python-keyring  python-launchpadlib  python-lazr.restfulclient  python-lazr.uri  python-medusa  python-meld3  python-oauth  python-paramiko  python-pip  python-pkg-resources  python-setuptools  python-simplejson  python-uno  python-virtualenv  python-wadllib  python-zope.interface  python2.6  python2.6-minimal  python2.7-dev  python3  python3-dev  python3-minimal  python3-pip  python3-pkg-resources  python3-setuptools  python3.2  python3.2-dev  python3.2-minimal  rsync  shared-mime-info  subversion  sudo  supervisor  ttf-dejavu  ttf-dejavu-core  ttf-dejavu-extra  ttf-liberation  uno-libs3  unzip  ure  wkhtmltopdf  wwwconfig-common  x11-common  x11-xkb-utils  xauth  xfonts-base  xfonts-encodings  xfonts-mathml  xfonts-utils  xkb-data  xserver-common  xvfb  zip  zlib1g-dev Debian repackaging of project_name-1.16.9.9.2.tar.bz2 Do you want to install the software package? [y/N]:y Get:1 http://apt.anybox.fr/openerp/ anybox-testing/contrib anybox-multipip all 0.1-1 [2756 B] (...) long list of downloads (...) Get:324 http://ftp.debian.org/debian/ wheezy/main xfonts-mathml all 6 [42.2 kB] Fetched 283 MB in 6s (554 kB/s) (...) long list of packages unpackacking/configuration  Setting up anybox-odoo-host (1.2-3) ... update-alternatives: error: alternative /usr/bin/vim.basic for editor not registered; not setting Adding group `openerp' (GID 108) ... Done. Adding user `backup' to group `openerp' ... Adding user backup to group openerp Done. Adding system user `openerp' (UID 102) ... Adding new user `openerp' (UID 102) with group `openerp' ... Creating home directory `/home/openerp' ... Initializing default system-wide buildout configuration /etc/buildout-anybox.cfg   Anybox: locale 'fr_FR.utf8' not installed !    If this is a fresh install, consider recreating the PostgreSQL cluster.   Otherwise, create a secondary one for OpenERP.   A cluster installed without the correct locale WON'T WORK for OpenERP   production at all !!    Suggested commands (as root):    0) COMMON PART      dpkg-reconfigure locales    1) FOR RE-CREATION (DROPING ALL EXISTING DATA)      pg_dropcluster 9.1 main # DROP !!      pg_createcluster --locale fr_FR.utf8 9.1 main    2) FOR SECONDARY CLUSTER CREATION.      If there is data in the main cluster that you want to keep, the commands below      create a cluster named 'openerp', managed by the system user 'openerp':       pg_createcluster -u openerp --locale fr_FR.utf8 9.1 openerp    Then don't forget to feed the correct port to OpenERP configurations.  (...) Setting up anybox-odoo-host (1.2-1) ... Setting up anybox-python2.7-aeroolib-1.2.0 (1.2.0-1) ... Setting up anybox-python2.7-anybox.recipe.openerp-1.8.6 (1.8.6-1) ...  Selecting previously unselected package project_name. (Reading database ... 45275 files and directories currently installed.) Unpacking project_name (from project_name_1.16.9.9.2_all.deb) ... Setting up project_name (1.16.9.9.2) ... To install/upgrade project_name for client/projects,   please run anybox-project_name-deploy for the wished client/project combinations   as root. For more explanations, do: anybox-project_name-deploy --help  real   10m12.670s user   0m48.736s sys    0m13.185s

Okay, so that took 10 minutes, of which 8.5 spent downloading (283 MB of at 553 kB/s). As you can see, I'm not much cheating: I forgot to prepare the locale, and it shows. In real life, it'd a better idea to install and prepare the PostgreSQL cluster, maybe with a precise version from apt.postgresql.org, but anyway, I followed the instructions (no need to repeat them here) and remade the cluster with the french locale.

From now, we'll operate fully offline, so let's kill connectivity:root@aohdeb:~# ifdown venet0 SIOCDELRT: No such process root@aohdeb:~# ping pypi.python.org ping: unknown host pypi.python.org

and install our project:root@aohdeb:/# anybox-odoo-add-instance-user instance Adding system user `instance' (UID 105) ... Adding new user `instance' (UID 105) with group `openerp' ... Creating home directory `/srv/openerp/instance' ... sending incremental file list ./ .buildout/ .buildout/default.cfg  sent 274 bytes  received 38 bytes  624.00 bytes/sec total size is 147  speedup is 0.47 CREATE ROLE instance NOSUPERUSER CREATEDB NOCREATEROLE INHERIT LOGIN;   root@aohdeb:~# time anybox-project_name-deploy --port 8069 --cron-port 8070 instance Running command: ['anybox-odoo-deploy', '--offline', '--bootstrap-setuptools-egg-path', '/opt/anybox/lib/python/setuptools-7.0-py2.7.egg', '--bootstrap-buildout-version', '2.2.5', '--port', '8069', '--cron-port', '8070', '--buildout-install-only-specified-part', 'instance', '/usr/share/anybox/project/project_name-1.16.9.9.2.tar.bz2'] This looks like a brand new deployment has to be made for '/srv/openerp/instance' !  Unpacking /usr/share/anybox/project/project_name-1.16.9.9.2.tar.bz2 Creating symlink '/srv/openerp/instance/current_buildout' to project_name-1.16.9.9.2 Creating subdirectory log/ Creating subdirectory dumps/  BUILDOUT OPERATIONS INFO:anybox-odoo-deploy:offline bootstrap: all conditions fulfilled, now attempting Creating directory '/srv/openerp/instance/project_name-1.16.9.9.2/bin'. Creating directory '/srv/openerp/instance/project_name-1.16.9.9.2/develop-eggs'. Generated script '/srv/openerp/instance/project_name-1.16.9.9.2/bin/buildout'. INFO:anybox-odoo-deploy:offline bootstrap: success Calling buildout on /srv/openerp/instance/aohdeb.cfg anybox.recipe.openerp.base: Created etc/ directory Installing openerp.  (..) there are some attemps to check what external find-links have but they are harmless (...): Download error on http://download.gna.org/pychart/: [Errno -2] Name or service not known -- Some packages may not be found! (...) anybox.recipe.openerp.base: 'openerp-command' is a direct soft requirement, retrying without it  Generated interpreter '/srv/openerp/instance/current_buildout/bin/python_openerp'. Generated script '/srv/openerp/instance/current_buildout/bin/upgrade_openerp'. Generated script '/srv/openerp/instance/current_buildout/bin/start_openerp'. Generated script '/srv/openerp/instance/current_buildout/bin/nosetests'. Generated script '/srv/openerp/instance/current_buildout/bin/test_openerp'. anybox.recipe.openerp.base: Creating config file: etc/openerp.cfg (..)  DATABASE OPERATIONS. DB name: 'instance' Calling create/upgrade script 'bin/upgrade_openerp' Starting upgrade, logging details to /srv/openerp/instance/log/upgrade_create.log at level INFO, and major steps to console at level INFO  2014-11-23 19:42:35,690 INFO  Database 'instance' base initialization done. Proceeding further 2014-11-23 19:42:35,691 INFO  Read package version: 1.16.9 from /srv/openerp/instance/current_buildout/VERSION.txt 2014-11-23 19:43:21,993 INFO  This is a fresh database, load init data (...) skip this project's specific initialization (...)  2014-11-23 19:44:31,190 INFO  setting version 1.16.9 in database 2014-11-23 19:44:31,194 INFO  Initialization successful. Total time: 129 seconds.  real  2m17.912s user  0m37.380s sys   0m2.853s

Is it really better ?

We've seen quite a bit of downloading in the above quoted session, so it doesn't look like so much of an improvement at first sight. The key feature however is that all downloads happen at once, following a standard that is very much widespread and easy to tell your counterparts about.

Here's a non exhaustive list of what can be done from there:

  • tell in advance the admin of the target system to go through the APT hooking procedure while provisioning the system. Chances are that the machine has a way to reach on the mainline Debian repositories anyway. In any case, that's presenting a prerequisite that has clear bounds and can be decided by the people that have the network mastery.
  • in case where hooking to an Anybox repo is against local policy, simply transmit all the involved packages directly.
  • use a tool such as apt-offline
  • mount a local CD copy of the involved Anybox repos
  • someone really committed could go all the way to create a bootable USB stick.

Build process and infrastructure

Core process

By default, anybox-odoo-host makes all buildouts use a common shared eggs directory: /opt/anybox/lib/python [3], so the idea was to prefill this directory. Because we still wish to allow installation of different instances on a single system, this meant building a Debian package for each egg in each version, since overlapping binary Debian packages are strictly prohibited [4].

Bootstrap

The key command, anybox-odoo-deploy (part of anybox-odoo-host), that's been used in the above demonstration is able to perform the bootstrap fully offline if three conditions are met [5]:

  • the exact wished versions of setuptools and zc.buildout are passed in command line arguments
  • these versions are already available within the eggs cache.
  • there's a working system-wide setuptools for inspection (provided by package dependencies)

So that leaves us with providing the eggs packages and passing the right options to anybox-odoo-deploy.

Eggs

For the eggs, I wrote a script to be executed in the same python environment as a working buildout. It looks at buildout installed eggs and makes a package for each of them, directly in binary form.

To allow for coexistence of several version, the version must actually be set in the package name

This script also detects whether each egg is pure Python or architecture dependent and builds packages accordingly.

A second script takes the output of the first and the archived tarball to cook the final package. It includes the anybox-project_name-deploy that we've seen in action and is nothing but a rewrapping of anybox-odoo-deploy with the correct options.

Early results where encouraging, I could finally get a project to install offline by copying all the Debian packages over.[3]This gets specified in ~instance/.buildout/defaults.cfg.[4]dpkg will throw an error if a package has a file that is listed in another package that's already installed.[5]Actually anybox-odoo-deploy also has two layers of fallback after this before attempting a bare python bootstrap.py.

Build infrastructure

Two triggered post-release builders

In Anybox's buildbot, we already have builders to produce the tarballs for all our significant projects. This is actually published as part of our buildbot configurator.

Of course I wanted to add Debian package production steps to them, but that would not have been enough, since the eggs packages can be architecture dependent, and there are at least two processor architectures to support: i386 and amd64. It's not a bit stretch to imagine more, completely foreign ones. Also, it'll be necessary to rebuild them for other Debian versions and derivatives… So we need to run on several build hosts.

Luckily, buildbot has this very flexible notion of triggerable builds, so I could make the tarball build trigger two separate builders, one for each architecture, and have them run on appropriate hosts.

Details of the build

The steps that these builds consist of are:

  • grab the tarball
  • bootstrap and buildout (actually I've just reused the standard steps collection provided by the configurator and also used by testing builds)
  • run the eggs extraction script. I didn't mention it above, but it leverages APT to extract only those packages that are not in pylib.apt.anybox.fr yet.
  • upload the produced eggs packages
  • include master-side the new packages in pylib.apt.anybox.fr, using reprepro.
  • (only for one build) produce the final package and upload it

The whole picture

article_deb.png

That makes a lot of infrastructure for an experiment, but that was fun, for some value of "fun".

Quircks and limitations

There are some remaining problems with that approach. Some just need to throw a bit more work at them, but some are deeper:

A lot relies on the script that does the eggs extraction, and there's no a priori closed list of what it must do.

For instance, I had to introduce a special rule for Gunicorn, in order to exclude a Python 3 source file that would break the compilation of bytecode that happens at installation.

  • The architecture dependent eggs have to be build for each Debian version or derivative. Typically, the stable distributions guarantee ABI stability of the system libraries during a the distribution version lifetime, but it'll differ for each

There's currently no way to rebuild a Debian package for an egg. This will be necessary if we become serious about using this system.

There's currently no way to require additional system libraries, except by hardcoding in the extraction script. Maybe dh_python could be of use here.

local configuration templates may require additional libraries that the buildbot could not have seen. Typically, if the local buildout template adds the gunicorn option, and if that option is not present in the project's buildout, the deb extractor cannot be aware of it, and we have a broken dependency.

Buildout is a multi-component system. A single buildout configuration can very well consist of an Odoo-based application, alongside a Django one, and a part providing Sphinx to build the documentation — or even something totally foreign to Python, such as a daemon written in Rust.

In full generality, it's impossible to guess what secondary parts aim to provide and how. Neither can one foretell whether they are dispendable or essential for the operation. Therefore, the anybox-project_name-deploy wrapper script above restricts the installation to the Odoo/OpenERP part. This can be overridden.

But, for more complicated setups, prebuilding and shipping a whole system image à la Docker might well be the only viable solution. Nevertheless, these Debian packages might serve well as an intermediary towards image production for various systems.