Inklings: a tumblelog

Setting up ReviewBoard without mod_wsgi

Yesterday I set up Review Board at work to see if it might be a better way to conduct code reviews than RhodeCode, which has code review functionality, but it’s never worked particularly well for us.

Owing problems we’ve had in the past[^3], we rarely use mod_wsgi these days and prefer run our applications as standalone daemons managed by Supervisor that listen on the loopback interface, with Apache[^1] acting as a reverse proxy using mod_proxy.

First thing I did was create a virtual environment for the application to run in:

# mkdir -p /opt/reviewboard
# cd /opt/reviewboard
# virtualenv --no-site-packages env

The server in question is running Debian Squeeze, which makes it necessary to use the --no-site-packages flag with virtualenv.

Review Board requires easy_install, so pip is a no-go:

# env/bin/easy_install ReviewBoard

It’s advisable to have a memcached instance running, so that was installed from APT.

As we authenticate off of a central LDAP server, I installed the python-ldap from PyPI into the virtual environment with pip:

# env/bin/pip install python-ldap

As we’ll need a WSGI server to run it under, I chose to use waitress. As of waitress 0.8.4, waitress comes with a command line runner I contributed called waitress-serve[^2]. It makes starting instances of waitress on the command line easier:

# env/bin/pip install waitress

With that done, it’s time to generate the site with the rb-site command:

./env/bin/rb-site install site

After asking a few questions, that will create a directory called /opt/reviewboard/site to contain the site assets.

Out of the box, reviewboard doesn’t have a way to run standalone. To get around that, I had it generate a mod_wsgi site, given that was the closest option, and took a look at the .wsgi file generated. It looked like this:

import os
import sys

os.environ['DJANGO_SETTINGS_MODULE'] = "reviewboard.settings"
os.environ['PYTHON_EGG_CACHE'] = "/opt/reviewboard/site/tmp/egg_cache"
os.environ['HOME'] = "/opt/reviewboard/site/data"
os.environ['PYTHONPATH'] = '/opt/reviewboard/site/conf'

sys.path = ['/opt/reviewboard/site/conf'] + sys.path

import django.core.handlers.wsgi
application = django.core.handlers.wsgi.WSGIHandler()

As we’ll be running this as a standalone server to be managed by Supervisor, I used this as the basis of the Supervisor configuration block for the application:

[program:reviewboard]
command=/opt/reviewboard/env/bin/waitress-serve
  --expose-tracebacks
  --url-scheme=https
  --host=127.0.0.1
  --port=8005
  --call
  django.core.wsgi:get_wsgi_application
user=nobody
stdout_logfile=/opt/reviewboard/site/logs/%(program_name)s.out.log
stderr_logfile=/opt/reviewboard/site/logs/%(program_name)s.err.log
environment=
  DJANGO_SETTINGS_MODULE="reviewboard.settings",
  PYTHON_EGG_CACHE="/opt/reviewboard/site/tmp/egg_cache",
  HOME="/opt/reviewboard/site/data",
  PYTHONPATH="/opt/reviewboard/site/conf"

Normally, waitress-serve expects a WSGI application object to be specified, but as none is created by default in a Django application, I used its --call flag so that a callable could be specified to call to get an application object.

With that set up, it’s time to set up Apache:

<VirtualHost *:80>
    ServerName reviewboard.example.com

    RewriteEngine on
    RewriteCond %{HTTPS} !^on$ [NC]
    RewriteRule . https://%{HTTP_HOST}%{REQUEST_URI} [L]
</VirtualHost>

<VirtualHost *:443>
    ServerName reviewboard.example.com

    DocumentRoot /opt/reviewboard/site/htdocs
    ProxyPass /static !
    ProxyPass /media !
    ProxyPass /errordocs !

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8005/
    ProxyPassReverse / http://127.0.0.1:8005/
    SetEnvIf X-Url-Scheme https HTTPS=1
    RequestHeader Set X-Forwarded-Proto https

    SSLEngine On
    SSLCertificateFile /path/to/certificate.crt
    SSLCertificateKeyFile /path/to/certificate.key
</VirtualHost>

The ProxyPass directives just following DocumentRoot ensure that any static content is handled by Apache. The block that follows that passes any other traffic through to the server we’ve bound to the loopback interface.

With all that in place, tell Supervisor and Apache to reload their configuration:

# supervisorctl update
# service apache2 graceful

And bingo! It should be working!

Caveat regarding LDAP authentication

When I set up LDAP authentication, I noticed it wasn’t working and there were some bizarre errors in the log:

WARNING:root:LDAP error: {'info': '(unknown error code)', 'desc': "Can't contact LDAP server"}

After some hunting and experimenting, I discovered that the problem was that python-ldap was doing certificate checking. To get around that, I opened up the reviewboard.accounts.backends module, found the LDAPBackend class, and in the authenticate() function, inserted the following statement following the import ldap statement:

ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)

And it was able to authenticate off of LDAP.

[^3]: YMMV: mod_wsgi is still a solid piece of software, but it just didn’t suit our deployment scenarios. [^1]: We may switch to Nginx for this for lower overhead, but it’s not a priority. [^2]: It doesn’t currently support WSGI middleware though. I didn’t feel strongly about adding support for middleware at the time, but I may change my mind in the future. It wouldn’t be a difficult thing to add.


2013-05-27: Updated for waitress 0.8.4: the waitress-serve package is no longer required as it’s been merged into waitress proper.