Benjamin W. Smith

Benjamin W. Smith

Benjamin W. Smith  //  Sysadmin by trade, Pythonista by passion. Dad to two boys and a girl. Guitarist. I like my coffee black, just like my metal.

Jan 18 / 8:06am

byteflow/django supervisord nginx = WIN

Recently I made the final step in converting my website over to nginx. I decided to manage my django application, byteflow, with supervisor. I have had great success with this over at AGI and thought I should advocate the success I’ve had to the public by extending it to my own site.

I first heard about supervisor at pycon last year and thought it could be useful in many ways, especially at the office. In a nutshell, supervisor ‘supervises’ processes, and allows you to manage them with a simple interface, but I’ll go into more detail about it later. Around the same time that I discovered supervisor I had also started experimenting with nginx and fastcgi to run my blog. I ended up going with lighty and fastcgi instead, however, mainly due to familiarity. Things change, and so do my opinions on technology. Nginx sold me on it’s performance and simplistic configuration, plain and simple. So now, here’s the meat and potatoes.

To break it down, here is what we’re looking at.

INTARWEBS -> nginx -> fastcgi -> django/byteflow(managed by supervisor)

Everything below assumes you have installed nginx, django, byteflow and supervisor.

So how about a look at the actual setup.

First, let’s take a look at the code to fire up the django app. I’ve dubbed it runserver.py and put it in my byteflow code tree.

#!/usr/bin/env python
if __name__ == '__main__':
    from flup.server.fcgi_fork import WSGIServer
    from django.core.handlers.wsgi import WSGIHandler
    WSGIServer(WSGIHandler()).run()

This keeps the django devs happy by not relying on manage.py to keep up with the server stuffs.

It’s pretty simple.. Starts up flups’ WSGIServer and uses django’s WSGIHandler to do the dirty work. Not much to it, really.

Now, lets take a look at the supervisor setup. You can get a lot of what you need by running:

# sudo echo_supervisord_conf > /etc/supervisord.conf

That sets up a basic (but verbose!) supervisor config. I’ve trimmed it down to this:

[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)

[supervisord]
logfile=/var/log/supervisord/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB       ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10          ; (num of main logfile rotation backups;default 10)
loglevel=info               ; (log level;default info; others: debug,warn,trace)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=false              ; (start in foreground if true;default false)
minfds=1024                 ; (min. avail startup file descriptors;default 1024)
minprocs=200                ; (min. avail process descriptors;default 200)
user=nobody                 ; (default is current user, required if root)
childlogdir=/var/log/supervisord/            ; ('AUTO' child log dir, default $TEMP)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket

; Production setup
[fcgi-program:django_fcgi]
socket=tcp://127.0.0.1:8080  ; We reference this later in nginx
command = /home/bsmith/Dev/byteflow/runserver.py  ; Calls the above code
environment=PYTHONPATH=/home/bsmith/Dev/byteflow  ; Setup needed environment
environment=DJANGO_SETTINGS_MODULE=settings

; Development setup
[fcgi-program:django_dev_fcgi]
socket=tcp://127.0.0.1:8081
command = /home/bsmith/Dev/byteflow_new/runserver.py
environment=PYTHONPATH=/home/bsmith/Dev/byteflow_new
environment=DJANGO_SETTINGS_MODULE=settings

Simple enough, eh? Comments in the configuration should explain what’s going on.

Now you can crank it up by running:

$ sudo supervisord

and check the status…

$ sudo supervisorctl status
django_dev_fcgi:django_dev_fcgi_0 RUNNING    pid 15949, uptime 0:00:08
django_fcgi:django_fcgi_0        RUNNING    pid 15950, uptime 0:00:08

Could it be more simple? I doubt it. This barely begins to scratch the surface of what supervisor can do. Process groups/pools, XML-RPC interface for remote management, built-in web interface for process management(utilizing XML-RPC interface), tons of process management options (priority, umask, user/group, capture std* pipes, environment variables, auto restart/start, process naming), event listeners/handling and a simple configuration ta boot! All I’m sayin’ is, it’s awesome…I’m just sayin’.

Keeping with our simple but awesome theme, enter nginx…

My main config:

user www-data;
# Could vary by number of processors available.
worker_processes  1;

error_log  /var/log/nginx/error.log;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
    use epoll;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    access_log      /var/log/nginx/access.log;

    sendfile        on;

    keepalive_timeout  65;
    tcp_nodelay        on;

    gzip  on;

    include /etc/nginx/sites/*;

}

and my various site-specific configurations that I keep /etc/nginx/sites:

server {
        # We listen on port 80
        listen 80;
        server_name just-another.net;

        # access and error logs for our site
        access_log /var/log/nginx/my_site_access_log;
        error_log /var/log/nginx/my_site_error_log;

        # Configure redirect for our fastcgi server
        # The fastcgi server later runs on localhost Port 8080
        location / {
                expires 10d;
                # to fastcgi server could use socket..
                #fastcgi_pass unix:{project_location}/log/django.sock;
                fastcgi_pass 127.0.0.1:8080;
                fastcgi_param PATH_INFO $fastcgi_script_name;
                fastcgi_param REQUEST_METHOD $request_method;
                fastcgi_param QUERY_STRING $query_string;
                fastcgi_param CONTENT_TYPE $content_type;
                fastcgi_param CONTENT_LENGTH $content_length;
                fastcgi_param REMOTE_ADDR $remote_addr;
                fastcgi_pass_header Authorization;
                fastcgi_intercept_errors on;
        }

        location /images {
    # For my own images..
            alias /home/bsmith/public_html/images;
            expires 10d;
        }

        # Alias for static content like themes
        location /static {
            alias /home/bsmith/Dev/byteflow/static;
            expires 10d;
        }

        # Alias for python contrib.admin stuff, needed for admin interface
        location /admin-media {
    # Point to my most recent install of django.
            alias /home/bsmith/Dev/django_trunk/django/contrib/admin/media;
            expires 10d;
        }
        # Point to media
        location /media {
            alias /home/bsmith/public_html/media;
            expires 10d;
        }
        # Use feedburner for my feeds.
        rewrite ^/rpc(.*) http://feeds.feedburner.com/Just-anothernetBlogPosts$1 permanent;
}

Pretty simple, eh? I have the same server setup for my ‘dev’ site, only with a volatile code base that I can hack on.

Now when I make code changes all I need to do is reload the supervisor process like so:

$ sudo supervisorctl restart django_fcgi:django_fcgi_0

for the production site or replacing the process name with django_dev_fcgi:django_dev_fcgi_0 to restart the dev site. I could also restart everything by replacing a process name with all as an argument.

The possibilities here for process management endless and I couldn’t be happier with how simple it has become to run what I have.

Basically, this setup is easy to get running and easy to keep running. Highly recommended for any lazy django site maintainer like myself!

Filed under  //  byteflow   django   nginx   python   supervisor  
Jun 4 / 8:11pm

Byteflow usability quick change.

Submitted this to the byteflow hackers, figured some might be able to use it as well. Code blocks don’t wrap lines (<pre>), probably want to have control of how it overflows.

Here’s the patch:

# HG changeset patch
# User bsmith@linode.just-another.net
# Date 1212606740 14400
# Node ID 9337a362eeacbe88db2da11d38e238db2644e31b
# Parent  93c1106436dd09dde52ec1dd25cfbbeca7cec00b
Add scroll if lines don't wrap in code blocks.

diff -r 93c1106436dd -r 9337a362eeac static/css/highlight.css
--- a/static/css/highlight.css  Wed Jun 04 09:47:44 2008 +0300
+++ b/static/css/highlight.css  Wed Jun 04 15:12:20 2008 -0400
@@ -1,6 +1,7 @@ pre code {
 pre code {
    display: block;
    background: #F0F0F0;
+   overflow:auto;
 }

pre code,
Filed under  //  byteflow   css   patch   python  
Jun 3 / 8:55pm

Byteflow preview blog post

Byteflow has a feature when authoring blogs that allows you to save your post as a draft. I use it constantly, but someone requested via a ticket to have a “Preview” option, allowing you to see your blog post (logically the same as saving it as a draft) before you post it. If anyone is interested in utilizing a “Preview” option, see the patch below. Basically all it does is utilize the draft option, but takes you to the generated draft to “Preview” it before you make it public. I don’t think it will be committed into the code base, but if anyone is looking to have this functionality now, see below code.

# HG changeset patch
# User bsmith@linode.just-another.net
# Date 1212543690 14400
# Node ID 4282e3429846d5193a798bdd14807f6c3ef579ac
# Parent  2c4fb2b21cc5c7b9cfbf7a1692af6e34a9bb5391
Preview post (and draft it).

diff -r 2c4fb2b21cc5 -r 4282e3429846 apps/blog/models.py
--- a/apps/blog/models.py       Mon Jun 02 19:41:33 2008 -0400
+++ b/apps/blog/models.py       Tue Jun 03 21:41:30 2008 -0400
@@ -7,7 +7,7 @@ from django.utils.html import strip_tags
 from django.utils.html import strip_tags
 from django.contrib.contenttypes import generic
 from django.dispatch import dispatcher
—
+from lib.exceptions import RedirectException
 from lib.helpers import reverse
 from render import render
 from blog.managers import PostManager, PublicPostManager, FeaturedPostManager
 @@ -38,6 +38,7 @@ class Post(models.Model):
    upd_date = models.DateTimeField(_(u'Date'), auto_now=True, editable=False)
     is_draft = models.BooleanField(verbose_name=u'Post would act as draft', default=False)
     is_featured = models.BooleanField(verbose_name=u'Featured post', default=False)
+    is_preview = models.BooleanField(verbose_name=u'Preview Before Submission', default=True)
    enable_comments = models.BooleanField(default=True)
    tags = TagField()

@@ -53,7 +54,7 @@ class Post(models.Model):
        search_fields = ('name', 'text')
        list_filter = ('date', )
        fields = (
—            (None, {'fields': ('author', ('name', 'slug'), 'tags', 'text', 'render_method', 'date', ('is_draft', 'enable_comments'))}),
+            (None, {'fields': ('author', ('name', 'slug'), 'tags', 'text', 'render_method', 'date',('is_preview','is_draft', 'enable_comments'))}),
            ('Featured post', {'classes': 'collapse', 'fields': ('is_featured', 'teaser')}),
            )
        if settings.WYSIWYG_ENABLE:
@@ -80,7 +81,13 @@ class Post(models.Model):
            self.slug = slugify(self.name)
            self.text = self.text.strip()
            self.html = render(self.text, self.render_method, unsafe=True)
—        super(Post, self).save()
+        if self.is_preview:
+            message = 'This is a preview of your post'
+            self.is_draft = True
+            super(Post, self).save()
+            raise RedirectException(self.get_absolute_url(), notice_message=message)
+        else:
+            super(Post, self).save()

    def comments_open(self):
        if settings.COMMENTS_EXPIRE_DAYS:

And to implement it via a template, try something like this:

{% if object.is_preview %}
  <div id="post-draft">THIS IS A PREVIEW Continue Editing</div>
{% endif %}
Filed under  //  byteflow   patch   python  
Jun 2 / 5:45am

Yay for contribution!

I was having troubles with the way my rss item categories (tags) were displaying on technorati and other feed readers, so I decided to fix it. This is why I love F/OSS! Since I’ve been using this software I’ve contributed a whopping 2 patches (heh, heh). It might not be a lot, but it matters! I didn’t post my last patch, but I will from now on. Here’s the most recent one:

# HG changeset patch
# User bsmith@linode.just-another.net
# Date 1212450093 14400
# Node ID 2c4fb2b21cc5c7b9cfbf7a1692af6e34a9bb5391
# Parent  fb8d51dd50e13eb7d2b63c646a9f2bb5cf21ec3c
Atom class wants a dict for item categories, django does not.

diff -r fb8d51dd50e1 -r 2c4fb2b21cc5 apps/feed/blog_feeds.py
--- a/apps/feed/blog_feeds.py   Fri May 30 11:13:58 2008 +0300
+++ b/apps/feed/blog_feeds.py   Mon Jun 02 19:41:33 2008 -0400
@@ -59,7 +59,10 @@ def _BlogEntries(Feed, type='atom'):
             return {'type': 'html'}, html

         def item_categories(self, item):
—            return ({'term': unicode(tag)} for tag in item.get_tags())
+            if (type == 'atom'):
+                return ({'term': unicode(tag)} for tag in item.get_tags())
+            else:
+                return (unicode(tag) for tag in item.get_tags())

         def item_links(self, item):
             return ({'rel': u'self', 'href': self.item_id(item)},

Hope this helps!

Filed under  //  byteflow   oss   patch   python  
May 13 / 6:29am

I'm spent...and an apology

I just spent the last few hours polishing up my site for maximum awesomeness. It doesn’t look much different, you say, but under the hood is a completely different beast. As I mentioned in a previous post, I spent last night converting the backend of this site to byteflow, which is built on django (python based) and running on top of lighttpd.

Previously (read; years ago) I had built a blogging system based on a LAMP stack, specifically Apache 2.0, MySQL and PHP. Slowly, over time I added to and ‘improved’ the software. Time and time again it felt like an exercise in futility. It seemed to be a never ending battle to keep up with the times and stay exposed. Since I started working for ag interactive I’ve fallen in love with Python. Quickly thereafter I started searching for a good, ”developing” blog package written in python. I longed for something I could extend and something that is portable. I long no longer…(?) The django project has provided many facilities to build a great blog (and cms type thing) and I’m glad someone has finally stepped up to take advantage of them. I’m sure I’ll be submitting patches to this project as I discover things to tweak or improve.

To the byteflow crew: Great Job so far!

Now onto some of the details about what I did.

I love lighttpd, all of my recent development has taken advantage of it. It’s configuration is intuitive, it’s feature rich for it’s size, and it’s performance is hard to match. Converting the config for this was relatively simple. This is, of course, coming from a sysadmin who deals with hundreds of web servers daily :). Anywho, here’s the config with paths cleaned up for clarity:

# Main server settings
server.modules              = ( 
            "mod_access",
            "mod_alias",
            "mod_accesslog",
            "mod_rewrite", 
            "mod_redirect", 
            "mod_status", 
            "mod_fastcgi",
            "mod_compress",
            "mod_expire",
)

server.document-root       = "/docroot"
server.errorlog            = "/var/log/lighttpd/error.log"
index-file.names           = ( "index.php", "index.html", 
                                        "index.htm", "default.htm" )
accesslog.filename         = "/var/log/lighttpd/access.log"
url.access-deny            = ( "~", ".inc" )
server.pid-file            = "/var/run/lighttpd.pid"
dir-listing.encoding        = "utf-8"
server.dir-listing          = "enable"
debug.dump-unknown-headers  = "enable"
server.username            = "www-data"
server.groupname           = "www-data"
#status.status-url = "/server-status"
#status.config-url = "/server-config"
# Expires stuff, per yahoo tips :)
$HTTP["url"] =~ "^/" {
     expire.url = ( "" => "access 8 hours" )
}
# Set mime-types.
include_shell "/usr/share/lighttpd/create-mime.assign.pl"
# Allow for vhost configs
include_shell "/usr/share/lighttpd/include-conf-enabled.pl"
# Compress settings
compress.cache-dir          = "/var/tmp/lighttpd/cache/compress/"
compress.filetype           = ("text/plain", "text/html")

# Custom settings
# Set-up some aliases
alias.url = ( 
              "/admin-media" => "/django/contrib/admin/media",
              "/images" => "/docroot/images",
              "/static" => "/docroot/static"
)
# Startup the fastcgi server, point it at my custom
# fastcgi script.
fastcgi.server = (
    "/byteflow.fcgi" => (
        "main" => (
            "socket" => "/tmp/byteflow.socket",
            "bin-path" => "/docroot/byteflow.fcgi" 
        ) 
    ) 
)
# Make sure we're passing on these urls
# bare, don't process them with django.
url.rewrite-once = (
    "^(/rpc.*)$" => "$1",
    "^(/images.*)$" => "$1",
    "^(/static.*)$" => "$1",
    "^(/admin-media.*)$" => "$1",
    # Then push the rest through django
    "^(/.*)$" => "/byteflow.fcgi$1"
)
# Legacy url, redirect it to new feedburner.
url.redirect = (
    "^/rpc([\/]*)" => "http://feeds.feedburner..truncated..",
)

Here is my fast-cgi script, which resides in my docroot.

#!/bin/sh
# byteflow.fcgi

export PYTHONPATH=$PYTHONPATH:/home/bsmith/Dev
export DJANGO_SETTINGS_MODULE=byteflow.settings

/home/bsmith/Dev/byteflow/manage.py runfcgi

Compared to a run of the mill apache setup, this is cake walk. I commented the important bits so you can get the gist of what’s going on. If you have a question required more detail, let me know.

After spending a few hours converting my data to the model that django built for byteflow, I then focused on cloning my old theme. As you can see, I didn’t to a bad job! I tried to clean it up a bit and make sure I have more content for the search engines to munch on. Other than that bit of extra work, building the templates for this was pretty simple.

As you may have noticed, I also began using feedburner to serve up my rss feeds. That’s where the apology comes in..If you’re subscribed, most likely you have experienced duplicate posts..like..all of them. So, with that said So Sorry!.

Filed under  //  appology   byteflow   django   lighthttpd   python   site  
May 12 / 11:21am

Wow this software rocks.

I just converted all my tags and posts to run on this byteflow system. I’m impressed. It is by far the most full featured django blogging app I’ve seen. I was also able to get it jamming with lighttpd. Right now I’m running this from my development area until I can get comments imported. I couldn’t be happier with the results thus far! At some point today, once I’m done polishing things, I’ll post my configs and a little how-to.

Filed under  //  byteflow   django   lighthttpd   python   site  
May 12 / 11:21am

Wow this software rocks.

I just converted all my tags and posts to run on this byteflow system. I’m impressed. It is by far the most full featured django blogging app I’ve seen. I was also able to get it jamming with lighttpd. Right now I’m running this from my development area until I can get comments imported. I couldn’t be happier with the results thus far! At some point today, once I’m done polishing things, I’ll post my configs and a little how-to.

Filed under  //  byteflow   django   lighthttpd   python   site