Showing posts with label WSGI. Show all posts
Showing posts with label WSGI. Show all posts

Friday, July 31, 2009

Plone 3.3 and Repoze

While I've already discussed what's generally required for making Plone run with Repoze and mod_wsgi, I had been using Plone 3.3rc3. Anyone (myself included) who's tried to sub in anything more recent (like Plone 3.3rc4) has run into a problem with version conflicts due to changes between 3.3rc3 and 3.3rc4. Specifically the upgrade of five.localsitemanager from 0.4 to 1.1 sets off a zope.component version conflict, as any version of it 1.0 or beyond tries to pull in a whole bunch of eggs that aren't really necessary. Now obviously pinning five.localsitemanager to a version less than 1.0 will allow the buildout to complete properly, but this isn't really a good solution as it creates a mutant (and thus untested) version of Plone that may or may not work in random situations.

The real fix isn't that hard, though. It's simply a matter of utilizing fake Zope 2 eggs. The only catch here is that in order for them to work, they must be in place before the Plone eggs get pulled, but as always they cannot be created until after Zope 2 itself is in place. This necessitates splitting the one Zope part I showed last time into two and sticking a fake eggs part in between. The second part will have to reference the eggs of the first part. To make things just a little more interesting, z3c.recipe.fakezope2eggs expects a location to be provided by the [zope2] part. We thus have to create one. The changed section will look as follows:

[zope2]
recipe = zc.recipe.egg
dependent-scripts = true
location = ${buildout:directory}/parts
eggs =
    lxml
    repoze.zope2

[zopeliblink]
recipe = iw.recipe.cmd
on_install = true
on_update = true
cmds =
    mkdir -p ${buildout:directory}/parts/lib/python
    ln -sh ${buildout:directory}/eggs/zopelib*/zope ${buildout:directory}/parts/lib/python/zope

[fakezope2eggs]
recipe = z3c.recipe.fakezope2eggs

[plone]
recipe = zc.recipe.egg
dependent-scripts = true
interpreter = zopepy
eggs =
    ${zope2:eggs}
    Plone
    PIL
    Products.DocFinderTab
    Products.ExternalEditor
    plone.openid
    deliverance
    repoze.urispace
    repoze.dvselect
    mysite.policy

It's probably obvious, but just in case it's not, the two new parts have to be added to the parts section near the top:

parts =
    lxml
    zope2
    zopeliblink
    fakezope2eggs
    plone
    instance
    slugs
    addpaths

Don't forget that the order is important, as the fake Zope eggs have to be created after Zope proper but before Plone.

Deliverance and URISpace should continue to work without changes.

Also, I've created eggs for zopelib 2.10.8.0 and ZODB 3.7.3 (the official versions that the Plone 3.3 builds are based upon; I've also made a zopelib 2.11.3 for people using straight Zope) and submitted them to the Repoze guys for inclusion in their distribution section. If you'd like to play around with these before they become official, you can find them in the Saugus.net distribution section.

Wednesday, July 15, 2009

Repoze, Zope, Plone, URISpace, and Deliverance

I've further developed the technique I outlined in my last post for making Plone run with Repoze and mod_wsgi. In particular I've included the ability to skin via Deliverance (including the ability to separate out different sections of the site for different treatments via URISpace) and enhanced the addpaths.py script to remove the limitations I'd indicated were there without going into details; it now both handles update cases better and is far more intelligent with regards to ensuring that egg path order is maintained regardless of whether Paste or mod_wsgi is being used. If you had trouble getting my prior instructions to work in your environment, you may just want to update to the smarter addpaths.py included below.

My use case is Saugus.org (although note that the current public version may not yet reflect the changes discussed in this post as they're obviously being done on a separate development server). Basically several different not-for-profit entities have their sites located on this domain, and while there is a significant overlap between members of the various sub-sites (probably 10% - 25% or so) it's certainly not absolute, and each sub-site has different administrators and different styles. While there are ways of making Deliverance theme things differently for different portions of a site, I didn't really care for any of them in this application as they'd make it too easy to break things for particular sub-sites and/or make it too difficult to actually do the theming for individual sub-sites. The obvious solution is repoze.urispace (which implements the W3C URISpace specification in a way that can be used to make Deliverance distinguish between different portions of a site). With repoze.urispace, it becomes easy to give each sub-site its own independent set of Deliverance rules and themes, and working on one will in no way affect another.

Making It Happen

Now that you understand the motivation it's time to see the implementation. Generally one needs to follow the instructions I outlined in my earlier post, but make a few changes:

  1. First, substitute its copy of addpaths.py with the following much improved version prior to running bin/buildout:

    import os
    from dircache import listdir
    
    # The bin directory holds all the executables for the buildout
    BinDir = 'bin'
    
    # The unadorned files aren't given any of the egg paths on creation
    UnadornedFiles = (os.path.abspath(BinDir+'/zope2.wsgi'),)
    
    # Most regular files are given the egg paths, but not the old-style products path
    RegularFiles = [os.path.abspath('%s/%s'%(BinDir,filename)) for filename in listdir(BinDir)]
    
    # Eggs can live in more than one location
    EggDirs = ('eggs',)
    
    # Old style products should all be contained in one location
    ProductsPath = os.path.abspath('products')
    
    # The sample file should be regular file with a regular egg path
    SampleFile = os.path.abspath(BinDir+'/paster')
    
    def main(options, buildout):
        # We have to ensure that the zopelib directory is earlier in the search path
        # than other possibly competing packages.  Unfortunately, we don't know its
        # full name a priori and have to hunt it down first.
        for eggDir in EggDirs:
            for filename in listdir(eggDir):
                if 'zopelib' in filename:
                    zopelibPath=os.path.abspath('%s/%s/%s'%(os.getcwd(),eggDir,filename))
                    break
    
        # First we handle the regular files.  We both relocate the zopelib component
        # (if present) to the beginning of the list and prepend the old-style products
        # path.  Note that this will ignore unadorned files mixed in.
        for filename in RegularFiles:
            lines = open(filename, 'r').readlines()
            file = open(filename, 'w')
            for line in lines:
                if 'zopelib' not in line:
                    file.write(line)
                if line.startswith('sys.path'):
                    if ProductsPath not in ' '.join(lines):
                        file.write("  '%s',\n"%ProductsPath)
                    if zopelibPath:
                        file.write("  '%s',\n"%zopelibPath)
            file.close()
    
        # The path list should now be perfect in our sample file.  Grab it.
        lines=open(SampleFile,'r').readlines()
        lineNum=begLineNum=endLineNum=0
        for line in lines:
            lineNum+=1
            if line.startswith('import sys'):
                begLineNum=lineNum-1
            elif begLineNum and ']' in line:
                endLineNum=lineNum
        eggLines=lines[begLineNum:endLineNum]
    
        # Now we're ready to handle the unadorned files.  Simply replace any existing
        # sys path with the good one we've obtained from the sample, or add it if nothing
        # yet exists.  We're assuming that in a healthy file the import os statement will
        # occur after the import sys statement.
        for filename in UnadornedFiles:
            lines = open(filename,'r').readlines()
            alreadyProcessed=False
            lineNum=begLineNum=endLineNum=0
            for line in lines:
                lineNum+=1
                if line.startswith('import sys'):
                    alreadyProcessed=True
                    begLineNum=lineNum-1
                elif begLineNum and ']' in line:
                    endLineNum=lineNum
                elif line.startswith('import os') and not alreadyProcessed:
                    begLineNum=endLineNum=lineNum-1
            lines[begLineNum:endLineNum]=eggLines
            file = open(filename,'w')
            file.writelines(lines)
            file.close()
    
        # All done!  Let's just tell the user roughly what we've done.
        print "Egg paths added to %s" % ', '.join(UnadornedFiles)
        print "Product path added to %s" % ', '.join(RegularFiles)
    
  2. Next, a slightly modified buildout.cfg must be used (obviously also before running bin/buildout). The following should work:

    [buildout]
    extends =
        http://good-py.appspot.com/release/repoze.zope2/1.0
        http://dist.plone.org/release/3.3rc3/versions.cfg
    
    versions = versions
    
    find-links =
        http://dist.repoze.org/zope2/latest
        http://dist.repoze.org/zope2/dev
        http://dist.plone.org/release/3.3rc3
        http://download.zope.org/ppix/
        http://download.zope.org/distribution/
        http://effbot.org/downloads
    
    develop =
        src/mysite.policy
    
    parts =
        lxml
        zope2
        instance
        slugs
        addpaths
    
    [versions]
    zopelib = 2.10.7.0
    
    [lxml]
    recipe = z3c.recipe.staticlxml
    egg = lxml
    libxml2-url = http://xmlsoft.org/sources/libxml2-2.7.2.tar.gz
    libxslt-url = http://xmlsoft.org/sources/libxslt-1.1.24.tar.gz
    
    [zope2]
    recipe = zc.recipe.egg
    dependent-scripts = true
    interpreter = zopepy
    eggs =
        lxml
        repoze.zope2
        Plone
        PIL
        Products.DocFinderTab
        Products.ExternalEditor
        plone.openid
        deliverance
        repoze.urispace
        repoze.dvselect
        mysite.policy
    
    [slugs]
    recipe = collective.recipe.zcml
    zope2-location=${buildout:directory}
    zcml =
        mysite.policy
    
    [instance]
    recipe = iw.recipe.cmd
    on_install = true
    cmds =
       bin/mkzope2instance --use-zeo --zeo-port=${buildout:directory}/var/zeo.zdsock --zope-port=8888
       sed -i "" "s/server localhost:/server /" ${buildout:directory}/etc/zope.conf
       echo "Please run 'bin/runzeo -C etc/zeo.conf' and 'bin/paster serve etc/zope2.ini', then 'bin/addzope2user  '"
    
    [addpaths]
    recipe = z3c.recipe.runscript
    install-script = addpaths.py:main
    update-script = addpaths.py:main
    

    As before note the dummy mysite.policy product included to show the most general solution. If you are not developing any products and do not need any additional ZCML slugs, these can be totally omitted. Otherwise they should be changed accordingly.

  3. Next, add rules and themes subdirectories to the etc directory.

  4. Create a default.xml file in the rules directory. The following sample:

    <?xml version="1.0" encoding="UTF-8"?>
    <rules xmlns:xi="http://www.w3.org/2001/XInclude"
           xmlns="http://www.plone.org/deliverance">
      <replace theme="/html/head" content="/html/head" />
      <replace theme="/html/body" content="/html/body||/html/frameset" />
    </rules>
    

    is basically a NOP and will get you started. There's more information on the Deliverance site on the sorts of commands you can add in here. Ultimately you'll be making more of these rules files with each one being used for a different section of your site.

  5. Create a default.html file in the themes directory. Something as basic as:

    <html>
      <head>
        <title>default.html theme for Deliverance</title>
      </head>
      <body>
      </body>
    </html>
    

    will likewise be enough to get you started. You can ultimately create this file however you'd like using any sorts of HTML generation tools that tickle your fancy. Ultimately you'll be making more of these theme files (probably one to go with each rules file) with each one being used for a different section of your site.

  6. Lastly you'll need to create a URISpace configuration file urispace.xml in the etc directory. While there's more info on the repoze.urispace site on the options that you can include in here, the following is a minimal one that directs everything to the default rules and theme we have already defined:

    <?xml version="1.0" ?>
    <themeselect
       xmlns:uri='http://www.w3.org/2000/urispace'
       xmlns:uriext='http://repoze.org/repoze.urispace/extensions'
       xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'
       >
    
     <!-- default theme and rules -->
     <theme>file:///home/eric/WSGI/SaugusOrg/etc/themes/theme.xhtml</theme>
     <rules>file:///home/eric/WSGI/SaugusOrg/etc/rules/rules.xml</rules>
    </themeselect>
    
  7. Finally, before trying to run the new site one must ensure that the WSGI pipeline is properly prepared. The following new zope2.ini will do so:

    [DEFAULT]
    debug = True
    
    [app:zope2]
    paste.app_factory = repoze.obob.publisher:make_obob
    repoze.obob.get_root = repoze.zope2.z2bob:get_root
    repoze.obob.initializer = repoze.zope2.z2bob:initialize
    repoze.obob.helper_factory = repoze.zope2.z2bob:Zope2ObobHelper
    zope.conf = %(here)s/zope.conf
    
    [filter:errorlog]
    use = egg:repoze.errorlog#errorlog
    path = /__error_log__
    keep = 20
    ignore = paste.httpexceptions:HTTPUnauthorized
           paste.httpexceptions:HTTPNotFound
           paste.httpexceptions:HTTPFound
    
    [filter:deliverance]
    use = egg:deliverance#main
    theme_uri = http://www.example.com/
    rule_uri = file:///%(here)s/rules/rules.xml
    
    [filter:urispace]
    use = egg:repoze.urispace#urispace
    filename = %(here)s/urispace.xml
    
    [pipeline:main]
    pipeline = egg:Paste#cgitb
               egg:Paste#httpexceptions
    #           egg:Paste#translogger
               egg:repoze.retry#retry
               egg:repoze.tm#tm
               egg:repoze.vhm#vhm_xheaders
               errorlog
               urispace
               egg:repoze.dvselect#main
               deliverance
               zope2
    
    # Note: replace egg:Paste#cgitb with egg:Paste#evalerror above to get
    # the browser to display eval'able traceback stacks (unsuitable for
    # production).
    
    # If you enable (uncomment) the translogger, it will show access log
    # info to the console.
    
    [server:main]
    use = egg:repoze.zope2#zserver
    host = 127.0.0.1
    port = 8888
    

    The theme referenced in the Deliverance section will never actually be used as it'll be overridden by repoze.urispace.

Details To Note

First off, thanks to Tres Seaver not just for originally writing repoze.urispace and its companion repoze.dvselect, but for also putting up with (and acting on) both my bug reports and suggestions for making things easier to use. I had originally gotten this all working with an earlier version of repoze.urispace and and the original unreleased version of repoze.dvselect, but the method described here is much cleaner, and it wouldn't have been possible without Tres' numerous recent updates to both repoze.urispace and repoze.dvselect.

Second, as seems to happen a lot lately, just as I was starting to write this I was informed of another effort to do something similar. Wojciech Lichota's technique seems to do some things better and some things worse than the technique described above. Depending upon what you're doing, you may find that what he's doing more directly addresses your needs. Probably a hybrid technique will be better than either...

Also, one may wonder about the whole z3c.recipe.staticlxml used above to build lxml and whether or not it's really necessary. It may look like extra work, but this method allows the exact same buildout to work on Mac OS X in addition to regular more traditional UNIX and UNIX-like environments.

Finally, note that as presented here Deliverance and URISpace won't actually do anything... it's expected that you'll add rules and themes appropriate to your own project.

Friday, June 19, 2009

Buildout with Repoze, Zope, and Plone

I know in my last post I indicated that I'd post more about the various technologies I'm using for the Antichrist Watch site, but I've now had a few requests related to some of the Repoze work I'm doing with some of the local Saugus, MA sites, so I'll be making a slight detour. I will get back to the Dojo stuff. I promise.

I discovered Repoze when first dabbling around with what would eventually become the aforementioned Antichrist Watch, and in fact I directly made use of repoze.chameleon to handle its templating needs. I liked what I saw, and decided to try first experimenting a bit with it and then actually porting over some real-world sites to it. We were in the process of doing some hardware upgrades on the some servers anyway, so the sites they hosted seemed liked good candidates. To make things interesting, most of them came in basically two distinct flavors: straight Zope, and customized Plone.

The first group was largely composed of mostly non-technical customers who take advantage of the bottom of the so-called "Z shaped curve" to do basic site edits through the Web. Some of these customers enjoy having fairly current versions of Zope. The second group featured various degrees of customization, from the fairly vanilla to the strikingly different. Most of these were handled via custom Plone products designed more or less in the manner described in Martin Aspeli's Professional Plone Development. With this logical grouping, buildout seemed a logical choice for site construction as I could make just three buildouts and then trivially generate as many sites as desired. The only problems were that the Repoze guys themselves seem not to use it much and (understandably considering the drudgery involved) don't have prepackaged versions of the latest-and-greatest of either Zope or Plone currently available.

Ultimately I wanted all the sites running through mod_wsgi using as few physical servers as practical. I wanted to only have one supervisord instance per physical server monitoring all the ZEO servers it contained. I also wanted to minimize the number of ports in use in order to reduce the bureaucracy of port tracking we'd have to do afterwards.

Getting current versions of Zope and Plone running under Repoze turned out not to be so simple. I read a few articles on the topic in addition to the Repoze Quick Start, but due to differences in versions and/or environment none of them did what I needed.

Making It Happen

Enough introduction! Here's what I did for the variant with Plone:

  1. paster create -t zope2_buildout targetname

    It doesn't much matter what answers are given, as buildout.cfg gets overwritten anyway.

  2. cd targetname

  3. Replaced buildout.cfg with the following:

    [buildout]
    extends =
        http://good-py.appspot.com/release/repoze.zope2/1.0
        http://dist.plone.org/release/3.3rc3/versions.cfg
    
    versions = versions
    
    find-links =
        http://dist.repoze.org/zope2/latest
        http://dist.repoze.org/zope2/dev
        http://dist.plone.org/release/3.3rc3
        http://download.zope.org/ppix/
        http://download.zope.org/distribution/
        http://effbot.org/downloads
    
    develop =
        src/mysite.policy
    
    parts =
        zope2
        instance
        slugs
        addpaths
    
    [zope2]
    recipe = zc.recipe.egg
    dependent-scripts = true
    interpreter = zopepy
    eggs =
        repoze.zope2
        Plone
        PIL
        Products.DocFinderTab
        Products.ExternalEditor
        plone.openid
        mysite.policy
    
    [slugs]
    recipe = collective.recipe.zcml
    zope2-location=${buildout:directory}
    zcml =
        mysite.policy
    
    [instance]
    recipe = iw.recipe.cmd
    on_install = true
    cmds =
       bin/mkzope2instance --use-zeo --zeo-port=${buildout:directory}/var/zeo.zdsock --zope-port=8888
       sed -i "" "s/server localhost:/server /" ${buildout:directory}/etc/zope.conf
       echo "Please run 'bin/runzeo -C etc/zeo.conf' and 'bin/paster serve etc/zope2.ini', then 'bin/addzope2user  '"
    
    [addpaths]
    recipe = z3c.recipe.runscript
    install-script = addpaths.py:main
    update-script = addpaths.py:main
    
  4. Added the file addpaths.py to the buildout's top level directory with the following contents:

    import os
    from dircache import listdir
    
    BinDir = 'bin'
    UnadornedFiles = ('bin/zope2.wsgi',)
    RegularFiles = ['%s/%s'%(BinDir,filename) for filename in listdir(BinDir)]
    EggDirs = ('eggs',)
    ProductsPath = os.path.abspath('products')
    
    def main(options, buildout):
        for filename in UnadornedFiles:
            lines = open(filename,'r').readlines()
            file = open(filename,'w')
            alreadyProcessed=False
            for line in lines:
                if line.startswith('import sys'):
                    alreadyProcessed=True
                elif line.startswith('import os') and not alreadyProcessed:
                    file.write("import sys\nsys.path[0:0] = [\n")
                    for eggDir in EggDirs:
                        for filename in listdir(eggDir):
                            file.write("  '%s',\n"%os.path.abspath('%s/%s'%(eggDir,filename)))
                    file.write("  ]\n\n")
                file.write(line)
        for filename in RegularFiles:
            lines = open(filename, 'r').readlines()
            file = open(filename, 'w')
            for line in lines:
                file.write(line)
                if line.startswith('sys.path'):
                    file.write("  '%s',\n"%ProductsPath)
            file.close()
    
        print "Egg paths added to %s" % ', '.join(UnadornedFiles)
        print "Product path added to %s" % ', '.join(RegularFiles)
    
  5. python2.4 bootstrap.py

  6. bin/buildout

    Just ignore any 'return' outside function types of errors you see.

Once these steps have been completed, ZEO can be started with the command bin/runzeo -C etc/zeo.conf (which can be easily controlled via supervisord) and Zope can be started manually for testing with bin/paster serve etc/zope2.ini or in a more production-ready form via mod_wsgi using zope2.wsgi.

Details To Note

I borrowed but heavily modified the addpath.py concept already living in the Plone collective to add all missing paths to all the scripts in bin. The WSGI script as created lacks pretty much everything, and all the others lack the products directory used for old-style products. As written it's not very clever and could be greatly improved, but it serves my current needs.

The order of parts matters somewhat, as both slugs and addpaths rely on pieces created earlier.

I'm using direct sockets in lieu of ports since these sites are living on single physical servers anyway and it means I have less to manage afterwards. Unfortunately the mkzope2instance doesn't do exactly what one might want in this case, so the sed line is necessary afterwards to clean up.

The slugs section adds ZCML slugs for those products that need them, including the hypothetical product mysite.policy being actively developed in src.

I just recently discovered Martin Aspeli's Good-Py. I was originally individually pinning the versions of the pieces that mattered, and only made this simplification this morning... so far it seems to be fine, though.

Just before starting this post I spotted Alex Clark's post also discussing this topic. We're doing a lot alike, but a few things differently. Depending upon what you're doing, you may find that what he's doing more directly addresses your needs.

Earlier on I mentioned how I needed to handle two main groups of sites: Plone ones and straight Zope ones. The above instructions only cover how I handled the Plone ones. This post has gotten long enough already, and although my treatment of the straight Zope ones is similar it's different enough to make things confusing for those who don't need it. If you need it, let me know and I'll probably give it a similar treatment to what I did here.