Archive for the 'Plone' Category


Using HAProxy with Zope via Buildout

After my post on reducing GIL contention by using fewer Zope threads, Lee Joramo asked for more information on setting up HAProxy, so let me share my configuration. Much of the credit for this goes to Hanno Schlichting and Alex Clark, who offered me much good advice and a sample configuration, respectively.

First, a few words about what HAProxy offers. For the past couple years I’ve been using Pound to load balance between multiple backend Zope instances. But recently I’ve been hearing recommendations from people I trust (such as Jarn and Elizabeth Leddy) to try HAProxy instead.

HAProxy offers some nice features:
- Backend health checks
- Various load-balance algorithms for how requests get distributed to backends
- Can do sticky sessions so that an authenticated user always hits the same backend
- Warmup time (don’t send as many requests to a Zope instance while it’s starting up)
- Provides a status page giving info on backend status and uptime, # of queued requests, # of active sessions, # of errors, etc.

Some of these are possible with pound too, but the status screen was really the “killer app” for me. This is fun to watch but also very useful for doing rolling restarts when new code needs to be deployed without an interruption in service.

HAProxy status page

Configuration

In my buildout.cfg I added:

[buildout]
...
parts =
    ...
    haproxy-build
    haproxy-conf

[haproxy-build]
recipe = plone.recipe.haproxy
url = http://dist.plone.org/thirdparty/haproxy-1.3.22.zip

[haproxy-conf]
recipe = collective.recipe.template
input = ${buildout:directory}/haproxy.conf.in
output = ${buildout:directory}/etc/haproxy.conf
maxconn = 24000
ulimit-n = 65536
user = zope
group = staff
bind = 127.0.0.1:8080

Here, we add a part called “haproxy-build” which uses the plone.recipe.haproxy recipe to build haproxy from source and add a bin/haproxy script for running it, and a part called “haproxy-conf” which builds the HAProxy configuration file by filling in variables in a template file called haproxy.conf.in.

Be sure to set the user and group variables to the user and group you want HAProxy to run as, and update the bind variable to set the port to which HAProxy should bind.

I run most of my Plone stack using supervisord, so I also updated my supervisord configuration in buildout to run HAProxy:

[supervisor]
recipe = collective.recipe.supervisor
...
programs =
    ...
    10 haproxy ${buildout:directory}/bin/haproxy [ -f ${buildout:directory}/etc/haproxy.conf -db ]

In a real life deployment, you’ll probably also want a caching reverse proxy like squid or varnish sitting in front of HAProxy.

What about the contents of haproxy.conf.in? Here’s mine:

global
  log 127.0.0.1 local6
  maxconn  ${haproxy-conf:maxconn}
  user     ${haproxy-conf:user}
  group    ${haproxy-conf:group}
  daemon
  nbproc 1

defaults
  mode http
  option httpclose
  # Remove requests from the queue if people press stop button
  option abortonclose
  # Try to connect this many times on failure
  retries 3
  # If a client is bound to a particular backend but it goes down,
  # send them to a different one
  option redispatch
  monitor-uri /haproxy-ping

  timeout connect 7s
  timeout queue   300s
  timeout client  300s
  timeout server  300s

  # Enable status page at this URL, on the port HAProxy is bound to
  stats enable
  stats uri /haproxy-status
  stats refresh 5s
  stats realm Haproxy statistics

frontend zopecluster
  bind ${haproxy-conf:bind}
  default_backend zope

# Load balancing over the zope instances
backend zope
  # Use Zope's __ac cookie as a basis for session stickiness if present.
  appsession __ac len 32 timeout 1d
  # Otherwise add a cookie called "serverid" for maintaining session stickiness.
  # This cookie lasts until the client's browser closes, and is invisible to Zope.
  cookie serverid insert nocache indirect
  # If no session found, use the roundrobin load-balancing algorithm to pick a backend.
  balance roundrobin
  # Use / (the default) for periodic backend health checks
  option httpchk

  # Server options:
  # "cookie" sets the value of the serverid cookie to be used for the server
  # "maxconn" is how many connections can be sent to the server at once
  # "check" enables health checks
  # "rise 1" means consider Zope up after 1 successful health check
  server  plone0101 127.0.0.1:${zeoclient1:http-address} cookie p0101 check maxconn 2 rise 1
  server  plone0102 127.0.0.1:${zeoclient2:http-address} cookie p0102 check maxconn 2 rise 1

This assumes that I have Zope instances built by parts called “zeoclient1″ and “zeoclient2″ in my buildout; you’ll probably need to update those names.

You may want to adjust the “option httpchk” line to use a different URL for checking whether the Zope instances are up — you want to point at something that can be rendered as quickly as possible (in my case it’s the Zope root information screen, so I’m not too worried).

The maxconn setting for each backend should be at least the number of threads that that Zope instance is running. Laurence Rowe pointed out to me that it should probably not be set to 1, since Zope also serves some things (blobs and ) via file stream iterators, which happens apart from the main ZPublisher threads. (So setting maxconn to 1 would mean serving a large blob could block other requests to that backend, for instance.)

See the HAProxy configuration documentation for more details on the settings that can be used in this file.



on Zope, multiple cores, and the GIL

I recently installed HAProxy as a load-balancer for a site that had previously been running using a single Zope instance using 4 threads. I switched to 2 instances using 2 threads each, load-balanced by HAProxy. I wasn’t anticipating that this change would have a noticeable effect on the site’s performance, so was happily surprised when the client mentioned that users of the site were commenting on the improved speed.

But why did the site get faster?

Looking at a munin graph of server activity, I observed a noticeable drop in the number of rescheduling interrupts — a change that coincided with my change in server configuration:

graph showing decreased contention when I switched to more Zope instances with fewer threads

I suspect that the “before” portion of this graph illustrates a problem that occurs when running multi-threaded Python programs on multi-core machines, wherein threads running in different cores fight for control of the Global Interpreter Lock (a problem Dave Beazley has called to the community’s attention in a recent presentation) — and that this explains the improvement in performance once I switched to multiple processes with fewer threads. By switching to multiple processes, we let concurrent processing get managed by the operating system, which is much better at it.

Moral of the story: If you’re running Zope on a multi-core machine, having more than 2 threads per Zope instance is probably a bad move performance-wise, compared to the option of running more (load-balanced) instances with fewer threads.

(Using a single thread per instance might be even better, although of course you need to make sure you have enough instances to still handle your load, and you need to make sure single-threaded instances don’t make calls to external services which then call back to that instance and block. I haven’t experimented with using single-threaded instances yet myself.)



Come improve Dexterity at the Tahoe Snow Sprint

This year the West Coast is hosting our own version of the infamous Snow Sprint. I’m really looking forward to spending a week coding, hanging out with Plonistas, and playing in the snow at the upcoming Tahoe Snow Sprint, organized by David Brenneman (dbfrombrc) and coming to California’s Sierra Nevada this March 15-19.

The goal of the sprint is to improve the Dexterity content type framework (a modern alternative to Archetypes created by Martin Aspeli and others). As part of the Dexterity team, I want to offer the following list of potential projects to help get your creative juices flowing.

At the sprint, you could…


Implement one of Dexterity’s missing features, such as:

Fix some of the other outstanding issues in the Dexterity issue tracker.

Create a ZopeSkel template for Dexterity-based projects.

Improve the through-the-web content type editor.

  • improve usability and/or sexiness

  • add UI for exporting types for work on the filesystem
  • add support for defining vocabularies
  • add support for selecting/configuring custom widgets

Create an editor that allows through-the-web editing of new behaviors (which can then be applied to existing types in a schema-extender-like fashion.)

Add a view editor to accompany the through-the-web schema editor. Deco is coming and will be great, but in the meantime it would be nice to at least have something that generates a basic view template based on your schema and then lets you tweak it and export it to the filesystem.

Build a better workflow editor to accompany the above.

Write a guide to migrating Archetypes-based content types to Dexterity. Or build a tool to do it automatically.

Create replacements for the ATContentTypes types using Dexterity types.

Determine how to handle existing content items sanely when editing schemas.

Devise a PloneFormGen successor that stores its schema in a fashion similar to Dexterity, and makes it easy to convert a form + results into a full-blown content type. Bonus points if the form editing is done using Deco. :)


There are so many interesting possibilities I’m having trouble deciding what to focus on myself. Space is limited, so if any of this strikes your fancy, head on over to Coactivate and sign right up to join us at the sprint!



mr.igor

Today I released mr.igor, a utility for helping you write Python faster by filling in missing imports based on where you’ve imported the names from before.

Here’s a one-minute screencast showing how it works.



Recombining ZODB storages

I recently faced the task of joining back together a Plone site composed of 4 ZODB filestorages that had been (mostly through cavalier naïveté on my part) split asunder some time ago.

Normally I would probably just do a ZEXP export of each of the folders that lived in its own mountpoint, then remove the mountpoints and reimport the ZEXP files into the main database. However, that wasn’t going to work in this case because the database included some cross-database references.

Some background: Normally in Zope, mountpoints are the only place where one filestorage references another one, but the ZODB has some support for *any* object to link to any other object in any other database, and this can happen within Zope if you copy an object from one filestorage to another. This is generally bad, since the ZODB’s support for cross-database references is partial — when you pack one filestorage, the garbage collection routine doesn’t know about the cross-database references (unless you use zc.zodbdgc), so an object might get removed even if some other filestorage still refers to it, and you’ll get POSKeyErrors. Also, in ZODB 3.7.x, the code that handles packing doesn’t know about cross-database references, so you’ll get KeyError: ‘m’ or KeyError: ‘n’ while packing.

Well, this is what had happened to my multi-database, and I wanted to keep those cross-database references intact while I merged the site back into one monolithic filestorage. So I ended up adapting the ZEXP export code to:

  1. traverse cross-database references (the standard ZEXP export ignores them and will not include objects in different filestorages from the starting object),
  2. traverse ZODB mountpoints (removing them in the process),
  3. and rewrite all the oids to avoid collisions in the new merged database.

Here is the script I ended up with. If you need to use it, you should:

  1. Edit the final line to pass the object you want to start traversing from, and the filename you want to write the ZEXP dump to.
  2. Run the script using bin/instance run multiexport.py
"""Support for export of multidatabases."""

##############################################################################
#
# Based on the ZODB import/export code.
# Copyright (c) 2009 David Glick.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
#
##############################################################################

import logging
import cPickle, cStringIO
from ZODB.utils import p64, u64
from ZODB.ExportImport import export_end_marker
from ZODB.DemoStorage import DemoStorage

logger = logging.getLogger('multiexport')

def export_zexp(self, fname):
    context = self
    f = open(fname, 'wb')
    f.write('ZEXP')
    for oid, p in flatten_multidatabase(context):
        f.writelines((oid, p64(len(p)), p))
    f.write(export_end_marker)
    f.close()

def flatten_multidatabase(context):
    """Walk a multidatabase and yield rewritten pickles with oids for a single database"""
    base_oid = context._p_oid
    base_conn = context._p_jar
    dbs = base_conn.connections

    dummy_storage = DemoStorage()

    oids = [(base_conn._db.database_name, base_oid)]
    done_oids = {}
    # table to keep track of mapping old oids to new oids
    ooid_to_oid = {oids[0]: dummy_storage.new_oid()}
    while oids:
        # loop while references remain to objects we haven't exported yet
        (dbname, ooid) = oids.pop(0)
        if (dbname, ooid) in done_oids:
            continue
        done_oids[(dbname, ooid)] = True

        db = dbs[dbname]
        try:
            # get pickle
            p, serial = db._storage.load(ooid, db._version)
        except:
            logger.debug("broken reference for db %s, oid %s", (dbname, repr(ooid)),
                         exc_info=True)
        else:
            def persistent_load(ref):
                """ Remap a persistent id to a new ID and create a ghost for it.

                This is called by the unpickler for each reference found.
                """

                # resolve the reference to a database name and oid
                if isinstance(ref, tuple):
                    rdbname, roid = (dbname, ref[0])
                elif isinstance(ref, str):
                    rdbname, roid = (dbname, ref)
                else:
                    try:
                        ref_type, args = ref
                    except ValueError:
                        # weakref
                        return
                    else:
                        if ref_type in ('m', 'n'):
                            rdbname, roid = (args[0], args[1])
                        else:
                            return

                # traverse Products.ZODBMountpoint mountpoints to the mounted location
                rdb = dbs[rdbname]
                p, serial = rdb._storage.load(roid, rdb._version)
                klass = p.split()[0]
                if 'MountedObject' in klass:
                    mountpoint = rdb.get(roid)
                    # get the object with the root as a parent, then unwrap,
                    # since there's no API to get the unwrapped object
                    mounted = mountpoint._getOrOpenObject(app).aq_base
                    rdbname = mounted._p_jar._db.database_name
                    roid = mounted._p_oid

                if roid:
                    print '%s:%s -> %s:%s' % (dbname, u64(ooid), rdbname, u64(roid))
                    oids.append((rdbname, roid))

                try:
                    oid = ooid_to_oid[(rdbname, roid)]
                except KeyError:
                    # generate a new oid and associate it with this old db/oid
                    ooid_to_oid[(rdbname, roid)] = oid = dummy_storage.new_oid()
                return Ghost(oid)

            # do the repickling dance to rewrite references

            pfile = cStringIO.StringIO(p)
            unpickler = cPickle.Unpickler(pfile)
            unpickler.persistent_load = persistent_load

            newp = cStringIO.StringIO()
            pickler = cPickle.Pickler(newp, 1)
            pickler.persistent_id = persistent_id

            pickler.dump(unpickler.load())
            pickler.dump(unpickler.load())
            p = newp.getvalue()

            yield ooid_to_oid[(dbname, ooid)], p

class Ghost(object):
    __slots__ = ("oid",)
    def __init__(self, oid):
        self.oid = oid

def persistent_id(obj):
    if isinstance(obj, Ghost):
        return obj.oid

export_zexp(app.mysite, '/tmp/mysite.zexp')

Download multiexport.py

I’ve used this script with apparent success, but it has not been extensively tested and your mileage may of course vary.



Seeing a real-time breakdown of web traffic by vhost

Occasionally our servers are hit by traffic spikes. Since we typically host a number of websites per server, we need a way to quickly determine which site is receiving the bulk of incoming requests. (Then we can improve caching on that site, perhaps.) In order to see a real-time indication of what vhosts are being requested, we use the following awk script:

histo.awk

# creates a histogram of values in the first column of piped-in data
function max(arr, big) {
    big = 0;
    for (i in cat) {
        if (cat[i] > big) { big=cat[i]; }
    }
    return big
}

NF > 0 {
    cat[$1]++;
    if (!start) { start = $6 }
    end = $6
}
END {
    printf "from %s to %sn", start, end
    maxm = max(cat);
    for (i in cat) {
        scaled = 60 * cat[i] / maxm;
        printf "%-25.25s  [%8d]:", i, cat[i]
        for (i=0; i<scaled; i++) {
            printf "#";
        }
        printf "n";
    }
}

Which can be used like this:

watch 'tail -n 100 /var/log/apache2/access_log | awk -f histo.awk | sort -nrk3'

which will give a histogram of the occurence of vhosts in the last 100 lines of the apache log, updating every 2 seconds, sorted with the most frequent vhosts at the top. (Note that this assumes you are using an apache log format which includes the vhost as the first column.) It looks something like this:

Every 2.0s: tail -n 100 /var/log/apache2/access_log | awk -f histo.awk | sort -nrk3       Thu Oct  1 09:51:41 2009

www.dogwoodinitiative.org  [      49]:############################################################
www.wildliferecreation.or  [      24]:##############################
www.earthministry.org      [      14]:##################
blogs.onenw.org            [       3]:####
www.tilth.org              [       2]:###
www.oeconline.org          [       2]:###
www.audubonportland.org    [       1]:##
oraction.org               [       1]:##
oeconline.org              [       1]:##
dogwoodinitiative.org      [       1]:##
bandon.onenw.org           [       1]:##
209.40.194.148             [       1]:##
from [01/Oct/2009:09:51:21 to [01/Oct/2009:09:48:40

(Another useful variant of this is to produce a histogram of requests by IP address, which can help determine what to block in a DOS attack.)



Extending kupu’s initialization with a Javascript wrapper decorator

Today I found myself struggling to do something in Javascript that I’m used to doing with ease in Python — replace an existing method (defined by code I don’t want to touch) with a wrapper that calls the original method and then also performs some additional actions. (Yeah, it’s a monkey patch. But sometimes it’s a cleaner and more maintainable way to extend something than the alternatives.)

In particular, I was trying to adjust the default kupu configuration without overriding kupuploneinit.js to add commands directly to the initPloneKupu method. Here’s the snippet that got me there:

var augmentKupuInit = function(orig_fn) {
  return function(){
    var editor = orig_fn.apply(this, arguments);
    // do what you need to on the editor object here.
    // For example, I was trying to prevent kupu from
    // filtering the 'fb:fan' tag of Facebook's "Fan Box"
    // widget, like so:
    editor.xhtmlvalid.tagAttributes['fb:fan'] = ['*'];
    return editor;
  };
};
initPloneKupu = augmentKupuInit(initPloneKupu);

This defines a decorator function called augmentKupuInit that can be used to wrap another function. Then it uses it to wrap the original initPloneKupu method, calling the newly generated function initPloneKupu. As long as this snippet is registered in such a way that it loads after kupuploneinit.js and before the initPloneKupu method is called, it works like a charm!

(Many thanks to http://stackoverflow.com/questions/326596/how-do-i-wrap-a-function-in-javascript, which finally pointed me in the right direction.)



Visualizing the ZODB with graphviz

While digging around in the ZEXP export code, I realized that it wouldn’t be too hard to modify it to dump a representation of a ZODB in graphviz .dot format. Here’s a Zope external method I devised to do that:

# Generic ZODB walker and graphviz exporter

####################################################################
#
# Copyright (c) 2003 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
####################################################################

import logging
import cPickle, cStringIO
from ZODB.utils import u64

logger = logging.getLogger('ZODB.ExportImport')

def get_reference_dumper(refs):
    # This is a callback which will be called whenever a reference is found.
    def dump_reference(oid, roid):
        refs.append('%s -> %sn' % (u64(oid), u64(roid)))
    return dump_reference

def export_graphviz(self):
    """
    Walks a ZODB database and dumps the object graph in graphviz .dot format.
    """
    context = self
    f = open('plone.dot', 'w')
    f.write('digraph plone {n')
    refs = []
    reference_dumper = get_reference_dumper(refs)
    for oid, p in walk_database(context, reference_callback=reference_dumper):
        # Walk to all the objects in the database and examine their references.
        # Whenever a reference is found, it will be recorded via the
        # reference_dumper.  Whenever a new object is found, it will be yieled
        # to this loop.

        # Read the module and class from the pickle bytestream without actually
        # loading the object.
        module, klass = p.split('n')[:2]
        module = module[2:]

        f.write('%s [label="%s.%s"]n' % (u64(oid), module, klass))
    for ref in refs:
        f.write(ref)
    f.write('}n')
    f.close()

def walk_database(context, reference_callback=None):
    # Get the object ID and database connection of the starting object.
    base_oid = context._p_oid
    conn = context._p_jar

    # oids is used to keep track of found oids that need to be visited.
    # done_oids is used to keep track of which oids have already been yielded.
    oids = [base_oid]
    done_oids = {}
    while oids:
        # loop while references remain to objects we haven't exported yet
        oid = oids.pop(0)
        if oid in done_oids:
            continue
        done_oids[oid] = True

        try:
            # fetch the pickle
            p, serial = conn._storage.load(oid, conn._version)
        except:
            logger.debug("broken reference for oid %s", repr(oid),
                         exc_info=True)
        else:
            # If the Unpickler's persistent_load attribute is set to a list,
            # then that list will be populated with the references found in
            # the pickle when noload is called, without actually loading the
            # object.
            refs = []
            u = cPickle.Unpickler(cStringIO.StringIO(p))
            u.persistent_load = refs
            # noload must be called the same # of times it was called when
            # pickling
            u.noload()
            u.noload()

            # loop through the references found on this object
            for ref in refs:

                # look for the various reference types supported by the ZODB
                # (see the docs in ZODB/serialize.py for details)
                if isinstance(ref, tuple):
                    roid = ref[0]
                elif isinstance(ref, str):
                    roid = ref
                else:
                    try:
                        ref_type, args = ref
                    except ValueError:
                        # weakref - not supported
                        continue
                    else:
                        if ref_type in ('m', 'n'):
                            # cross-database ref - not supported
                            continue
                if roid:
                    # record this reference
                    if reference_callback:
                        reference_callback(oid, roid)

                    # add the referenced object to the list of objects we need
                    # to visit
                    oids.append(roid)

            # yield the oid and pickle
            yield oid, p

Download graphviz_export.py

And after running this on a fresh Plone site, sending the result through dot and loading it in zgrviewer, here’s the result:

ZODB graphviz visualization

The site root is toward the upper right; most of the graph is persistent tools and such rather than actual content, since there is minimal content in a fresh Plone installation. That hairy mess on the left is the mimetype registry. Any resemblance to the shape of the BFG logo is entirely coincidental.

I’m not really sure what sort of useful information one might be able to get using this sort of technique, but I’m sure there are some possibilities, so please let me know if you have ideas or if you modify this to do something cool.

I want to try this on a site that has real data in it, but at the moment I’m waiting for the latest XCode to download so that I can build the newest graphviz which includes sfdp which is supposed to be better for handling really big graphs.



New product: collective.weightedportlets

Ever been frustrated with not being able to control exactly what order your portlets show up in, if you’ve got inherited portlets, content type portlets, and group portlets? I just released collective.weightedportlets, which allows you to specify an integer weight for each portlet assignment, which will be taken into account in the final ordering. See the Plone.org product page for details.



A buildout for Plone 2.0.5

At ONE/Northwest we’re always looking for ways to improve and streamline our system administration tasks. Recently, we’ve been working on converting all our old Zope instances to be buildout-based (to make it easier to recreate the environment for local testing of changes or in case the instance needs to move to another server). Here are some tips based on things we’ve learned in the process of putting together our buildout for Plone 2.0.5 …

Use the right Python

Plone 2.0.5 is based on Zope 2.7, which requires Python 2.3 rather than Python 2.4 like modern versions of Zope. (We tested using Python 2.4 and it seems to work okay; however Zope 2.7’s RestrictedPython has not been audited in Python 2.4 and there’s no guarantee that users with the rights to edit scripts won’t be able to do something nasty.)

I installed Python 2.3 using macports, then made sure to bootstrap and run my buildout using Python 2.3. Buildout initially complained about the ’subprocess’ module being missing, but I was able to work around this by copying subprocess.py from my Python 2.4 libs (/opt/local/lib/python2.4/subprocess.py in my case) into the Python 2.3 libs.

Update: Recent versions of plone.recipe.zope2install use some Python generators which aren’t compatible with Python 2.3, so I had to pin this egg to version 3.2.

Fry up some products

In a classic Zope installation you keep all your products in one Products directory. In buildout they are typically spread between several different product directories. In the case of Zope 2.7, we were seeing an issue where it only found products located in a Products directory at the root of the buildout, even if we listed additional directories. To work around this, I used collective.recipe.omelette to symlink the various buildout-generated products dirs into the main Products dir that Zope finds. Always nice to find a new use for a tool I designed for a completely different problem!

We do our development on OS X which uses a case insensitive filesystem, so I started using /svnproducts as a replacement for the /products dir that is often found in buildouts, since this would otherwise conflict with the auto-generated /Products.

Your configuration is no good here

Unfortunately the zope.conf that the plone.recipe.zope2instance recipe generates contains a couple bits of configuration (verbose-security and default-zpublisher-encoding) that cause Zope 2.7 to barf, since they were not added until later versions of Zope. To work around this, we used sed (via the plone.recipe.command recipe) to remove the offending bits.

The buildout

I ripped out the bits specific to our own systems and ended up with the following, which incorporates the above learnings. If I didn’t mess up while abridging it, it even works!

[buildout]
parts =
    plone
    zope2
    productdistros
    omelette
    instance
    fixer
versions = versions

[versions]
plone.recipe.zope2install = 3.2

[plone]
recipe = plone.recipe.distros
urls = http://heanet.dl.sourceforge.net/sourceforge/plone/Plone-2.0.5.tar.gz
nested-packages = Plone-2.0.5.tar.gz
version-suffix-packages = Plone-2.0.5.tar.gz

[zope2]
recipe = plone.recipe.zope2install
url = http://www.zope.org/Products/Zope/2.7.7/Zope-2.7.7-final.tgz
fake-zope-eggs = false

# Archetypes and kupu are not strictly required, but here's how to get them if you need them.
[productdistros]
recipe = plone.recipe.distros
urls =
    http://voxel.dl.sourceforge.net/sourceforge/archetypes/Archetypes-1.3.2-final-Bundle.tar.gz
    http://plone.org/products/kupu/releases/1.3.9/kupu.tgz
nested-packages =
    Archetypes-1.3.2-final-Bundle.tar.gz
version-suffix-packages =

[omelette]
recipe = collective.recipe.omelette
eggs =
packages =
    ${buildout:directory}/svnproducts .
    ${buildout:directory}/parts/productdistros .
    ${buildout:directory}/parts/plone .
location = ${buildout:directory}/Products

[instance]
recipe = plone.recipe.zope2instance
zope2-location = ${zope2:location}
user = admin:admin
http-address = 8080
debug-mode = on
verbose-security = on
products =
    ${buildout:directory}/Products

[fixer]
recipe = plone.recipe.command
command =
    sed -i '' 's/verbose-security/#verbose-security/' ${buildout:directory}/parts/instance/etc/zope.conf
    sed -i '' 's/default-zpublisher-encoding/#default-zpublisher-encoding/' ${buildout:directory}/parts/instance/etc/zope.conf
update-command = ${fixer:command}

Many thanks to my colleague Jon Baldivieso who did some of the initial work on this buildout.

Update 5/1/2009: Added fake-zope-eggs = false to avoid trying to build fake eggs from a directory that doesn’t exist in Zope 2.7.

Update 8/21/2009: Pinned plone.recipe.zope2install to version 3.2, as newer versions use Python generators that aren’t compatible with Python 2.3.