<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>AdamFranco.com</title>
	<atom:link href="http://www.adamfranco.com/feed/" rel="self" type="application/rss+xml" />
	<link>http://www.adamfranco.com</link>
	<description>Musings, projects, software, and photography.</description>
	<lastBuildDate>Mon, 14 Jun 2010 16:03:57 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.0.1</generator>
		<item>
		<title>Adding reverse-proxy caching to PHP applications</title>
		<link>http://www.adamfranco.com/2010/06/14/adding-reverse-proxy-caching-to-php-applications/</link>
		<comments>http://www.adamfranco.com/2010/06/14/adding-reverse-proxy-caching-to-php-applications/#comments</comments>
		<pubDate>Mon, 14 Jun 2010 16:03:57 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Work]]></category>
		<category><![CDATA[caching]]></category>
		<category><![CDATA[reverse-proxy]]></category>
		<category><![CDATA[Varnish]]></category>
		<category><![CDATA[web-development]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=426</guid>
		<description><![CDATA[Note: This is a cross-post of documentation I am writing about Lazy Sessions. Why use reverse-proxy caching? For most public-facing web applications, the significant majority of their traffic is anonymous, non-authenticated users. Even with a variety of internal data-cache mechanisms and other good optimizations, a large amount of code execution goes into executing a PHP [...]]]></description>
			<content:encoded><![CDATA[<p><em>Note: This is a cross-post of <a href="http://wiki.github.com/adamfranco/lazy_sessions/adding-reverse-proxy-caching-to-php-applications">documentation I am writing about Lazy Sessions</a>.</em></p>
<h1>Why use reverse-proxy caching?</h1>
<p>For most public-facing web applications, the significant majority of their traffic is anonymous, non-authenticated users. Even with a variety of internal data-cache mechanisms and other good optimizations, a large amount of code execution goes into executing a <span class="caps">PHP</span> application to generate a page even if the content of this page will be the same for many users. Code and query optimization are very important to improving the experience for all users of a web application, but even the most basic &ldquo;Hello World&rdquo; script will top out at about 3k requests/second due to the overhead of Apache and <span class="caps">PHP</span> &mdash; many real applications top out at less than 200 requests/second. Varnish, a light-weight proxy-server that can run on the same host as the webserver, can cache pages in memory and can serve them at rates of more than 10k requests/second with thousands of concurrent connections.</p>
<p>While the point of web-applications is to have content be dynamic and easily changeable, for most applications and most of the anonymous users, receiving content that is slightly stale (cached for 5 minutes or something similar) isn&rsquo;t a big deal. Sure, visitors to your blog might not see the latest post for a few minutes, but they will get their response in 4 milliseconds rather than 2 seconds.</p>
<p>Should your site get posted on Slashdot, a caching reverse-proxy server will give anonymous visitor #2 and up the same page from cache (until expiration), while authenticated users continue to have their requests passed through to the Apache/<span class="caps">PHP</span> back-end. Everyone wins.</p>
<p><span id="more-426"></span></p>
<h1>Caveats</h1>
<p>Before we get into how to set this up, you should be aware of a few caveats (in addition to increased complexity) that come with this scheme.</p>
<h2>1. Stale Content</h2>
<p>Ideally, pages would always be served from the cache for as long as they don&rsquo;t change, then the application would expire pages when they are changed on the back-end. Varnish has an <span class="caps">API</span> that supports this behavior and <a href="http://drupal.org/project/Varnish">Drupal Varnish module</a> is being developed to do this dynamic cache-clearing for Drupal sites, but overall, dynamic cache clearing is much more difficult to set up than time-based cache expiration.</p>
<p>When using time-based cache expiration, the challenge is to balance the needs for content freshness (shorter cache lifetimes) against the efficiency of cache hits (longer cache lifetimes will result in more clients using the cached versions). For content that doesn&rsquo;t need to be up-to-the-minute fresh, a cache lifetime of around 5 minutes might be a good starting point. If the content only changes daily at certain time, a fixed expiration time (shortly after the data sync) might be appropriate.</p>
<h2>2. Cookie Use</h2>
<p>If your application only uses a cookies set by PHP&rsquo;s <code>session_start()</code> function, then <code>lazy_sessions.php</code> should work transparently without modification of either that include file or your application (other than including the file). If your application sets other cookies then these will cause the reverse-proxy not to cache them unless you specifically exclude them in the reverse-proxy server&rsquo;s configuration.</p>
<h2>3. Data Caching in the <code>$_SESSION</code></h2>
<p>If you use the <code>$_SESSION</code> array as a data cache on anonymous requests, then these anonymous requests will be given a session cookie and their requests won&rsquo;t be served from the reverse-proxy&rsquo;s cache. Rather than using the <code>$_SESSION</code> array for non-user-specific data, cache such data with <span class="caps">APC</span> or memcached. This also has the advantage of such non-user-specific data not having to be rebuilt for every new client.</p>
<h2>4. <code>flush()</code> and output buffering</h2>
<p>The default <span class="caps">PHP</span> session handling mechanism adds the session cookie to the response headers right when <code>session_start()</code> is called and writes the data off to the file-system after the script exits and the data has been sent. This default behavior ensures that users will always get a session cookie and saves the session data as the final processing step after all class destructors have been called.</p>
<p>Since we don&rsquo;t want to always set a session cookie, we need to remove the <code>Set-Cookie</code> header before headers are sent to the client. Output buffering with <code>ob_start()</code> will ensure that we have a chance to decide to clear the <code>Set-Cookie</code> header at script shutdown.</p>
<p>In some cases (such as incrementally sending large binary files) we want to send the content body (and therefor also the headers) before the script exits using the <code>flush()</code> function. To ensure that the session cookie is properly removed <code>session_write_close()</code> must be called before <code>flush()</code> or any other code that causes headers to be sent.</p>
<h1>Implementation</h1>
<p>Implementing reverse-proxy caching has three steps: <span class="caps">PHP</span> changes to enable lazy sessions, <span class="caps">PHP</span> changes to set cache-controlling headers, and finally the reverse-proxy server setup. For this example I&rsquo;ll use the Varnish reverse-proxy server, but others could be used instead.</p>
<h2>1. <span class="caps">PHP</span>: Lazy Sessions</h2>
<p>The first thing that needs to happen to make anonymous requests cache-able in an application that uses sessions is to ensure that sessions are only started when there is session data to be stored. By default, PHP&rsquo;s session handling mechanisms add a session cookie to the response header and store a session data file on the server on page-load that calls <code>session_start()</code>. While this behavior makes it easy to write applications that use sessions, it effectively means that there is no way to differentiate between responses that are for a particular user and those that could be for many users.</p>
<p>Including the <a href="http://github.com/adamfranco/lazy_sessions/blob/master/lazy_sessions.php"><code>lazy_sessions.php</code> file</a> before <code>session_start()</code> is called will override the default session-handling mechanism with one that checks to see if there is any data in the <code>$_SESSION</code> array before sending the user a <code>Set-Cookie</code> header and storing a session file:</p>
<pre>
<code>&lt;?php

// Include files or other pre-session_start code

require_once('lazy_sessions/lazy_sessions.php');
start_session();

// The rest of the application code.
?&gt;
</code>
</pre>
<p>If your application needs to flush content and thereby send headers before script shutdown (such as incrementally sending file data), call <code>session_write_close()</code> if <code>session_start()</code> has been called for that script:</p>
<pre>
<code>&lt;?php

// Include files or other pre-session_start code

require_once('lazy_sessions/lazy_sessions.php');
start_session();

// other application code.

// If session_write_close() is not called before flushing, then the Set-Cookie
// header will be sent before our custom session handler has a chance to determine
// if a session is even needed.
session_write_close();

print "Hello";
flush();
print " World.";
flush();

?&gt;
</code>
</pre>
<h2>2. <span class="caps">PHP</span>: Cache-Control headers</h2>
<p>Now that we have our cookies straightened out, we need to ensure that our <span class="caps">PHP</span> scripts respond with <span class="caps">HTTP</span> headers that indicate that downstream clients such as our reverse-proxy and the user&rsquo;s browser are allowed to cache anonymous pages. There are a number of different <a href="http://wiki.github.com/adamfranco/lazy_sessions/cache-controlling-headers">Cache-Controlling Headers</a> that may affect whether a particular cache may store a given response. By default, <span class="caps">PHP</span> sets all of these headers to indicate that no caches may store any pages, ensuring that they are dynamic.</p>
<pre>
<code>&lt;?php

// If the session data is empty, then we could assume that there is no per-user data
// and that the response can be cached.
if (!count($_SESSION)) {

// Alternatively, we could check an application-specific value (such as a user-id)
// to determine if the response is for a particular user.
// if (!isset($_SESSION['user_id'])) {

// Cache for 5 minutes
$maxAge = 300;

header('Expires: '.gmdate('D, d M Y H:i:s', time() + $maxAge).' GMT', true);
header('Cache-Control: public, max-age='.$maxAge, true);
header('Pragma: ', true);
}

header('Vary: Cookie,Accept-Encoding', true);
</code>
</pre>
<p>The two most important headers with regard to caching with varnish are the following:</p>
<h3>The <code>Cache-Control</code> header.</h3>
<p>The <code>Cache-Control: public, max-age=300</code> header indicates to any clients (such as the Varnish caching proxy) that this response can be cached in public caches valid for many downstream clients. The <code>max-age</code> portion of the header indicates that the cache may store this response for 300 seconds.</p>
<p>As I understand it (possibly wrong) Varnish only looks at the <code>max-age</code> portion of the <code>Cache-Control</code> header when determining how long to store a response. Apparently it ignores the <code>Expires</code> header for its cache-expiration purposes, though this header is passed on to downstream clients.</p>
<h3>The <code>Vary</code> header</h3>
<p>The <code>Vary: Cookie,Accept-Encoding</code> header tells Varnish (and in-browser caches) that they should not respond with the cached version of a response if the request includes a cookie or a different cookie from the request that previously had its response cached. Similarly, if one client says that it accepts gzip encoding via an <code>Accept-Encoding: gzip</code> request header, then the cached response may be compressed with gzip and should not be sent in response to requests from clients that do not state that they accept gzip encoding.</p>
<p>While Varnish&rsquo;s behavior is to never cache or respond from cache when cookies are present, without the <code>Vary: Cookie</code> response header, browsers or other downstream caches may respond with a cached response valid for only anonymous users even though a cookie is now present.</p>
<p>See my notes on <a href="http://wiki.github.com/adamfranco/lazy_sessions/cache-controlling-headers">Cache-Controlling Headers</a> for more details about other headers and how they affect the Varnish cache and in-browser caches.</p>
<h2>3. Varnish (Reverse-Proxy) Configuration</h2>
<p>The <code>/etc/varnish/default.vcl</code> config file controls how Varnish responds to requests and responses, in particular whether or not it should cache or not. Below is the contents of my <code>default.vcl</code> file.</p>
<div>
<strong>Notes:</strong></p>
<ol>
<li>The backend portion is the default, you probably will want to modify this to point at your correct backend hosts and ports.</li>
<li>The <code>vcl_recv</code> and <code>vcl_hash</code> sections come directly from the <a href="https://wiki.fourkitchens.com/display/PF/Configure+Varnish+for+Pressflow?focusedCommentId=15335604">Pressflow wiki</a> and are set up to allow requests that include Google Analytics cookies to be cached while not caching requests that include other cookies.</li>
<li>The <code>vcl_fetch</code> section is the default with my addition of the lines to unset empty Set-Cookie headers that can&rsquo;t be removed from within <span class="caps">PHP</span> &lt; 5.3.</li>
</ol>
</div>
<pre>
<code>
backend default {
.host = "127.0.0.1";
.port = "80";
}

sub vcl_recv {
// Remove has_js and Google Analytics __* cookies.
set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(__[a-z]+|has_js)=[^;]*", "");
// Remove a ";" prefix, if present.
set req.http.Cookie = regsub(req.http.Cookie, "^;\s*", "");
// Remove empty cookies.
if (req.http.Cookie ~ "^\s*$") {
unset req.http.Cookie;
}

// Cache all requests by default, overriding the
// standard Varnish behavior.
// if (req.request == "GET" || req.request == "HEAD") {
//   return (lookup);
// }
}

sub vcl_hash {
if (req.http.Cookie) {
set req.hash += req.http.Cookie;
}
}

sub vcl_fetch {
if (!beresp.cacheable) {
	return (pass);
}

// If using PHP &lt; 5.3 there is no way to fully delete headers, so empty
// Set-Cookie headers may be in the response. Ignore these empty headers.
if (beresp.http.Set-Cookie ~ "^\s*$") {
	unset beresp.http.Set-Cookie;
}

if (beresp.http.Set-Cookie) {
	return (pass);
}
return (deliver);
}
</code>
</pre>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/06/14/adding-reverse-proxy-caching-to-php-applications/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Sofa Table Complete</title>
		<link>http://www.adamfranco.com/2010/04/11/sofa-table-complete/</link>
		<comments>http://www.adamfranco.com/2010/04/11/sofa-table-complete/#comments</comments>
		<pubDate>Mon, 12 Apr 2010 00:55:09 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Projects]]></category>
		<category><![CDATA[Wood Working]]></category>
		<category><![CDATA[table]]></category>
		<category><![CDATA[woodworking]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=419</guid>
		<description><![CDATA[Over the course of the past year I built this cherry sofa table based on a design by Scott Gibson in Fine Woodworking&#8217;s &#34;Furniture&#34; book. All frame joinery is mortise and tenon, while the drawers use doweled rabbit joints. The finish is boiled linseed oil topped with 3 coats of Minwax wiping varnish. Building this [...]]]></description>
			<content:encoded><![CDATA[<p><object width="600" height="450"><param name="flashvars" value="offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157623710369899%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157623710369899%2F&#038;set_id=72157623710369899&#038;jump_to="></param><param name="movie" value="http://www.flickr.com/apps/slideshow/show.swf?v=71649"></param><param name="allowFullScreen" value="true"></param><embed type="application/x-shockwave-flash" src="http://www.flickr.com/apps/slideshow/show.swf?v=71649" allowFullScreen="true" flashvars="offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157623710369899%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157623710369899%2F&#038;set_id=72157623710369899&#038;jump_to=" width="600" height="450"></embed></object></p>
<p>Over the course of the past year I built this cherry sofa table based on a design by Scott Gibson in Fine Woodworking&#8217;s &quot;Furniture&quot; book.</p>
<p>All frame joinery is mortise and tenon, while the drawers use doweled rabbit joints. The finish is boiled linseed oil topped with 3 coats of Minwax wiping varnish.</p>
<p>Building this table was quite a learning experience as just about every part required techniques that I hadn&#8217;t used before. Mortise and tenon joinery, biscuits to align the table top during glue-up, doweled joints fort the drawers, quartersawn veneers for the legs, breadboard-ends, and varnish; all of these were new to me and required a bit of trial and error to get right.</p>
<p>This project certainly had its share of &quot;oops&quot; moments, but nothing that couldn&#8217;t be repaired or worked-around. I cut the bottom shelf stretcher one inch too short, but was able to cut it in half and splice in a section with a small mortise and tenon in the middle. Later, I made the hipped-tenons on which the breadboard-ends of the top sit too thin. This was repaired with the addition of some 5-minute epoxy to thicken the tenon.</p>
<p>All that remains now is to choose and install drawer-pull hardware.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/04/11/sofa-table-complete/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Importing users into Bugzilla</title>
		<link>http://www.adamfranco.com/2010/03/08/importing-users-into-bugzilla/</link>
		<comments>http://www.adamfranco.com/2010/03/08/importing-users-into-bugzilla/#comments</comments>
		<pubDate>Tue, 09 Mar 2010 04:11:45 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[Work]]></category>
		<category><![CDATA[Bugzilla]]></category>
		<category><![CDATA[import]]></category>
		<category><![CDATA[LDAP]]></category>
		<category><![CDATA[Perl]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=374</guid>
		<description><![CDATA[For the past 6 months our Web Application Development work-group has been Bugzilla as our issue tracker with quite a bit of success. While it has its warts, Bugzilla seems like a pretty decent issue-tracking system and is flexible enough to fit into a variety of different work-flows. One very important feature of Bugzilla is [...]]]></description>
			<content:encoded><![CDATA[<p>For the past 6 months our <a href="http://go.middlebury.edu/webservices">Web Application Development work-group</a> has been Bugzilla as our issue tracker with quite a bit of success. While it has its warts, Bugzilla seems like a pretty decent issue-tracking system and is flexible enough to fit into a variety of different work-flows. One very important feature of Bugzilla is support for LDAP authentication. This enables any Middlebury College user to log in and report a bug using their standard campus credentials.</p>
<p>While LDAP authentication works great, there is one problem: If a person has never logged into our Bugzilla, we can&#8217;t add them to the CC list of an issue. This is important for us because issues usually don&#8217;t get submitted directly to the bug tracker, but rather come in via calls, emails, tweets, and face-to-face meetings. We are then left to submit issues to Bugzilla ourselves to keep track of our to-do items. Ideally we&#8217;d add the original reporter to the bug&#8217;s CC list so that they will automatically be notified as we make progress on the issue, but their Bugzilla account must exist before we can add them to the bug.</p>
<p>Searching about the internet I wasn&#8217;t able to find anything about how to import LDAP users (or any kind of users) into Bugzilla, though I was able to find some <a href="http://groups.google.com/group/mozilla.support.bugzilla/browse_thread/thread/165d4fc1a8b4ad82/b1e31ad20bfef3f0">basic instructions</a> on how to create a single user via Bugzilla&#8217;s Perl API. To improve on the lack of user-import support I&#8217;ve created an Perl script that creates users from lines in a tab-delimited text file (<code>create_users.pl</code>) as well as a companion PHP script that will export an appropriately-formatted list of users from an Active Directory (LDAP) server (<code>export_users.php</code>).</p>
<p><span id="more-374"></span><br />
<a href='http://tmp.adamfranco.com/files/2010/03/BugzillaImport.zip'>BugzillaImport.zip</a> &#8212; Unzip in your Bugzilla directory, run via the command line. See below for examples.</p>
<h1>File Listings:</h1>
<h2>create_users.pl</h2>
<p>This script can safely be run repeatedly. Only new users not already in Bugzilla will be added, users matching existing email addresses will be skipped.</p>
<pre>#!/usr/bin/env perl
##########################################################
# This is a basic script to import users into Bugzilla.
#
# Users can be imported from tab-delimited text files or
# tab-delimited lines piped to STDIN. Lines should have 3
# columns: login	email	name
#
#
# Author:
#	Adam Franco (afranco@middlebury.edu)
# Date:
#	2010-03-08
# URL:
#	http://www.adamfranco.com/archives/374
# License:
#   The contents of this file are subject to the Mozilla Public
#   License Version 1.1 (the "License"); you may not use this file
#   except in compliance with the License. You may obtain a copy of
#   the License at http://www.mozilla.org/MPL/
#
#   Software distributed under the License is distributed on an "AS
#   IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
#   implied. See the License for the specific language governing
#   rights and limitations under the License.
##########################################################

use FindBin qw($Bin);
BEGIN {
    push @INC,$Bin;
    push @INC,$Bin."/lib";
    push @INC,$Bin."/lib/x86_64-linux-thread-multi";
}
use Bugzilla;
use Bugzilla::User;
use Error qw(:try);

sub usage {
    print "
Usage:
    $0 ListOfUsers1.txt [ListOfUsers2.txt [...]]
    $0 < ListOfUsers.txt

The ListOfUsers can be passed as either a file argument or passed to STDIN.

The ListOfUsers must be tab-delimited with the following columns:
login   email   name

";
    exit 1;
}

foreach (@ARGV) {
    if ($_ =~ /^-h|--help$/) {
        usage();
    }
}

my $lines = 0;
my $users = 0;
my $usersAdded = 0;
while (<>) {
    chomp; # Remove the trailing new-line.
    my($login, $email, $name) = split(/\t/, $_);

    if ($login &#038;&#038; $email &#038;&#038; $name &#038;&#038; $login =~ /[a-z0-9]+/ &#038;&#038;  $email =~ /[a-z0-9]+.*@.*[a-z0-9]+/ &#038;&#038; $name =~ /[a-z]+/) {
        if (is_available_username($email)) {
            try {
                my $user = Bugzilla::User->create({
                    login_name    => $email,
                    realname      => $name,
                    cryptpassword => '*',
                    disable_mail  => 0,
                    extern_id     => $login
                });
                print "Account for " . $user->login . " was created.\n";
                $usersAdded++;
            } catch Error with {
                my $ex = shift;
                my $error = "Error: $ex";
                $error =~ s/\n|\r/ /g;
                print $error."\n";
            };
        }

        $users++;
    }
    $lines++;
    close (ARGV) if (eof);
}

if (!$lines) {
    print "No input lines given.\n\n";
    usage();
}

print "\n$lines lines evaluated, $users user records checked, $usersAdded users added.\n";

exit 0;
</pre>
<h2>export_users.php</h2>
<pre>#!/usr/bin/env php
&lt;?php
##########################################################
# This is a basic script to export users from an
# MS Active Directory via LDAP in the format required
# by create_users.pl.
#
# Authors:
#	Adam Franco (afranco@middlebury.edu)
#	Ian McBride (imcbride@middlebury.edu)
# Date:
#	2010-03-08
# URL:
#	http://www.adamfranco.com/archives/374
# License:
#   The contents of this file are subject to the Mozilla Public
#   License Version 1.1 (the "License"); you may not use this file
#   except in compliance with the License. You may obtain a copy of
#   the License at http://www.mozilla.org/MPL/
#
#   Software distributed under the License is distributed on an "AS
#   IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
#   implied. See the License for the specific language governing
#   rights and limitations under the License.
##########################################################

$ldaphost = "ldap.example.com";
$ldapport = 389;
$ldapuser = "username";
$ldappass = "password";
$baseDN = "DC=example,DC=com";

$connection = ldap_connect($ldaphost, $ldapport);

if (!$connection) die();

if (ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION,3) === FALSE) die();

if (ldap_set_option($connection, LDAP_OPT_REFERRALS,0) === FALSE) die();

$bind = ldap_bind($connection, $ldapuser, $ldappass);

if (!$bind) die();

$filter = "(&#038;(objectClass=User)(!(objectClass=Computer)))";

$search = ldap_search($connection, $baseDN, $filter, array("samaccountname", "mail", "givenname", "sn"));

$entries = ldap_get_entries($connection, $search);

print "samaccountname\temail\tname\n";

foreach($entries as $entry) {
  if(isset($entry['samaccountname'])) {
    print iconv('UTF-8', 'UTF-8//IGNORE', $entry['samaccountname'][0]);
  }
  print "\t";

  if(isset($entry['mail'])) {
    print iconv('UTF-8', 'UTF-8//IGNORE', $entry['mail'][0]);
  }
  print "\t";

  $name = '';
  if(isset($entry['givenname'])) {
    $name .= iconv('UTF-8', 'UTF-8//IGNORE', $entry['givenname'][0]);
  }  $name .= ' ';
  if(isset($entry['sn'])) {
    $name .= iconv('UTF-8', 'UTF-8//IGNORE', $entry['sn'][0]);
  }
  print trim($name);

  print "\n";
}
</pre>
<h1>Example Usage</h1>
<p>After unzipping the scripts in your Bugzilla directory you can use the <code>create_users.pl</code> script right away. To use <code>export_users.php</code> you will need to edit it and add your LDAP server configuration.<br />
<code>[root@hostname /var/www/htdocs/bugzilla/]# ./export_users.php | ./create_users.pl</code></p>
<p>If you&#8217;d rather import users from another source, simply create one or more tab-delimited text files that have the following columns:<br />
login&nbsp;&nbsp;&nbsp;&nbsp;email&nbsp;&nbsp;&nbsp;&nbsp;name<br />
<code>[root@hostname /var/www/htdocs/bugzilla/]# ./create_users.pl users.txt otherusers.txt</code></p>
<p>You can pipe tab-delimited data to the script as well:<br />
<code>[root@hostname /var/www/htdocs/bugzilla/]# head -n 20 users.txt | ./create_users.pl</code></p>
<h2>Update:</h2>
<ul>
<li>Changed the license statement to the MPL be compatible with the rest of Bugzilla</li>
<li>Changed the password to &#8216;*&#8217; based on Max&#8217;s suggestion</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/03/08/importing-users-into-bugzilla/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Project 365</title>
		<link>http://www.adamfranco.com/2010/01/01/project-365/</link>
		<comments>http://www.adamfranco.com/2010/01/01/project-365/#comments</comments>
		<pubDate>Sat, 02 Jan 2010 03:47:23 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Photography]]></category>
		<category><![CDATA[project 365]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=366</guid>
		<description><![CDATA[Sarah announced today that she was going to do a &#8220;365 project&#8221; this year: taking a photo every day of the year, both as a journal and to force one&#8217;s self to get out and take some pictures. This sounded like a fun idea and one that would be easier to stick to if we [...]]]></description>
			<content:encoded><![CDATA[<p>Sarah announced today that she was going to do a &#8220;365 project&#8221; this year: taking a photo every day of the year, both as a journal and to force one&#8217;s self to get out and take some pictures. This sounded like a fun idea and one that would be easier to stick to if we were both doing it, so I&#8217;m going to give it a whirl as well. You can follow along with <a href="http://api.flickr.com/services/feeds/photoset.gne?set=72157622991414327&#038;nsid=60594606@N00&#038;lang=en-us">this feed</a> or check the <a href="http://www.flickr.com/photos/adamfranco/sets/72157622991414327/">photo-set</a> for updates.</p>
<p><object width="500" height="375"><param name="flashvars" value="offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157622991414327%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157622991414327%2F&#038;set_id=72157622991414327&#038;jump_to="></param><param name="movie" value="http://www.flickr.com/apps/slideshow/show.swf?v=71649"></param><param name="allowFullScreen" value="true"></param><embed type="application/x-shockwave-flash" src="http://www.flickr.com/apps/slideshow/show.swf?v=71649" allowFullScreen="true" flashvars="offsite=true&#038;lang=en-us&#038;page_show_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157622991414327%2Fshow%2F&#038;page_show_back_url=%2Fphotos%2Fadamfranco%2Fsets%2F72157622991414327%2F&#038;set_id=72157622991414327&#038;jump_to=" width="500" height="375"></embed></object></p>
<p><a href="http://api.flickr.com/services/feeds/photoset.gne?set=72157622991414327&amp;nsid=60594606@N00&amp;lang=en-us"><img src="http://l.yimg.com/g/images/feed-icon-16x16.png" alt="Subscribe to a feed of 365 - 2010" class="absmiddle" width="16" height="16"/> Feed</a> – Subscribe to the set &#8220;Project 365 &#8211; 2010&#8243;</p>
<p>See <a href="http://www.flickr.com/photos/sarahmcgowen/sets/72157623117929920/">Sarah&#8217;s &#8220;Project 365&#8243;</a> on Flickr.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/01/01/project-365/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Slow Cooking</title>
		<link>http://www.adamfranco.com/2010/01/01/slow-cooking/</link>
		<comments>http://www.adamfranco.com/2010/01/01/slow-cooking/#comments</comments>
		<pubDate>Fri, 01 Jan 2010 21:34:47 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Food]]></category>
		<category><![CDATA[Video]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=363</guid>
		<description><![CDATA[Sarah&#8217;s big Christmas present this year was a Cuisinart slow-cooker. We tried it out a few days ago to make &#8220;Curried Cream of Chicken Soup&#8221; from a recipe in The Silver Palate Cookbook The cooker worked great and the chicken melted off of the bone.]]></description>
			<content:encoded><![CDATA[<p>Sarah&#8217;s big Christmas present this year was a Cuisinart slow-cooker. We tried it out a few days ago to make &#8220;Curried Cream of Chicken Soup&#8221; from a recipe in <em>The Silver Palate Cookbook</em></p>
<p><object width="500" height="340"><param name="movie" value="http://www.youtube.com/v/a9dw9rGbQk0&#038;hl=en_US&#038;fs=1&#038;rel=0"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/a9dw9rGbQk0&#038;hl=en_US&#038;fs=1&#038;rel=0" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="500" height="340"></embed></object></p>
<p>The cooker worked great and the chicken melted off of the bone.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2010/01/01/slow-cooking/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Bicycle commuting update</title>
		<link>http://www.adamfranco.com/2009/12/13/bicycle-commuting-update/</link>
		<comments>http://www.adamfranco.com/2009/12/13/bicycle-commuting-update/#comments</comments>
		<pubDate>Mon, 14 Dec 2009 04:46:23 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Musings]]></category>
		<category><![CDATA[Outdoors]]></category>
		<category><![CDATA[bicycle commuting]]></category>
		<category><![CDATA[commuting]]></category>
		<category><![CDATA[Cycling]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=349</guid>
		<description><![CDATA[It is now solidly mid-December and I&#8217;m still doing my 3-mile (each way) commute by bicycle. I started biking to work for this season around the beginning of April and purchased a dedicated commuting bike on April 21st. Since then I&#8217;ve logged 770 miles commuting just about every day; rain, snow, or shine. The commuter [...]]]></description>
			<content:encoded><![CDATA[<p>It is now solidly mid-December and I&#8217;m still doing my 3-mile (each way) commute by bicycle. I started biking to work for this season around the beginning of April and <a href="http://www.adamfranco.com/archives/119">purchased a dedicated commuting bike on April 21st</a>. Since then I&#8217;ve <a href="http://spreadsheets.google.com/pub?key=piXp7VTzR6qOZUsOiML9VGg&#038;single=true&#038;gid=0&#038;output=html">logged 770 miles</a> commuting just about every day; rain, snow, or shine.</p>
<p><a href="http://www.flickr.com/photos/adamfranco/4182906871/" title="Commuter Bike by Adam Franco, on Flickr"><img src="http://farm3.static.flickr.com/2673/4182906871_ce99d03175.jpg" width="500" height="332" alt="Commuter Bike" /></a></p>
<p>The commuter bike, a <a href="http://www.adamfranco.com/archives/119">Giant &#8220;Tran Send&#8221;</a>, has received some accoutrements over the course of the year: storage, improved lights, and winter rubber.</p>
<p><span id="more-349"></span><br />
<a href="http://www.flickr.com/photos/adamfranco/4182908355/" title="Commuter Bike by Adam Franco, on Flickr"><img src="http://farm3.static.flickr.com/2783/4182908355_dd057017bb.jpg" width="332" height="500" alt="Commuter Bike" /></a></p>
<p>The panniers I had purchased in April ended up being returned since they were too small to fit my laptop and gi. In their stead was a plastic crate lashed to the rack with zip-ties. The crate wobbled a bit when piled high, but sufficed for most of the summer until I had the chance to augment it with some old panniers that had been stored in my parents garage since a Boyscout bike-hike many years ago. The old panniers didn&#8217;t fit the laptop either, so to fit both them and the crate I fashioned a wooden riser-platform to allow the crate to sit a few inches off the rack. While difficult to attach, this arrangement has the benefit of enhancing that stability of the crate as well as providing as much storage as I care to fill.</p>
<p><a href="http://www.flickr.com/photos/adamfranco/4183668972/" title="Crate-platform detail. by Adam Franco, on Flickr"><img src="http://farm5.static.flickr.com/4009/4183668972_0a1c7eaaf4.jpg" width="500" height="332" alt="Crate-platform detail." /></a></p>
<p>The second upgrade was the addition of several more blinking tail lights and the <a href="http://www.adamfranco.com/archives/231">CygoLight Mity-Cross headlight</a> that I had purchased for the <a href="http://www.adamfranco.com/archives/240">24 Hours of Great Glen mountain bike race</a>. A reflective vest also joins the lights for what I&#8217;m hoping will provoke a reaction along the lines of, &#8220;Is that a Christmas tree ahead?&#8221;.</p>
<p><a href="http://www.flickr.com/photos/adamfranco/4183670514/" title="Commuter Bike by Adam Franco, on Flickr"><img src="http://farm3.static.flickr.com/2610/4183670514_dc41bdb783.jpg" width="332" height="500" alt="Commuter Bike" /></a></p>
<p>Last week brought the latest update, studded snow tires. I purchased the Schwalbe &#8220;Marathon Winter&#8221; tires from <a href="http://www.peterwhitecycles.com/studdedtires.asp">Peter White Cycles in New Hampshire</a> (mail order), with 240 tungsten-carbide studs in each tire. At this point they&#8217;ve only seen a bit of snow and ice, but they remove the hesitation I built up after a tense, snowy commute with my slick summer tires.</p>
<p><a href="http://www.flickr.com/photos/adamfranco/4183671114/" title="Studded Snow Tires by Adam Franco, on Flickr"><img src="http://farm5.static.flickr.com/4006/4183671114_db0b5bd858_b.jpg" width="679" height="1024" alt="Studded Snow Tires" /></a></p>
<p>Now that winter seems to have started for real, my plan is to bike as much as I feel comfortable with and car-pool with Sarah or take the bus if the weather is bad. I&#8217;d like to push myself a bit to continue through the winter, but would rather not take any risks of being hit by drivers who might be sliding around the highway in a storm.</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2009/12/13/bicycle-commuting-update/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>The future of phones: Google Voice, Skype, mobile, and more</title>
		<link>http://www.adamfranco.com/2009/11/08/the-future-of-phones-google-voice-skype-mobile-and-more/</link>
		<comments>http://www.adamfranco.com/2009/11/08/the-future-of-phones-google-voice-skype-mobile-and-more/#comments</comments>
		<pubDate>Sun, 08 Nov 2009 05:21:04 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[google]]></category>
		<category><![CDATA[google-voice]]></category>
		<category><![CDATA[phones]]></category>
		<category><![CDATA[telephony]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=319</guid>
		<description><![CDATA[As members of the under-30 club, my wife Sarah and I have come into adulthood in the age of mobile phones. I got my first cell phone right after college and Sarah has had hers since she was 14; neither of us has ever had a land-line of our own. While the mobile-only lifestyle has [...]]]></description>
			<content:encoded><![CDATA[<p>As members of the under-30 club, my wife Sarah and I have come into adulthood in the age of mobile phones. I got my first cell phone right after college and Sarah has had hers since she was 14; neither of us has ever had a land-line of our own.</p>
<p>While the mobile-only lifestyle has generally worked great for us over the years, it does have downsides that have become more apparent as our lifestyles have shifted to a more settled routine. Currently Sarah and I find ourselves generally splitting our time between work and home. At work we each have an office phone supplied, but we had only had our mobile phones at home. An unfortunately common occurrence was for one of us to come home and leave the mobile on silent/vibrate in a coat pocket and become unreachable. After a few incidents of being stranded, stood up, or not getting the message to pick up milk we decided that a home phone was needed &#8212; but were shocked to find that a local-only land-line would run us $40 per month (about the same as a cell phone plan in this area).</p>
<p>We were in search of a solution that would allow us to have a phone ringing audibly at home, keep our mobile phones for mobile usage, and come in at less than $120/month (if not lower our bills). Our solution is shown in the diagram below. While it looks a bit complicated, it meets our goals, didn&#8217;t require any tricky setup, and comes in at a grand total of $50/month for maintaining two mobile phones and a home phone. It has the added benefits of a single number to reach each of us and Google&#8217;s snazzy transcribed-voice-mail service.</p>
<div id="attachment_320" class="wp-caption aligncenter" style="width: 510px"><a href="http://tmp.adamfranco.com/files/2009/11/Phone-System-1.png"><img src="http://tmp.adamfranco.com/files/2009/11/Phone-System-1.png" alt="New phone system with Google Voice  (click to enlarge)" title="Phone System 1" width="500" class="size-full wp-image-320" /></a><p class="wp-caption-text">New phone system with Google Voice (click to enlarge)</p></div>
<h2>The new home phone: Skype + a handset</h2>
<p>The first piece of the puzzle was to purchase a handset (the <a href="http://www.amazon.com/gp/product/B002V45UEE">IPEVO SO-20</a>) that sits at home on our wireless network, signed in to my <a href="http://skype.com/">Skype</a> account. This handset works just like the Skype-application on a desktop computer, but doesn&#8217;t require keeping a large computer on to make or receive Skype calls. In addition to making free Skype-to-Skype calls, Skype also offers services for making calls from your Skype client to normal telephone numbers (known as &#8220;<a href="http://www.skype.com/intl/en/allfeatures/callphones/">Skype-Out</a>&#8220;) as well as a service which provides you with a telephone number that will ring your Skype client (known as &#8220;<a href="http://www.skype.com/intl/en/allfeatures/onlinenumber/">Skype In</a>&#8220;). Skype-Out charges a minimal 2-cents/minute for calls to most of the world and maintaining the Skype-In number costs $3/month with no charge for talk-time.</p>
<p>We&#8217;ve been using the Skype phone for a few months now and have been very pleased with it. We notice a 1-1.5 second delay in hearing the caller when we first answer a call. This was a little confusing at first and resulted in a lot of &#8220;Hello? Hello? Can you hear me?&#8221; back-and-forth with the caller, but the delay is only at connection time and saying &#8220;Hello?&#8221; and then just pausing for a moment gives the call time to connect fully. Once in a call, the audio quality is generally a bit better than my mobile phone.</p>
<h2>Routing calls with Google Voice</h2>
<p>With the Skype-phone in place we now had a number that would reliably ring at home and costs us less than $10/month for a few hours of incoming and outgoing calls to anywhere in the world. Now the question is: How do we get people to call us on the Skype-phone rather than our mobile phones? Enter (from stage left) Google Voice.</p>
<p><a href="http://www.google.com/googlevoice/about.html">Google Voice</a> (from here out referred to as &#8220;GV&#8221;) is at its heart a phone-number forwarding service. The basic idea is that you get a GV phone number and then in your account settings, configure it to forward incoming calls to one or more other phone numbers. When a call comes in, all of your phones ring at the same time (this can be quite shocking if you have them in close proximity) and you pick up whichever one is at hand (and doesn&#8217;t incur a usage fee if you want to avoid that). Once you&#8217;ve picked up one phone the others stop ringing and you talk away.</p>
<p>I have my GV set up to ring three phones, my mobile number, our Skype-In number, and my work number. Since I spend the majority of my time either at work or home, most of the time I pick up calls at one of those two places. This cuts my mobile phone usage to only a few days per week, opening up other options for cutting costs.</p>
<h2>Prepaid mobile + minimal usage = savings</h2>
<p>Another driver for this entire phone-system change was that Unicel&#8217;s network in Vermont was recently sold to AT&#038;T. After some bad customer-service experiences with Verizon I switched to Unicel in 2007 and was very happy with their service. In particular, they used unlocked GSM phones and didn&#8217;t charge for incoming calls or text messages, all for $35/month. With the sale to AT&#038;T I was looking at an increase to $40/month for the minimal plan plus airtime usage for incoming calls.</p>
<p>With the Skype-phone in place and GV forwarding calls to all numbers, our mobile-phone usage wasn&#8217;t as high, allowing us to try some other options. Rather than signing up for a new AT&#038;T contract, I instead kept the unlocked phone I used with Unicel and went with a prepaid (&#8220;<a href="http://www.wireless.att.com/cell-phone-service/go-phones/pyg-plans-phones.jsp">GoPhone</a>&#8220;) plan from AT&#038;T. Rather than paying a monthly fee, I pre-pay on my account and then only have my account balance debited when I use the phone. I&#8217;m currently using the version of the plan where I pay $1/day on days that I use the phone, plus 10-cents/minute. While this sounds like it would add up, with GV routing calls to my other numbers I&#8217;ve averaged $16/month in mobile charges for the past two months. Also, unlike the monthly phone contract this has the potential to get much lower as more friends and family learn of my GV number and stop calling my mobile directly.</p>
<h2>All said and done</h2>
<p>From a pure cost perspective this telephony setup has been a big success. From two cell phones at $40/month each for a total of $80/month (with additional for a home phone); we&#8217;ve now gone to $3/month for the Skype-In number with ~$3/month of Skype-out calls from home, plus about $16/month each in mobile phone charges leaves us with a new total of a bit under $40/month. We had the additional $140 up-front cost for the IPEVO Skype-phone, but amortized over a year that still leaves us at about $50/month, with the potential to drop costs further if our cell-phone usage drops.</p>
<p>The non-monetary benefits are certainly harder to quantify. The biggest benefit I find is the increased control over my phone environment. For example, I could swap out the Skype-phone for something else (or get rid of it entirely) and no callers would know the difference. Once my contacts are all using my GV number, the same is true of my mobile phone.</p>
<p>Other features of GV such as voicemail transcription, caller filtering, scheduling of times when each phone should ring, and free SMS sending are all pretty neat too, but I haven&#8217;t yet made heavy use of them.</p>
<p>Now for the downsides:</p>
<ul>
<li>Complexity: While I find the increased flexibility valuable and none of the steps are challenging, others may find the whole thing not worth the hassle to set up.</li>
<li>A new number: While I now have one number that will ring all of my phones, Google currently doesn&#8217;t support transferring existing numbers to their service. I&#8217;m now trying to wean friends and family off of the mobile number I&#8217;ve had for 7 years.</li>
<li>Apparently some have found that using GV causes delays or other audio degradation. I haven&#8217;t noticed this myself.</li>
<li>One more thing relying on Google. Since all of the phone companies hosted NSA <a href="http://en.wikipedia.org/wiki/NSA_warrantless_surveillance_controversy">warrentless-wire-tapping</a> computers in their data-centers, I&#8217;m not particularly worried about Google having my calling data as well. That said, I&#8217;m relying on them to stick around for my email, searching, RSS reading, spreadsheets, and now call-routing.</li>
<li>Users don&#8217;t see my GV number in the caller id. You can make calls with GV so that the person you are calling sees your GV number in the caller-id, but this requires either initiating the call from the GV website (your phone rings first), or dialing your GV number, then from there initiating the call. I find this to be too much hassle so I never bother</li>
</ul>
<p>One final note: If you are a friend, family, or colleague who I missed in my number-update-email, let me know and I&#8217;ll send you my new GV number. <img src='http://www.adamfranco.com/wp-includes/images/smilies/icon_smile.gif' alt=':-)' class='wp-smiley' /> </p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2009/11/08/the-future-of-phones-google-voice-skype-mobile-and-more/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Hinesburg Town Forest</title>
		<link>http://www.adamfranco.com/2009/09/13/hinesburg-town-forest/</link>
		<comments>http://www.adamfranco.com/2009/09/13/hinesburg-town-forest/#comments</comments>
		<pubDate>Sun, 13 Sep 2009 22:27:35 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Musings]]></category>
		<category><![CDATA[Outdoors]]></category>
		<category><![CDATA[Cycling]]></category>
		<category><![CDATA[Mountain Biking]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=306</guid>
		<description><![CDATA[Today Spencer and I had a great rainy day mountain bike ride in the Hinesburg Town Forest. The blue line is our GPS track, the purple are the trails (provided by LocalMotion.com). View Larger Map It was a bit damp and cloudy, but the trails weren&#8217;t too muddy the woods were beautiful. A fine time [...]]]></description>
			<content:encoded><![CDATA[<p>Today Spencer and I had a great rainy day mountain bike ride in the Hinesburg Town Forest.</p>
<p>The blue line is our GPS track, the purple are the trails (provided by <a href="http://www.localmotion.org/trails/trail.php?trail=30">LocalMotion.com</a>).</p>
<p><iframe width="425" height="350" frameborder="0" scrolling="no" marginheight="0" marginwidth="0" src="http://maps.google.com/maps?f=q&amp;hl=en&amp;ie=UTF8&amp;om=1&amp;t=h&amp;q=http:%2F%2Fwww2.adamfranco.com%2Fkml_joiner.php%3F%26title%3DHinesburg%2BTown%2BForest%26description%3DA%2Blovely%2Bride%2BSpencer%2Band%2BI%2Btook%2Bin%2Bthe%2BHinesburg%2BTown%2BForest.%250D%250A%250D%250A%26urls%255B%255D%3Dhttp%253A%252F%252Fwww.adamfranco.com%252Fwp-content%252Fuploads%252F2009%252F09%252FHinesburg_Town_Forest_trk.kml%26titles%255B%255D%3DTrails%2B%2528from%2BLocalMotion.com%2529%26refresh%255B%255D%3D%26urls%255B%255D%3Dhttp%253A%252F%252Fwww.adamfranco.com%252Fwp-content%252Fuploads%252F2009%252F09%252FHinesburg_Town_Forest_wpt.kml%26titles%255B%255D%3DWaypoints%2B%2528from%2BLocalMotion.com%2529%26refresh%255B%255D%3D%26urls%255B%255D%3Dhttp%253A%252F%252Fwww.adamfranco.com%252Fwp-content%252Fuploads%252F2009%252F09%252FHinesburgTownForestRideTrack.kml%26titles%255B%255D%3DGPS%2Btrack%26refresh%255B%255D%3D&amp;ll=44.325199,-73.034534&amp;spn=0.021491,0.036478&amp;z=14&amp;output=embed"></iframe><br /><small><a href="http://maps.google.com/maps?f=q&amp;hl=en&amp;ie=UTF8&amp;om=1&amp;t=h&amp;q=http:%2F%2Fwww2.adamfranco.com%2Fkml_joiner.php%3F%26title%3DHinesburg%2BTown%2BForest%26description%3DA%2Blovely%2Bride%2BSpencer%2Band%2BI%2Btook%2Bin%2Bthe%2BHinesburg%2BTown%2BForest.%250D%250A%250D%250A%26urls%255B%255D%3Dhttp%253A%252F%252Fwww.adamfranco.com%252Fwp-content%252Fuploads%252F2009%252F09%252FHinesburg_Town_Forest_trk.kml%26titles%255B%255D%3DTrails%2B%2528from%2BLocalMotion.com%2529%26refresh%255B%255D%3D%26urls%255B%255D%3Dhttp%253A%252F%252Fwww.adamfranco.com%252Fwp-content%252Fuploads%252F2009%252F09%252FHinesburg_Town_Forest_wpt.kml%26titles%255B%255D%3DWaypoints%2B%2528from%2BLocalMotion.com%2529%26refresh%255B%255D%3D%26urls%255B%255D%3Dhttp%253A%252F%252Fwww.adamfranco.com%252Fwp-content%252Fuploads%252F2009%252F09%252FHinesburgTownForestRideTrack.kml%26titles%255B%255D%3DGPS%2Btrack%26refresh%255B%255D%3D&amp;ll=44.325199,-73.034534&amp;spn=0.021491,0.036478&amp;z=14&amp;source=embed" style="color:#0000FF;text-align:left">View Larger Map</a></small></p>
<p>It was a bit damp and cloudy, but the trails weren&#8217;t too muddy the woods were beautiful. A fine time was had by all (especially Hudson, the pooch).</p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2009/09/13/hinesburg-town-forest/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>High-availability Drupal &#8212; File-handling</title>
		<link>http://www.adamfranco.com/2009/09/09/high-availability-drupal-file-handling/</link>
		<comments>http://www.adamfranco.com/2009/09/09/high-availability-drupal-file-handling/#comments</comments>
		<pubDate>Wed, 09 Sep 2009 21:18:27 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Computers and Technology]]></category>
		<category><![CDATA[Drupal]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[Work]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=266</guid>
		<description><![CDATA[One of the requirements in the migration of our web sites to Drupal is that we create a robust and redundant platform that can stay running or degrade gracefully when hardware or software problems inevitably arise. While our sites get heavy use from our communities and the public, our traffic numbers are no where near [...]]]></description>
			<content:encoded><![CDATA[<p>One of the requirements in the migration of our <a href="http://www.middlebury.edu/">web</a> <a href="http://www.miis.edu">sites</a> to <a href="http://drupal.org/">Drupal</a> is that we create a robust and redundant platform that can stay running or degrade gracefully when hardware or software problems inevitably arise. While our sites get heavy use from our communities and the public, our traffic numbers are no where near those of a top-1000 site and could comfortably run off of one machine that ran both the database and web-server.<br />
<div id="attachment_297" class="wp-caption aligncenter" style="width: 541px"><img src="http://tmp.adamfranco.com/files/2009/09/1-SingleMachine.jpg" alt="Single Machine Configuration" title="Single Machine Configuration" width="531" height="332" class="size-full wp-image-297" /><p class="wp-caption-text">Single Machine Configuration</p></div><br />
This simple configuration however has the major weakness that any hiccups in the hardware or software of the machine will likely take the site offline until the issues can be addressed. In order to give our site a better chance at staying up as failures occur, we separate some of the functional pieces of the site onto discrete machines and then ensure that each function is redundant or fail-safe. This post and the next will detail a few of the techniques we have used to build a robust site.</p>
<p><span id="more-266"></span></p>
<h2>Pull out the database, use multiple web-servers</h2>
<p>The two main components of Drupal (and most similar web applications) are the webserver, which handles PHP execution and file-serving; and the MySQL database, which stores all data with the exception of uploaded files. By putting the database on a separate machine we can can have multiple machines acting as front-end web-servers, both of them reading and writing to the same database. In this way, it doesn&#8217;t matter which web-server handles a given request as they will both get the same information out of the database. With two or more web-servers, our platform gains some redundancy since one web-server can fail while the second keeps handling requests.</p>
<p>With both web-servers point at the same database server, the database server still remains a single point of failure. Database clustering can alleviate this problem, but will be the subject of a future post.</p>
<h2>Multiple web-server challenges</h2>
<p>This redundancy does come at a cost in complexity however, since we need to ensure that any uploaded files are available on both web-servers. There seem to be <a href="http://groups.drupal.org/node/1648">two primary ways</a> of tackling this problem (without resorting to costly and complex distributed file-system tools). The first is use rsync to copy files between the web-servers every few minutes.<br />
<div id="attachment_299" class="wp-caption aligncenter" style="width: 610px"><img src="http://tmp.adamfranco.com/files/2009/09/2a-Two-Web-servers-rsync.jpg" alt="Two web servers with rsync" title="2a - Two Web servers - rsync" width="600" class="size-full wp-image-299" /><p class="wp-caption-text">Two web servers with rsync</p></div><br />
While this is reasonably simple to set up between two web-servers, it comes with significant downsides:</p>
<ul>
<li>Files cannot be deleted in the sync as newly-added files will exist on only one web-server. Since the sync is two-way, there is no way for the rsync processes to tell the difference between a new file and a deleted file.</li>
<li>Requests that come to the &#8220;other&#8221; web-server will not be able to access new files until the sync happens.</li>
<li>If additional web-servers are added, the sync process needs to be updated on every existing web-server to include the new web-server</li>
</ul>
<p>The other alternative is to store uploaded files on a separate file-server, whose upload directory is mounted on each web-server using NFS. This method eliminates the synchronization problems, since all web-servers are essentially writing to the same directory.<br />
<div id="attachment_300" class="wp-caption aligncenter" style="width: 610px"><img src="http://tmp.adamfranco.com/files/2009/09/2b-Two-Web-servers-nfs.jpg" alt="Two web servers with NFS" title="2b - Two Web servers - nfs" width="600" class="size-full wp-image-300" /><p class="wp-caption-text">Two web servers with NFS</p></div><br />
On top of the complexity of adding a fourth machine (the file-server) to our mix, this method also leaves us with the file-server as a single point of failure &#8212; were it to go down, no uploaded files would be accessible.</p>
<h2>Best of both worlds</h2>
<p>In order to better solve this problem, the approach we took is to go the NFS route, but augment it with a backup copy of the files stored on the local file-system of each web-server. Every ten minutes or so a script (<a href='http://tmp.adamfranco.com/files/2009/09/sync_files.sh'>sync_files.sh</a>) runs that checks to see if the shared NFS directory is available, and if so syncs the uploaded-files to a backup location on the web-server&#8217;s file-system. This backup copy has its permissions set so that the Apache process cannot write to it, preventing synchronization problems if the shared NFS directory goes offline and we need to serve files out of the backup copy.<br />
<div id="attachment_302" class="wp-caption aligncenter" style="width: 610px"><img src="http://tmp.adamfranco.com/files/2009/09/3-Two-Web-servers-nfs+backup1.jpg" alt="Two web servers with NFS and local backup copies." title="3 - Two Web servers - nfs+backup" width="600" class="size-full wp-image-302" /><p class="wp-caption-text">Two web servers with NFS and local backup copies.</p></div><br />
A second script (<a href='http://tmp.adamfranco.com/files/2009/09/check_link.sh'>check_link.sh</a>) runs every minute and checks to see if the shared NFS directory is available. If it is offline, this script changes the symbolic link of our &#8220;files&#8221; directory so that Drupal will now use the read-only backup copy for its files. If the NFS directory comes back online, this script will again update the symbolic link to point at our writable shared NFS directory.</p>
<p>An important consideration in this setup is that the NFS share is mounted in &#8216;soft&#8217; mode so that file-access errors will time out quickly and allow for a timely switch-over to our backup files.<br />
<div class="wp-caption alignnone" style="width: 610px">
<pre>files.example.edu:/images       /mnt/files     nfs     soft    0 0</pre>
<p><p class="wp-caption-text">An example 'soft' mount line in /etc/fstab</p></div></p>
<p>If the default &#8216;hard&#8217; NFS mount is used, the check_link processes will hang indefinitely while trying to communicate with the file-server and never switch to our backup files.</p>
<p>Here is an example layout on the web-server to accomplish this setup:</p>
<pre style='width: 100%'># The scripts that will be run by cron:
/usr/local/bin/check_link.sh  # Run every minute
/usr/local/bin/sync_files.sh   # Run every 10 minutes

# The mounted NFS share:
/mnt/files/

# The backup copy of files:
/srv/files_read_only/

# The 'files' symbolic link, pointing normally at the NFS share:
/srv/files/ => /mnt/files/
# On NFS failure, this link will be switched to the backup directory:
/srv/files/ => /srv/files_read_only/

# The Drupal code directory:
/srv/drupal/
# The files directory for a site is a link into the switched files link
/srv/drupal/sites/www.example.com/files/ => /srv/files/www.example.com/files/
</pre>
<p>By mounting the shared NFS directory, keeping a read-only local copy of the files, and monitoring the state of the NFS directory we gain the following benefits:</p>
<ul>
<li>No problems with synchronization as all web-servers share the same remote filesystem.</li>
<li>Synchronization of the local backup copies is not a problem as this is always a one-way sync rather than a two-way sync between different web-servers.</li>
<li>While the NFS file-server is still a single point of failure, read access to the uploaded files (via the backup copy) will be restored after a maximum of one minute plus the NFS time-out (2 minutes by default for &#8216;soft&#8217; mounts).</li>
<li>The web-servers don&#8217;t need to know about each other, easing configuration if additional web-servers are added.</li>
</ul>
<p>This configuration adds an extra machine to the platform mix and a bit of complexity, but it makes normal operation robust (instant file availability to all web-servers) and allows for graceful degradation (file-access becomes read-only) if the file-server goes down.</p>
<p><em>Many thanks to our system administrator, Mark Pyfrom, for all of his help in developing and testing this platform.</em></p>
<p><em>* Update on 2009-09-10: added note about &#8216;soft&#8217; NFS mounts and an example file-system layout.</em></p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2009/09/09/high-availability-drupal-file-handling/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>24 Hours of Great Glen</title>
		<link>http://www.adamfranco.com/2009/08/15/24-hours-of-great-glen/</link>
		<comments>http://www.adamfranco.com/2009/08/15/24-hours-of-great-glen/#comments</comments>
		<pubDate>Sat, 15 Aug 2009 23:12:38 +0000</pubDate>
		<dc:creator>Adam</dc:creator>
				<category><![CDATA[Outdoors]]></category>
		<category><![CDATA[24 Hours of Great Glen]]></category>
		<category><![CDATA[Cycling]]></category>
		<category><![CDATA[Mountain Biking]]></category>
		<category><![CDATA[Racing]]></category>

		<guid isPermaLink="false">http://www.adamfranco.com/?p=240</guid>
		<description><![CDATA[This past weekend we headed to the White Mountains of New Hampshire for the 24 Hours of Great Glen mountain bike relay race. I had the pleasure of riding on a 5-person team with Spencer Taylor, Serena Taylor, Steffie Gould, and Simon Bird. Sarah and Celia filled in as our support crew. The goal of [...]]]></description>
			<content:encoded><![CDATA[<p>This past weekend we headed to the White Mountains of New Hampshire for the <a href="http://www.24hoursofgreatglen.com/">24 Hours of Great Glen</a> mountain bike relay race. I had the pleasure of riding on a 5-person team with Spencer Taylor, Serena Taylor, Steffie Gould, and Simon Bird. Sarah and Celia filled in as our support crew.</p>
<p><a href="http://tmp.adamfranco.com/files/2009/08/24-Hours-of-Great-Glen-GPS-2.jpg"><img src="http://www.adamfranco.com/wp-content/uploads/2009/08/24-Hours-of-Great-Glen-GPS-2-1024x689.jpg" alt="24 Hours of Great Glen GPS" title="24 Hours of Great Glen GPS" width="500" class="aligncenter size-large wp-image-242" /></a></p>
<p><span id="more-240"></span></p>
<p>The goal of the race is for a team to ride as many 8-mile laps as they can in 24 hours with everyone doing at least one lap at night (with high-powered helmet and/or bar mounted headlights). Teams are grouped in &#8216;classes&#8217; based on the size of team (1, 2, 4, or 5 members) and by skill (pro, expert, sport, beginner). There were just two skill-levels for 5-person teams: &#8220;Open&#8221; and &#8220;Cruiser&#8221;. There was some confusion in registering and we ended up in the &#8220;Open&#8221; category instead of the more appropriate &#8220;Cruiser&#8221; category, so we got crushed in standings. We had a great time anyway though and ended up coming in in 55th place over all out of 156 teams.</p>
<div id="PictoBrowser090815185249">Get the flash player here: http://www.adobe.com/flashplayer</div>
<p><script type="text/javascript" src="http://www.db798.com/pictobrowser/swfobject.js"></script><script type="text/javascript"> var so = new SWFObject("http://www.db798.com/pictobrowser.swf", "PictoBrowser", "500", "500", "8", "#EEEEEE"); so.addVariable("source", "sets"); so.addVariable("names", "24 Hours of Great Glen"); so.addVariable("userName", "Adam Franco"); so.addVariable("userId", "60594606@N00"); so.addVariable("ids", "72157621924084527"); so.addVariable("titles", "on"); so.addVariable("displayNotes", "on"); so.addVariable("thumbAutoHide", "off"); so.addVariable("imageSize", "medium"); so.addVariable("vAlign", "mid"); so.addVariable("vertOffset", "0"); so.addVariable("colorHexVar", "EEEEEE"); so.addVariable("initialScale", "off"); so.addVariable("bgAlpha", "74"); so.write("PictoBrowser090815185249");	</script></p>
<p>The race had a pretty neat computerized timing system going which allowed us to instantly see our times and standings after finishing a lap. According to <a href="http://www.grannygear.com/realtime/public/gg_team.php?team_id=184&#038;action=display&#038;display_class_split=1">our results</a>, we completed 22 laps, covered 182.6 miles, and climbed 26,070 feet. Of that, <a href="http://www.grannygear.com/realtime/public/gg_rider.php?action=display&#038;person_id=371">I rode</a> 6 laps, covering 49.8 miles, and climbing 7,110 feet.</p>
<p><a href='http://www.flickr.com/photos/sarahmcgowen/sets/72157621999349284/'>Photos by Sarah</a>:</p>
<div id="PictoBrowser090815185612">Get the flash player here: http://www.adobe.com/flashplayer</div>
<p><script type="text/javascript" src="http://www.db798.com/pictobrowser/swfobject.js"></script><script type="text/javascript"> var so = new SWFObject("http://www.db798.com/pictobrowser.swf", "PictoBrowser", "500", "500", "8", "#EEEEEE"); so.addVariable("source", "sets"); so.addVariable("names", "24 Hours of Great Glen "); so.addVariable("userName", "sarfrancisco"); so.addVariable("userId", "62861095@N00"); so.addVariable("ids", "72157621999349284"); so.addVariable("titles", "on"); so.addVariable("displayNotes", "always"); so.addVariable("thumbAutoHide", "off"); so.addVariable("imageSize", "medium"); so.addVariable("vAlign", "top"); so.addVariable("vertOffset", "0"); so.addVariable("colorHexVar", "EEEEEE"); so.addVariable("initialScale", "off"); so.addVariable("bgAlpha", "90"); so.write("PictoBrowser090815185612");	</script></p>
<p>We had a great time both riding and just hanging out. Since only one person was riding at a time, we usually had 5 or 6 people in camp hanging out in camp by the fire. This was my first 24-hour race and I wasn&#8217;t really  sure what to expect both with sleepiness and night-time riding difficulty. To deal with the latter, I ended up buying a <a href="http://www.adamfranco.com/archives/231">headlight for my handlebars</a> and borrowed one of Eli&#8217;s LED flashlights for my helmet.<br />
<a href="http://tmp.adamfranco.com/files/2009/08/CRW_6432.jpg"><img src="http://tmp.adamfranco.com/files/2009/08/CRW_6432.jpg" alt="CRW_6432" title="CRW_6432" width="500" class="aligncenter size-full wp-image-250" /></a><br />
This combo of lights worked amazingly well. The wide flood of the handlebar-mounted MityCross providing nice shadows for depth perception, while the spot-beam of the flashlight lit up the trail up to a hundred yards ahead. While some of the pits of mud were still dark, I was able to ride my night laps at full speed &#8211; I even maxed out my gearing on one of the carriage-road downhill sections.</p>
<p>Overall, I was pretty exhausted by Sunday afternoon, but waking up in the middle of the night to ride wasn&#8217;t so bad. Getting a lot of QT with great friends made the whole thing a blast. I can&#8217;t wait until next year!</p>
<p>Here are a few videos from Sarah to finish things off:</p>
<p>Start of the race, part 1:<br />
<object width="560" height="340"><param name="movie" value="http://www.youtube.com/v/35OFMrO27Ww&#038;hl=en&#038;fs=1&#038;"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/35OFMrO27Ww&#038;hl=en&#038;fs=1&#038;" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="560" height="340"></embed></object></p>
<p>Start of the race, part 2:<br />
<object width="560" height="340"><param name="movie" value="http://www.youtube.com/v/9S5KNMrb5Lc&#038;hl=en&#038;fs=1&#038;"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/9S5KNMrb5Lc&#038;hl=en&#038;fs=1&#038;" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="560" height="340"></embed></object></p>
<p>Me riding &#8220;The Birches&#8221; section:<br />
<object width="425" height="344"><param name="movie" value="http://www.youtube.com/v/IdrONA1cOOQ&#038;hl=en&#038;fs=1&#038;"></param><param name="allowFullScreen" value="true"></param><param name="allowscriptaccess" value="always"></param><embed src="http://www.youtube.com/v/IdrONA1cOOQ&#038;hl=en&#038;fs=1&#038;" type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" width="425" height="344"></embed></object></p>
]]></content:encoded>
			<wfw:commentRss>http://www.adamfranco.com/2009/08/15/24-hours-of-great-glen/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
