This commit was generated by cvs2svn to compensate for changes in r1747,
authorMark Huang <mlhuang@cs.princeton.edu>
Mon, 27 Mar 2006 17:36:46 +0000 (17:36 +0000)
committerMark Huang <mlhuang@cs.princeton.edu>
Mon, 27 Mar 2006 17:36:46 +0000 (17:36 +0000)
which included commits to RCS files with non-trunk default branches.

api-config [new file with mode: 0755]
build.sh [new file with mode: 0755]
guest.init [new file with mode: 0755]
host.init [new file with mode: 0755]
myplc.spec [new file with mode: 0644]
plc-config [new file with mode: 0755]
plc_config.py [new file with mode: 0644]
plc_config.xml [new file with mode: 0644]

diff --git a/api-config b/api-config
new file mode 100755 (executable)
index 0000000..5901a6f
--- /dev/null
@@ -0,0 +1,97 @@
+#!/usr/bin/python
+#
+# Bootstraps the PLC database with a default administrator account and
+# a default site.
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import plcapilib
+(plcapi, moreopts, argv) = plcapilib.plcapi(globals())
+from plc_config import PLCConfiguration
+
+
+def main():
+    cfg = PLCConfiguration()
+    cfg.load()
+    variables = cfg.variables()
+
+    # Load variables into dictionaries
+    (category, variablelist) = variables['plc']
+    plc = dict(zip(variablelist.keys(),
+                   [variable['value'] for variable in variablelist.values()]))
+
+    (category, variablelist) = variables['plc_www']
+    plc_www = dict(zip(variablelist.keys(),
+                       [variable['value'] for variable in variablelist.values()]))
+
+    (category, variablelist) = variables['plc_api']
+    plc_api = dict(zip(variablelist.keys(),
+                       [variable['value'] for variable in variablelist.values()]))
+
+    # Create/update the default administrator account (should be
+    # person_id 2).
+    admin = { 'person_id': 2,
+              'first_name': "Default",
+              'last_name': "Administrator",
+              'email': plc['root_user'],
+              'password': plc['root_password'] }
+    persons = AdmGetPersons([admin['person_id']])
+    if not persons:
+        person_id = AdmAddPerson(admin['first_name'], admin['last_name'], admin)
+        if person_id != admin['person_id']:
+            # Huh? Someone deleted the account manually from the database.
+            AdmDeletePerson(person_id)
+            raise Exception, "Someone deleted the \"%s %s\" account from the database!" % \
+                  (admin['first_name'], admin['last_name'])
+        AdmSetPersonEnabled(person_id, True)
+    else:
+        person_id = persons[0]['person_id']
+        AdmUpdatePerson(person_id, admin)
+
+    # Create/update the default site (should be site_id 0)
+    if plc_www['port'] == '80':
+        url = "http://" + plc_www['host'] + "/"
+    elif plc_www['port'] == '443':
+        url = "https://" + plc_www['host'] + "/"
+    else:
+        url = "http://" + plc_www['host'] + ":" + plc_www['port'] + "/"
+    site = { 'site_id': 1,
+             'name': plc['name'] + " Central",
+             'abbreviated_name': plc['name'],
+             'login_base': plc['slice_prefix'],
+             'is_public': False,
+             'url': url,
+             'max_slices': 100 }
+
+    sites = AdmGetSites([site['site_id']])
+    if not sites:
+        site_id = AdmAddSite(site['name'], site['abbreviated_name'], site['login_base'], site)
+        if site_id != site['site_id']:
+            AdmDeleteSite(site_id)
+            raise Exception, "Someone deleted the \"%s\" site from the database!" % \
+                  site['name']
+    else:
+        site_id = sites[0]['site_id']
+        # XXX login_base cannot be updated
+        del site['login_base']
+        AdmUpdateSite(site_id, site)
+
+    # The default administrator account must be associated with a site
+    # in order to login.
+    AdmAddPersonToSite(admin['person_id'], site['site_id'])
+    AdmSetPersonPrimarySite(admin['person_id'], site['site_id'])
+
+    # Grant admin and PI roles to the default administrator account
+    AdmGrantRoleToPerson(admin['person_id'], 10)
+    AdmGrantRoleToPerson(admin['person_id'], 20)
+
+    # XXX Setup default slice attributes and initscripts (copy from PLC)
+
+    # XXX Setup PlanetLabConf entries (copy from PLC)
+
+if __name__ == '__main__':
+    main()
diff --git a/build.sh b/build.sh
new file mode 100755 (executable)
index 0000000..d172a84
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,216 @@
+#!/bin/bash
+#
+# Builds a Fedora based PLC image. You should be able to run this
+# script multiple times without a problem.
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+PATH=/sbin:/bin:/usr/sbin:/usr/bin
+
+# In a normal CVS environment, the requisite CVS modules (including
+# build/) are located at the same level we are. In a PlanetLab RPM
+# build environment (see the RPM spec file), they are checked out into
+# a subdirectory.
+if [ -d ./build ] ; then
+    PATH=$PATH:./build
+    srcdir=.
+elif [ -d ../build ] ; then
+    PATH=$PATH:../build
+    srcdir=..
+else
+    echo "Error: Could not find sources in either . or .."
+    exit 1
+fi
+
+export PATH
+
+# PLC configuration file
+config=plc_config.xml
+
+# Release and architecture to install
+releasever=2
+basearch=i386
+
+# Data directory base
+usr_share=/usr/share
+
+# Initial size of the image
+size=1000000000
+
+usage()
+{
+    echo "Usage: build.sh [OPTION]..."
+    echo "     -c file         PLC configuration file (default: $config)"
+    echo "     -r release      Fedora release number (default: $releasever)"
+    echo "     -a arch         Fedora architecture (default: $basearch)"
+    echo "     -d datadir      Data directory base (default: $usr_share)"
+    echo "     -s size         Approximate size of the installation (default: $size)"
+    echo "     -h              This message"
+    exit 1
+}
+
+# Get options
+while getopts "c:r:a:d:s:h" opt ; do
+    case $opt in
+       c)
+           config=$OPTARG
+           ;;
+       r)
+           releasever=$OPTARG
+           ;;
+       a)
+           basearch=$OPTARG
+           ;;
+       d)
+           usr_share=$OPTARG
+           ;;
+       s)
+           size=$OPTARG
+           ;;
+       h|*)
+           usage
+           ;;
+    esac
+done
+
+root=fc$releasever
+data=data$releasever
+
+if [ ! -f $root.img ] ; then
+    bs=4096
+    count=$(($size / 4096))
+    dd bs=$bs count=$count if=/dev/zero of=$root.img
+    mkfs.ext3 -j -F $root.img
+fi
+
+mkdir -p $root $data
+mount -o loop $root.img $root
+trap "umount $root; exit 1" ERR
+
+#
+# Build
+#
+
+# Get package list
+while read package ; do
+    packages="$packages -p $package"
+done < <(./plc-config --packages $config)
+
+# Install base system
+mkfedora -v -r $releasever -a $basearch $packages $root
+
+# FC2 minilogd starts up during shutdown and makes unmounting
+# impossible. Just get rid of it.
+rm -f $root/sbin/minilogd
+ln -nsf /bin/true $root/sbin/minilogd
+
+# Build schema
+make -C $srcdir/pl_db
+
+#
+# Install
+#
+
+# Install configuration scripts
+install -D -m 755 plc_config.py $root/tmp/plc_config.py
+chroot $root sh -c 'cd /tmp; python plc_config.py build; python plc_config.py install'
+install -D -m 755 plc-config $root/usr/bin/plc-config
+install -D -m 755 api-config $root/usr/bin/api-config
+
+# Install init script
+install -D -m 755 guest.init $root/etc/init.d/plc
+chroot $root sh -c 'chkconfig --add plc; chkconfig plc on'
+
+# Install DB schema and API code
+mkdir -p $root/usr/share
+rsync -a $srcdir/pl_db $srcdir/plc_api $root/usr/share/
+
+# Install web scripts
+mkdir -p $root/usr/bin
+install -m 755 \
+    plc/scripts/gen-sites-xml.py \
+    plc/scripts/gen-slices-xml-05.py \
+    plc/scripts/gen-static-content.py \
+    $root/usr/bin/
+
+# Install web pages
+mkdir -p $root/var/www/html
+rsync -a $srcdir/plc_www/ $root/var/www/html/
+
+# Install configuration file
+install -D -m 644 $config $data/etc/planetlab/plc_config.xml
+
+# Move "data" directories out of the installation
+datadirs=(
+/etc/planetlab
+/var/lib/pgsql
+/var/www/html/alpina-logs
+/var/www/html/boot
+/var/www/html/download
+/var/www/html/generated
+/var/www/html/install-rpms
+/var/www/html/xml
+)
+
+mkdir -p $root/data
+for datadir in "${datadirs[@]}" ; do
+    mkdir -p ${data}$datadir
+    if [ -d $root/$datadir -a ! -h $root/$datadir ] ; then
+       (cd $root && find ./$datadir | cpio -p -d -u ../$data/)
+    fi
+    rm -rf $root/$datadir
+    mkdir -p $(dirname $root/$datadir)
+    ln -nsf /data$datadir $root/$datadir
+done
+
+# Shrink to 100 MB free space
+kb=$(python <<EOF
+import os
+df = os.statvfs('$root')
+target = 100 * 1024 * 1024 / df.f_bsize
+if df.f_bavail > target:
+    print (df.f_blocks - (df.f_bavail - target)) * df.f_bsize / 1024
+EOF
+)
+
+umount $root
+trap - ERR
+
+if [ -n "$kb" ] ; then
+    # Setup loopback association. Newer versions of losetup have a -f
+    # option which finds an unused loopback device, but we must
+    # support FC2 for now.
+    # dev_loop=$(losetup -f)
+    for i in `seq 1 7` ; do
+       if ! grep -q "^/dev/loop$i" /proc/mounts ; then
+           dev_loop="/dev/loop$i"
+           break
+       fi
+    done
+    losetup $dev_loop $root.img
+    trap "losetup -d $dev_loop" ERR
+
+    # Resize the filesystem
+    e2fsck -f $dev_loop
+    resize2fs $dev_loop ${kb}K
+
+    # Tear down loopback association
+    losetup -d $dev_loop
+    trap - ERR
+
+    # Truncate the image file
+    perl -e "truncate '$root.img', $kb*1024"
+fi
+
+# Write sysconfig
+cat >plc.sysconfig <<EOF
+PLC_ROOT=$usr_share/plc/$root
+PLC_DATA=$usr_share/plc/$data
+#PLC_OPTIONS="-v"
+EOF
+
+exit $RETVAL
diff --git a/guest.init b/guest.init
new file mode 100755 (executable)
index 0000000..2df5a9e
--- /dev/null
@@ -0,0 +1,634 @@
+#!/bin/bash
+#
+# plc  Manages all PLC services on this machine
+#
+# chkconfig: 2345 5 99
+#
+# description: Manages all PLC services on this machine
+#
+# $Id: plc.init,v 1.6 2005/04/24 19:48:11 mlhuang Exp $
+#
+
+PATH=/sbin:/bin:/usr/bin:/usr/sbin
+
+# Source function library.
+. /etc/init.d/functions
+
+# Verbosity
+verbose=0
+
+# Keep in order! All steps should be idempotent. This means that you
+# should be able to run them multiple times without depending on
+# anything previously being run. The idea is that when the
+# configuration changes, "service plc restart" is called, all
+# dependencies are fixed up, and everything just works.
+steps=(
+network
+syslog
+postgresql
+ssl
+gpg
+ssh
+apache
+api
+cron
+)
+nsteps=${#steps[@]}
+
+gethostbyname ()
+{
+    perl -MSocket -e '($a,$b,$c,$d,@addrs) = gethostbyname($ARGV[0]); print inet_ntoa($addrs[0]) . "\n";' $1 2>/dev/null
+}
+
+# Regenerate configuration files
+reload ()
+{
+    # Load configuration
+    plc-config --shell >/etc/planetlab/plc_config
+    . /etc/planetlab/plc_config
+
+    # Generate various defaults
+    if [ -z "$PLC_DB_PASSWORD" ] ; then
+       PLC_DB_PASSWORD=$(uuidgen)
+       plc-config --category=plc_db --variable=password --value="$PLC_DB_PASSWORD" --save
+    fi
+
+    if [ -z "$PLC_API_MAINTENANCE_PASSWORD" ] ; then
+       PLC_API_MAINTENANCE_PASSWORD=$(uuidgen)
+       plc-config --category=plc_api --variable=maintenance_password --value="$PLC_API_MAINTENANCE_PASSWORD" --save
+    fi
+
+    if [ -z "$PLC_API_MAINTENANCE_SOURCES" ] ; then
+       for server in API BOOT WWW ; do
+           eval hostname=\${PLC_${server}_HOST}
+           ip=$(gethostbyname $hostname)
+           if [ -n "$ip" ] ; then
+               if [ -n "$PLC_API_MAINTENANCE_SOURCES" ] ; then
+                   PLC_API_MAINTENANCE_SOURCES="$PLC_API_MAINTENANCE_SOURCES $ip"
+               else
+                   PLC_API_MAINTENANCE_SOURCES=$ip
+               fi
+           fi
+       done
+       plc-config --category=plc_api --variable=maintenance_sources --value="$PLC_API_MAINTENANCE_SOURCES" --save
+    fi
+
+    # Save configuration
+    mkdir -p /etc/planetlab/php
+    plc-config --php >/etc/planetlab/php/plc_config.php
+    plc-config --shell >/etc/planetlab/plc_config
+
+    # For backward compatibility, until we can convert all code to use
+    # the now standardized variable names.
+
+    # DB constants are all named the same
+    ln -sf plc_config /etc/planetlab/plc_db
+
+    # API constants
+    cat >/etc/planetlab/plc_api <<EOF
+PL_API_SERVER='$PLC_API_HOST'
+PL_API_PATH='$PLC_API_PATH'
+PL_API_PORT=$PLC_API_SSL_PORT
+PL_API_CAPABILITY_AUTH_METHOD='capability'
+PL_API_CAPABILITY_PASS='$PLC_API_MAINTENANCE_PASSWORD'
+PL_API_CAPABILITY_USERNAME='$PLC_API_MAINTENANCE_USER'
+PL_API_TICKET_KEY_FILE='$PLC_API_TICKET_KEY'
+PLANETLAB_SUPPORT_EMAIL='$PLC_MAIL_SUPPORT_ADDRESS'
+BOOT_MESSAGES_EMAIL='$PLC_MAIL_BOOT_ADDRESS'
+WWW_BASE='$PLC_WWW_HOST'
+BOOT_BASE='$PLC_BOOT_HOST'
+EOF
+
+    # The format is
+    #
+    # ip:max_role_id:organization_id:password
+    #
+    # It is unlikely that we will let federated sites use the
+    # maintenance account to access each others' APIs, so we always
+    # set organization_id to -1.
+    (
+       echo -n "PL_API_CAPABILITY_SOURCES='"
+       first=1
+       for ip in $PLC_API_MAINTENANCE_SOURCES ; do
+           if [ $first -ne 1 ] ; then
+               echo -n " "
+           fi
+           first=0
+           echo -n "$ip:-1:-1:$PLC_API_MAINTENANCE_PASSWORD"
+       done
+       echo "'"
+    ) >>/etc/planetlab/plc_api
+
+    cat >/etc/planetlab/php/site_constants.php <<"EOF"
+<?php
+include('plc_config.php');
+
+DEFINE('PL_API_SERVER', PLC_API_HOST);
+DEFINE('PL_API_PATH', PLC_API_PATH);
+DEFINE('PL_API_PORT', PLC_API_SSL_PORT);
+DEFINE('PL_API_CAPABILITY_AUTH_METHOD', 'capability');
+DEFINE('PL_API_CAPABILITY_PASS', PLC_API_MAINTENANCE_PASSWORD);
+DEFINE('PL_API_CAPABILITY_USERNAME', PLC_API_MAINTENANCE_USER);
+DEFINE('WWW_BASE', PLC_WWW_HOST);
+DEFINE('BOOT_BASE', PLC_BOOT_HOST);
+DEFINE('DEBUG', PLC_WWW_DEBUG);
+DEFINE('API_CALL_DEBUG', PLC_API_DEBUG);
+DEFINE('SENDMAIL', PLC_MAIL_ENABLED);
+DEFINE('PLANETLAB_SUPPORT_EMAIL', PLC_NAME . 'Support <' . PLC_MAIL_SUPPORT_ADDRESS . '>');
+DEFINE('PLANETLAB_SUPPORT_EMAIL_ONLY', PLC_MAIL_SUPPORT_ADDRESS);
+?>
+EOF
+}
+
+config_network ()
+{
+    case "$1" in
+       start)
+           # Minimal /etc/hosts
+           (
+               echo "127.0.0.1 localhost.localdomain localhost"
+               for server in API BOOT WWW ; do
+                   eval hostname=\${PLC_${server}_HOST}
+                   ip=$(gethostbyname $hostname)
+                   if [ -n "$ip" ] ; then
+                       echo "$ip       $hostname"
+                   fi
+               done
+           ) >/etc/hosts
+
+           # Set up nameservers
+           (
+               [ -n "$PLC_NET_DNS1" ] && echo "nameserver $PLC_NET_DNS1"
+               [ -n "$PLC_NET_DNS2" ] && echo "nameserver $PLC_NET_DNS2"
+           ) >/etc/resolv.conf
+           ;;
+    esac
+}
+
+config_syslog ()
+{
+    service syslog $1
+    RETVAL=$?
+}
+
+config_postgresql ()
+{
+    # Default locations
+    PGDATA=/var/lib/pgsql/data
+    postgresql_conf=$PGDATA/postgresql.conf
+    pghba_conf=$PGDATA/pg_hba.conf
+
+    case "$1" in
+       start)
+           if [ "$PLC_DB_ENABLED" != "1" ] ; then
+               return 0
+           fi
+
+           # Set data directory and redirect startup output to /var/log/pgsql
+           mkdir -p /etc/sysconfig/pgsql
+           (
+               echo "PGDATA=$PGDATA"
+               echo "PGLOG=/var/log/pgsql"
+           ) >>/etc/sysconfig/pgsql/postgresql
+
+           # PostgreSQL must be started at least once to bootstrap
+           # /var/lib/pgsql/data
+           if [ ! -f $postgresql_conf ] ; then
+               service postgresql start
+               service postgresql stop
+           fi
+
+           # Enable DB server. PostgreSQL >=8.0 defines listen_addresses,
+           # PostgreSQL 7.x uses tcpip_socket.
+           if grep -q listen_addresses $postgresql_conf ; then
+               sed -i -e '/^listen_addresses/d' $postgresql_conf
+               echo "listen_addresses = '*'" >>$postgresql_conf
+           elif grep -q tcpip_socket $postgresql_conf ; then
+               sed -i -e '/^tcpip_socket/d' $postgresql_conf
+               echo "tcpip_socket = true" >>$postgresql_conf
+           fi
+
+           # Disable access to all DBs from all hosts
+           sed -i -e '/^\(host\|local\)/d' $pghba_conf
+
+           # Enable passwordless localhost access
+           echo "local all all trust" >>$pghba_conf
+
+           # Enable access from the API and web servers
+           PLC_API_IP=$(gethostbyname $PLC_API_HOST)
+           PLC_WWW_IP=$(gethostbyname $PLC_WWW_HOST)
+           (
+               echo "host $PLC_DB_NAME $PLC_DB_USER $PLC_API_IP/32 password"
+               echo "host $PLC_DB_NAME $PLC_DB_USER $PLC_WWW_IP/32 password"
+           ) >>$pghba_conf
+
+           # Fix ownership (sed -i changes it)
+           chown postgres:postgres $postgresql_conf $pghba_conf
+
+           # Start up the server
+           service postgresql start
+           RETVAL=$?
+
+           # Create/update the unprivileged database user and password
+           if ! psql -U $PLC_DB_USER -c "" template1 >/dev/null 2>&1 ; then
+               psql -U postgres -c "CREATE USER $PLC_DB_USER PASSWORD '$PLC_DB_PASSWORD'" template1
+           else
+               psql -U postgres -c "ALTER USER $PLC_DB_USER WITH PASSWORD '$PLC_DB_PASSWORD'" template1
+           fi
+
+           # Create the database if necessary
+           if ! psql -U $PLC_DB_USER -c "" $PLC_DB_NAME >/dev/null 2>&1 ; then
+               createdb -U postgres $PLC_DB_NAME
+               psql -U $PLC_DB_USER -f /usr/share/pl_db/plc_schema_3.sql $PLC_DB_NAME
+           fi
+           ;;
+
+       stop)
+           # Drop the current user in case the username changes
+           psql -U postgres -c "DROP USER $PLC_DB_USER" template1
+
+           # WARNING: If the DB name changes, the old DB will be left
+           # intact and a new one will be created. If it changes
+           # back, the old DB will not be re-created.
+
+           # Shut down the server
+           service postgresql stop
+           RETVAL=$?
+           ;;
+    esac
+}
+
+# Generate GPG keys
+config_gpg ()
+{
+    case "$1" in
+       start)
+           # Generate GPG keyrings
+           if [ ! -f $PLC_ROOT_GPG_KEY_PUB -o ! -f $PLC_ROOT_GPG_KEY ] ; then
+               mkdir -p $(dirname $PLC_ROOT_GPG_KEY_PUB)
+               mkdir -p $(dirname $PLC_ROOT_GPG_KEY)
+
+               # Temporarily replace /dev/random with /dev/urandom to
+               # avoid running out of entropy.
+               rm -f /dev/random
+               mknod /dev/random c 1 9
+               gpg --homedir=/root --batch --gen-key <<EOF
+Key-Type: DSA
+Key-Length: 1024
+Subkey-Type: ELG-E
+Subkey-Length: 1024
+Name-Real: $PLC_NAME Central
+Name-Comment: http://$PLC_WWW_HOST/
+Name-Email: $PLC_MAIL_SUPPORT_ADDRESS
+Expire-Date: 0
+%pubring $PLC_ROOT_GPG_KEY_PUB
+%secring $PLC_ROOT_GPG_KEY
+%commit
+EOF
+               RETVAL=$?
+               rm -f /dev/random
+               mknod /dev/random c 1 8
+               chmod 600 $PLC_ROOT_GPG_KEY_PUB $PLC_ROOT_GPG_KEY
+           fi
+           ;;
+    esac
+}
+
+symlink ()
+{
+    mkdir -p $(dirname $2)
+    rm -f $2
+    ln -s $1 $2
+}
+
+# Generate SSL certificates
+config_ssl ()
+{
+    case "$1" in
+       start)
+           # Generate a self-signed SSL certificate. These nice
+           # commands came from the mod_ssl spec file for Fedora Core
+           # 2. We generate only a single certificate for the web
+           # server, then make copies for the API and boot
+           # servers. As always, these certificates may be overridden
+           # later.
+
+           # Generate SSL private key
+           if [ ! -f $PLC_WWW_SSL_KEY ] ; then
+               mkdir -p $(dirname $PLC_WWW_SSL_KEY)
+               openssl genrsa -rand /proc/apm:/proc/cpuinfo:/proc/dma:/proc/filesystems:/proc/interrupts:/proc/ioports:/proc/pci:/proc/rtc:/proc/uptime 1024 >$PLC_WWW_SSL_KEY
+               RETVAL=$(($RETVAL+$?))
+               chmod 600 $PLC_WWW_SSL_KEY
+           fi
+
+           # Generate self-signed certificate
+           if [ ! -f $PLC_WWW_SSL_CRT ] ; then
+               mkdir -p $(dirname $PLC_WWW_SSL_CRT)
+               openssl req -new -x509 -days 365 -set_serial $RANDOM \
+                   -key $PLC_WWW_SSL_KEY -out $PLC_WWW_SSL_CRT <<EOF
+--
+State
+City
+Organization
+$PLC_NAME Central
+$PLC_WWW_HOST
+$PLC_MAIL_SUPPORT_ADDRESS
+EOF
+               RETVAL=$(($RETVAL+$?))
+               chmod 644 $PLC_WWW_SSL_CRT
+           fi
+
+           # Make copies for the API and boot servers
+           if [ ! -f $PLC_API_SSL_KEY ] ; then
+               cp -a $PLC_WWW_SSL_KEY $PLC_API_SSL_KEY
+           fi
+           if [ ! -f $PLC_API_SSL_CRT ] ; then
+               cp -a $PLC_WWW_SSL_CRT $PLC_API_SSL_CRT
+           fi
+           if [ ! -f $PLC_API_SSL_KEY ] ; then
+               cp -a $PLC_WWW_SSL_KEY $PLC_API_SSL_KEY
+           fi
+           if [ ! -f $PLC_API_SSL_CRT ] ; then
+               cp -a $PLC_WWW_SSL_CRT $PLC_API_SSL_CRT
+           fi
+
+           # Install into both /etc/pki (Fedora Core 4) and
+           # /etc/httpd/conf (Fedora Core 2). If the API, boot, and
+           # web servers are all running on the same machine, the web
+           # server certificate takes precedence.
+           for server in API BOOT WWW ; do
+               eval enabled=\${PLC_${server}_ENABLED}
+               if [ "$enabled" != "1" ] ; then
+                   continue
+               fi
+               eval ssl_crt=\${PLC_${server}_SSL_CRT}
+               eval ssl_key=\${PLC_${server}_SSL_KEY}
+
+               symlink $ssl_crt /etc/pki/tls/certs/localhost.crt
+               symlink $ssl_key /etc/pki/tls/private/localhost.key
+               symlink $ssl_crt /etc/httpd/conf/ssl.crt/server.crt
+               symlink $ssl_key /etc/httpd/conf/ssl.key/server.key
+           done
+           ;;
+    esac
+}
+
+# Generate SSH keys
+config_ssh ()
+{
+    # XXX Could make these configurable
+    KEY_TYPE_ROOT=rsa
+    KEY_LEN_ROOT=1024
+    KEY_TYPE_DEBUG=rsa
+    KEY_LEN_DEBUG=2048 
+
+    case "$1" in
+       start)
+           tmp=$(mktemp -d /tmp/ssh.XXXXXX)
+
+           # Generate root SSH key
+           if [ ! -f $PLC_ROOT_SSH_KEY_PUB -o ! -f $PLC_ROOT_SSH_KEY ] ; then
+               ssh-keygen -N "" -C "$PLC_NAME Central <$PLC_MAIL_SUPPORT_ADDRESS>" \
+                   -b $KEY_LEN_ROOT -t $KEY_TYPE_ROOT -f $tmp/root
+               RETVAL=$(($RETVAL+$?))
+               install -D -m 600 $tmp/root $PLC_ROOT_SSH_KEY
+               install -D -m 600 $tmp/root.pub $PLC_ROOT_SSH_KEY_PUB
+           fi
+
+           # Generate debug SSH key
+           if [ ! -f $PLC_DEBUG_SSH_KEY_PUB -o ! -f $PLC_DEBUG_SSH_KEY ] ; then
+               ssh-keygen -N "" -C "$PLC_NAME Central <$PLC_MAIL_SUPPORT_ADDRESS>" \
+                   -b $KEY_LEN_DEBUG -t $KEY_TYPE_DEBUG -f $tmp/debug
+               RETVAL=$(($RETVAL+$?))
+               install -D -m 600 $tmp/debug $PLC_DEBUG_SSH_KEY
+               install -D -m 600 $tmp/debug.pub $PLC_DEBUG_SSH_KEY_PUB
+           fi
+
+           rm -rf $tmp
+           ;;
+    esac
+}
+
+# Configure Apache web server
+config_apache ()
+{
+    # Default locations
+    DocumentRoot=/var/www/html
+    php_ini=/etc/php.ini
+    httpd_conf=/etc/httpd/conf/httpd.conf
+    ssl_conf=/etc/httpd/conf.d/ssl.conf
+    plc_conf=/etc/httpd/conf.d/plc.conf
+
+    case "$1" in
+       start)
+           if [ "$PLC_API_ENABLED" != "1" -a \
+                "$PLC_BOOT_ENABLED" != "1" -a \
+                "$PLC_WWW_ENABLED" != "1" ] ; then
+               return 0
+           fi
+
+           # Set the default include path
+           include_path=".:$DocumentRoot/includes:$DocumentRoot/generated:/etc/planetlab/php"
+           sed -i -e "s@;include_path = \"\.:.*\"@include_path = \"$include_path\"@" $php_ini
+
+           # Set the port numbers. If the API, boot, and web servers
+           # are all running on the same machine, the web server port
+           # numbers take precedence.
+           for server in API BOOT WWW ; do
+               eval enabled=\${PLC_${server}_ENABLED}
+               if [ "$enabled" != "1" ] ; then
+                   continue
+               fi
+               eval http_port=\${PLC_${server}_PORT}
+               eval https_port=\${PLC_${server}_SSL_PORT}
+
+               if [ -n "$http_port" ] ; then
+                   sed -i -e "s/^Listen .*/Listen $http_port/" $httpd_conf
+               fi
+               if [ -n "$https_port" ] ; then
+                   sed -i -e "s/^Listen .*/Listen $https_port/" $ssl_conf
+               fi
+           done
+                   
+           # Set custom Apache directives
+           (
+               if [ "$PLC_API_ENABLED" = "1" ] ; then
+                   cat <<EOF
+<Location $PLC_API_PATH>
+    SetHandler python-program
+    PythonPath "sys.path + ['/usr/share/plc_api']"
+    PythonHandler mod_pythonXMLRPC
+</Location>
+EOF
+               fi
+
+               cat <<EOF
+<VirtualHost $PLC_WWW_HOST:$PLC_WWW_PORT>
+    Redirect /db https://$PLC_WWW_HOST:$PLC_WWW_SSL_PORT/db
+</VirtualHost>
+EOF
+           ) >$plc_conf
+
+           # Make alpina-logs directory writable for bootmanager log upload
+           chown apache:apache $DocumentRoot/alpina-logs/nodes
+
+           service httpd start
+           RETVAL=$?
+           ;;
+
+       stop)
+           service httpd stop
+           RETVAL=$?
+           ;;
+    esac
+}
+
+config_api ()
+{
+    case "$1" in
+       start)
+           if [ "$PLC_API_ENABLED" -ne 1 ] ; then
+               return
+           fi
+
+           # Update the maintenance account username. This can't be
+           # done through the api-config script since it uses the
+           # maintenance account to access the API. The maintenance
+           # account should be person_id 1 since it is created by the
+           # DB schema itself.
+           psql -U $PLC_DB_USER -c "UPDATE persons SET email='$PLC_API_MAINTENANCE_USER' WHERE person_id=1" $PLC_DB_NAME
+
+           # Bootstrap the DB
+           api-config
+           RETVAL=$?
+           ;;
+    esac
+}
+
+config_cron ()
+{
+    case "$1" in
+       start)
+           cat >/etc/cron.d/plc.cron <<EOF
+SHELL=/bin/bash
+PATH=/sbin:/bin:/usr/sbin:/usr/bin
+MAILTO=$PLC_MAIL_SUPPORT_ADDRESS
+HOME=/
+
+# minute hour day-of-month month day-of-week user command
+*/5 * * * * root gen-slices-xml-05.py
+*/15 * * * * root gen-sites-xml.py
+*/15 * * * * root gen-static-content.py
+EOF
+
+           # Run them once at startup
+           gen-slices-xml-05.py
+           gen-sites-xml.py
+           gen-static-content.py
+
+           service crond start
+           RETVAL=$?
+           ;;
+
+       stop)
+           service crond stop
+           RETVAL=$?
+           ;;
+    esac
+}
+
+usage()
+{
+    echo "Usage: $0 [OPTION]... [COMMAND]"
+    echo "     -v              Be verbose"
+    echo "     -h              This message"
+    echo
+    echo "Commands:"
+    echo "     start           Start all PLC subsystems"
+    echo "     stop            Stop all PLC subsystems"
+    echo "     reload          Regenerate configuration files"
+    echo "     restart         Restart all PLC subsystems"
+    exit 1
+}
+
+# Get options
+while getopts "vh" opt ; do
+    case $opt in
+       v)
+           verbose=1
+           set -x
+           ;;
+       h|*)
+           usage
+           ;;
+    esac
+done
+
+shift $(($OPTIND - 1))
+if [ -z "$1" ] ; then
+    usage
+fi
+
+exec 3>&1
+exec 4>&2
+if [ $verbose -eq 0 ] ; then
+    exec 1>/dev/null
+    exec 2>/dev/null
+fi
+
+# Generate and load configuration
+reload
+. /etc/planetlab/plc_config
+
+RETVAL=0
+
+start ()
+{
+    for step in "${steps[@]}" ; do
+       echo -n $"PLC: Starting $step: " >&3
+       RETVAL=0
+       config_$step start
+       if [ $RETVAL -eq 0 ] ; then
+           success $"PLC: $step startup" >&3
+       else
+           failure $"PLC: $step startup" >&3
+       fi
+       echo >&3
+    done
+}
+
+stop ()
+{
+    for i in $(seq 1 $nsteps) ; do
+       step=${steps[$(($nsteps - $i))]}
+       echo -n $"PLC: Shutting down $step: " >&3
+       RETVAL=0
+       config_$step stop
+       if [ $RETVAL -eq 0 ] ; then
+           success $"PLC: $step shutdown" >&3
+       else
+           failure $"PLC: $step shutdown" >&3
+       fi
+       echo >&3
+    done
+}
+
+case "$1" in
+    start|stop)
+       $1
+       ;;
+
+    restart)
+       stop
+       start
+       ;;
+
+    reload)
+       ;;
+
+    *)
+       usage >&3
+       ;;
+esac
+
+exit $RETVAL
diff --git a/host.init b/host.init
new file mode 100755 (executable)
index 0000000..0c101e2
--- /dev/null
+++ b/host.init
@@ -0,0 +1,106 @@
+#!/bin/bash
+#
+# plc  Manages all PLC services on this machine
+#
+# chkconfig: 2345 99 5
+#
+# description: Manages all PLC services on this machine
+#
+# $Id: plc.init,v 1.6 2005/04/24 19:48:11 mlhuang Exp $
+#
+
+PATH=/sbin:/bin:/usr/bin:/usr/sbin
+
+# Source function library.
+. /etc/init.d/functions
+
+# Source configuration
+if [ -f /etc/sysconfig/plc ] ; then
+    . /etc/sysconfig/plc
+fi
+
+RETVAL=0
+
+# Get options
+while getopts "vh" opt ; do
+    case $opt in
+       v)
+           verbose=1
+           set -x
+           ;;
+       h|*)
+           usage
+           ;;
+    esac
+done
+
+start ()
+{
+    echo -n $"Mounting PLC: "
+
+    if ! grep -q $PLC_ROOT.img /proc/mounts ; then
+       if ! e2fsck -a $PLC_ROOT.img | logger -t "PLC" ; then
+           e2fsck $PLC_ROOT.img
+       fi
+       mount -o loop $PLC_ROOT.img $PLC_ROOT
+       RETVAL=$(($RETVAL+$?))
+    fi
+    if ! grep -q $PLC_DATA /proc/mounts ; then
+       mount -t none -o bind,rw $PLC_DATA $PLC_ROOT/data
+       RETVAL=$(($RETVAL+$?))
+    fi
+    if ! grep -q $PLC_ROOT/proc /proc/mounts ; then
+       mount -t proc none $PLC_ROOT/proc
+       RETVAL=$(($RETVAL+$?))
+    fi
+
+    if [ $RETVAL -eq 0 ]; then
+       success $"PLC mount"
+    else
+       failure $"PLC mount"
+    fi
+    echo
+
+    chroot $PLC_ROOT /sbin/service plc $PLC_OPTIONS start
+    RETVAL=$?
+}
+
+stop ()
+{
+    chroot $PLC_ROOT /sbin/service plc $PLC_OPTIONS stop
+
+    echo -n $"Unmounting PLC: "
+
+    umount $PLC_ROOT/proc
+    RETVAL=$(($RETVAL+$?))
+    umount $PLC_ROOT/data
+    RETVAL=$(($RETVAL+$?))
+    umount $PLC_ROOT
+    RETVAL=$(($RETVAL+$?))
+
+    if [ $RETVAL -eq 0 ]; then
+       success $"PLC unmount"
+    else
+       failure $"PLC unmount"  
+    fi
+    echo
+}
+
+restart ()
+{
+    stop
+    start
+}
+
+case "$1" in
+    start|stop|restart)
+       $1
+       ;;
+
+    *)
+       echo "Usage: $0 {start|stop|restart}"
+       RETVAL=1
+       ;;
+esac
+
+exit $RETVAL
diff --git a/myplc.spec b/myplc.spec
new file mode 100644 (file)
index 0000000..131e079
--- /dev/null
@@ -0,0 +1,85 @@
+# Fedora Core release version to base the installation on. Currently
+# supported: 2, 4.
+%define releasever 2
+
+Vendor: PlanetLab
+Packager: PlanetLab Central <support@planet-lab.org>
+Distribution: PlanetLab 3.0
+URL: http://cvs.planet-lab.org/cvs/myplc
+
+Summary: PlanetLab Central (PLC) Portable Installation
+Name: myplc
+Version: %{releasever}.0
+Release: 1
+License: BSD
+Group: Applications/Systems
+Source0: %{name}-%{version}.tar.gz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
+
+%define debug_package %{nil}
+
+%description
+MyPLC is a complete PlanetLab Central (PLC) portable installation
+contained within a chroot jail. The default installation consists of a
+web server, an XML-RPC API server, a boot server, and a database
+server: the core components of PLC. The installation may be customized
+through a graphical interface. All PLC services are started up and
+shut down through a single System V init script installed in the host
+system.
+
+%prep
+%setup -q
+
+%build
+cd myplc
+./build.sh -r %{releasever} -d %{_datadir}
+
+# If run under sudo, allow user to delete the build directory
+if [ -n "$SUDO_USER" ] ; then
+    chown -R $SUDO_USER .
+    # Some temporary chroot files like /var/empty/sshd and
+    # /usr/bin/sudo get created with non-readable permissions.
+    find . -not -perm +0600 -exec chmod u+rw {} \;
+fi
+
+%install
+rm -rf $RPM_BUILD_ROOT
+
+cd myplc
+install -d -m 755 $RPM_BUILD_ROOT/%{_datadir}/plc/fc%{releasever}
+install -D -m 644 fc%{releasever}.img $RPM_BUILD_ROOT/%{_datadir}/plc/fc%{releasever}.img
+find data%{releasever} | cpio -p -d -u $RPM_BUILD_ROOT/%{_datadir}/plc/
+install -D -m 755 host.init $RPM_BUILD_ROOT/%{_sysconfdir}/init.d/plc
+install -D -m 644 plc.sysconfig $RPM_BUILD_ROOT/%{_sysconfdir}/sysconfig/plc
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+# If run under sudo, allow user to delete the built RPM
+if [ -n "$SUDO_USER" ] ; then
+    chown $SUDO_USER %{_rpmdir}/%{_arch}/%{name}-%{version}-%{release}.%{_arch}.rpm
+fi
+
+%post
+chkconfig --add plc
+chkconfig plc on
+
+%preun
+# 0 = erase, 1 = upgrade
+if [ $1 -eq 0 ] ; then
+    chkconfig plc off
+    chkconfig --del plc
+fi
+
+%files
+%defattr(-,root,root,-)
+%dir %{_datadir}/plc/fc%{releasever}
+%{_datadir}/plc/fc%{releasever}.img
+%{_datadir}/plc/data%{releasever}
+%{_sysconfdir}/init.d/plc
+%{_sysconfdir}/sysconfig/plc
+
+%changelog
+* Fri Mar 17 2006 Mark Huang <mlhuang@CS.Princeton.EDU> - 0.1-1
+- Initial build.
+
diff --git a/plc-config b/plc-config
new file mode 100755 (executable)
index 0000000..840ee46
--- /dev/null
@@ -0,0 +1,151 @@
+#!/usr/bin/python
+#
+# Script for basic access to the PlanetLab Central (PLC) configuration
+# file store.
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import sys
+import os
+import fcntl
+import getopt
+import signal
+from plc_config import PLCConfiguration
+
+
+def usage():
+    print """
+Script to access the PLC configuration file store.
+    
+Usage: %s [OPTION]... [FILES]
+        Conversion:
+
+        --shell         Output shell configuration file
+        --python        Output Python configuration file
+        --php           Output PHP configuration file
+
+        Information:
+
+        --variables     List names of all variables
+        --packages      List names of all packages
+        --comps         List comps.xml from configuration
+
+        Basic variable value manipulation:
+
+        --category=     Category identifier
+        --variable=     Variable identifier
+        --value=        Variable value
+
+        Basic package list manipulation:
+
+        --group=        Package group identifier
+        --package=      Package name
+        --type=         Package type
+
+        Miscellaneous:
+
+        -h, --help      This message
+        -s, --save      Save changes to first configuration file
+""".lstrip() % sys.argv[0]
+    sys.exit(1)
+
+
+def main():
+    plc = PLCConfiguration()
+    fileobjs = []
+    output = None
+    category = {}
+    variable = {}
+    group = {}
+    package = {}
+    save = False
+
+    # Standard options
+    shortopts = "hs"
+    longopts = ["shell", "bash", "python",
+                "php",
+                "xml",
+                "variables",
+                "packages",
+                "comps",
+                "category=", "variable=", "value=",
+                "group=", "package=", "type=",
+                "help",
+                "save"]
+
+    try:
+        (opts, argv) = getopt.gnu_getopt(sys.argv[1:], shortopts, longopts)
+    except Exception, err:
+        sys.stderr.write("Error: " + str(err) + os.linesep)
+        sys.exit(1)
+
+    for (opt, optval) in opts:
+        if opt == "--shell" or \
+             opt == "--bash" or \
+             opt == "--python":
+            output = plc.output_shell
+        elif opt == "--php":
+            output = plc.output_php
+        elif opt == "--xml":
+            output = plc.output_xml
+        elif opt == "--variables":
+            output = plc.output_variables
+        elif opt == "--packages":
+            output = plc.output_packages
+        elif opt == "--comps":
+            output = plc.output_comps
+        elif opt == "--category":
+            category['id'] = optval
+        elif opt == "--variable":
+            variable['id'] = optval
+        elif opt == "--value":
+            variable['value'] = optval
+        elif opt == "--group":
+            group['id'] = optval
+        elif opt == "--package":
+            package['name'] = optval
+        elif opt == "--type":
+            package['type'] = optval
+        elif opt == '-s' or opt == "--save":
+            save = True
+        elif opt == '-h' or opt == "--help":
+            usage()
+
+    # Try the default
+    if not argv:
+        argv = ["/etc/planetlab/plc_config.xml"]
+
+    # Merge all files
+    for file in argv:
+        try:
+            plc.load(file)
+        except IOError:
+            pass
+        except Exception, err:
+            sys.stderr.write("Error: %s: %s" % (file, str(err)) + os.linesep)
+            sys.exit(1)
+
+    # --category, --variable, --value
+    if category.has_key('id') and variable.has_key('id'):
+        if variable.has_key('value'):
+            plc.set(category, variable)
+        else:
+            (category, variable) = plc.get(category['id'], variable['id'])
+            if variable.has_key('value'):
+                print variable['value']
+
+    # --shell, --php, --xml
+    if output is not None:
+        sys.stdout.write(output())
+
+    # --save
+    if save:
+        plc.save()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/plc_config.py b/plc_config.py
new file mode 100644 (file)
index 0000000..b3ae90d
--- /dev/null
@@ -0,0 +1,773 @@
+#!/usr/bin/python
+#
+# Merge PlanetLab Central (PLC) configuration files into a variety of
+# output formats. These files represent the global configuration for a
+# PLC installation.
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# Copyright (C) 2006 The Trustees of Princeton University
+#
+# $Id$
+#
+
+import xml.dom.minidom
+from StringIO import StringIO
+import time
+import re
+import textwrap
+import codecs
+import os
+import types
+
+
+class PLCConfiguration:
+    """
+    Configuration file store. Optionally instantiate with a file path
+    or object:
+
+    plc = PLCConfiguration()
+    plc = PLCConfiguration(fileobj)
+    plc = PLCConfiguration("/etc/planetlab/plc_config.xml")
+
+    You may load() additional files later, which will be merged into
+    the current configuration:
+
+    plc.load("/etc/planetlab/local.xml")
+
+    You may also save() the configuration. If a file path or object is
+    not specified, the configuration will be written to the file path
+    or object that was first loaded.
+    
+    plc.save()
+    plc.save("/etc/planetlab/plc_config.xml")
+    """
+
+    def __init__(self, file = None):
+        impl = xml.dom.minidom.getDOMImplementation()
+        self._dom = impl.createDocument(None, "configuration", None)
+        self._variables = {}
+        self._packages = {}
+        self._files = []
+
+        if file is not None:
+            self.load(file)
+
+
+    def _get_text(self, node):
+        """
+        Get the text of a text node.
+        """
+
+        if node.firstChild and \
+           node.firstChild.nodeType == node.TEXT_NODE:
+            if node.firstChild.data is None:
+                # Interpret simple presence of node as "", not NULL
+                return ""
+            else:
+                return node.firstChild.data
+
+        return None
+
+
+    def _get_text_of_child(self, parent, name):
+        """
+        Get the text of a (direct) child text node.
+        """
+
+        for node in parent.childNodes:
+            if node.nodeType == node.ELEMENT_NODE and \
+               node.tagName == name:
+                return self._get_text(node)
+
+        return None
+
+
+    def _set_text(self, node, data):
+        """
+        Set the text of a text node.
+        """
+
+        if node.firstChild and \
+           node.firstChild.nodeType == node.TEXT_NODE:
+            if data is None:
+                node.removeChild(node.firstChild)
+            else:
+                node.firstChild.data = data
+        elif data is not None:
+            text = TrimText()
+            text.data = data
+            node.appendChild(text)
+
+
+    def _set_text_of_child(self, parent, name, data):
+        """
+        Set the text of a (direct) child text node.
+        """
+
+        for node in parent.childNodes:
+            if node.nodeType == node.ELEMENT_NODE and \
+               node.tagName == name:
+                self._set_text(node, data)
+                return
+
+        child = TrimTextElement(name)
+        self._set_text(child, data)
+        parent.appendChild(child)
+
+
+    def _category_element_to_dict(self, category_element):
+        """
+        Turn a <category> element into a dictionary of its attributes
+        and child text nodes.
+        """
+
+        category = {}
+        category['id'] = category_element.getAttribute('id').lower()
+        for node in category_element.childNodes:
+            if node.nodeType == node.ELEMENT_NODE and \
+               node.tagName in ['name', 'description']:
+                category[node.tagName] = self._get_text_of_child(category_element, node.tagName)
+        category['element'] = category_element
+
+        return category
+
+
+    def _variable_element_to_dict(self, variable_element):
+        """
+        Turn a <variable> element into a dictionary of its attributes
+        and child text nodes.
+        """
+
+        variable = {}
+        variable['id'] = variable_element.getAttribute('id').lower()
+        if variable_element.hasAttribute('type'):
+            variable['type'] = variable_element.getAttribute('type')
+        for node in variable_element.childNodes:
+            if node.nodeType == node.ELEMENT_NODE and \
+               node.tagName in ['name', 'value', 'description']:
+                variable[node.tagName] = self._get_text_of_child(variable_element, node.tagName)
+        variable['element'] = variable_element
+
+        return variable
+
+
+    def _group_element_to_dict(self, group_element):
+        """
+        Turn a <group> element into a dictionary of its attributes
+        and child text nodes.
+        """
+
+        group = {}
+        for node in group_element.childNodes:
+            if node.nodeType == node.ELEMENT_NODE and \
+               node.tagName in ['id', 'name', 'default', 'description', 'uservisible']:
+                group[node.tagName] = self._get_text_of_child(group_element, node.tagName)
+        group['element'] = group_element
+
+        return group
+
+
+    def _packagereq_element_to_dict(self, packagereq_element):
+        """
+        Turns a <packagereq> element into a dictionary of its attributes
+        and child text nodes.
+        """
+
+        package = {}
+        if packagereq_element.hasAttribute('type'):
+            package['type'] = packagereq_element.getAttribute('type')
+        package['name'] = self._get_text(packagereq_element)
+        package['element'] = packagereq_element
+
+        return package
+
+
+    def load(self, file = "/etc/planetlab/plc_config.xml"):
+        """
+        Merge file into configuration store.
+        """
+
+        dom = xml.dom.minidom.parse(file)
+        if type(file) in types.StringTypes:
+            self._files.append(os.path.abspath(file))
+
+        # Parse <variables> section
+        for variables_element in dom.getElementsByTagName('variables'):
+            for category_element in variables_element.getElementsByTagName('category'):
+                category = self._category_element_to_dict(category_element)
+                self.set(category, None)
+
+                for variablelist_element in category_element.getElementsByTagName('variablelist'):
+                    for variable_element in variablelist_element.getElementsByTagName('variable'):
+                        variable = self._variable_element_to_dict(variable_element)
+                        self.set(category, variable)
+
+        # Parse <comps> section
+        for comps_element in dom.getElementsByTagName('comps'):
+            for group_element in comps_element.getElementsByTagName('group'):
+                group = self._group_element_to_dict(group_element)
+                self.add_package(group, None)
+
+                for packagereq_element in group_element.getElementsByTagName('packagereq'):
+                    package = self._packagereq_element_to_dict(packagereq_element)
+                    self.add_package(group, package)
+
+
+    def save(self, file = None):
+        """
+        Write configuration store to file.
+        """
+
+        if file is None:
+            if self._files:
+                file = self._files[0]
+            else:
+                file = "/etc/planetlab/plc_config.xml"
+
+        if type(file) in types.StringTypes:
+            fileobj = open(file, 'r+')
+        else:
+            fileobj = file
+
+        fileobj.seek(0)
+        fileobj.write(self.output_xml())
+        fileobj.truncate()
+
+        fileobj.close()
+
+
+    def get(self, category_id, variable_id):
+        """
+        Get the specified variable in the specified category.
+
+        Arguments:
+
+        category_id = unique category identifier (e.g., 'plc_www')
+        variable_id = unique variable identifier (e.g., 'port')
+
+        Returns:
+
+        variable = { 'id': "variable_identifier",
+                     'type': "variable_type",
+                     'value': "variable_value",
+                     'name': "Variable name",
+                     'description': "Variable description" }
+        """
+
+        if self._variables.has_key(category_id.lower()):
+            (category, variables) = self._variables[category_id]
+            if variables.has_key(variable_id.lower()):
+                variable = variables[variable_id]
+            else:
+                variable = None
+        else:
+            category = None
+            variable = None
+
+        return (category, variable)
+
+
+    def delete(self, category_id, variable_id):
+        """
+        Delete the specified variable from the specified category. If
+        variable_id is None, deletes all variables from the specified
+        category as well as the category itself.
+
+        Arguments:
+
+        category_id = unique category identifier (e.g., 'plc_www')
+        variable_id = unique variable identifier (e.g., 'port')
+        """
+
+        if self._variables.has_key(category_id.lower()):
+            (category, variables) = self._variables[category_id]
+            if variable_id is None:
+                category['element'].parentNode.removeChild(category['element'])
+                del self._variables[category_id]
+            elif variables.has_key(variable_id.lower()):
+                variable = variables[variable_id]
+                variable['element'].parentNode.removeChild(variable['element'])
+                del variables[variable_id]
+
+
+    def set(self, category, variable):
+        """
+        Add and/or update the specified variable. The 'id' fields are
+        mandatory. If a field is not specified and the category and/or
+        variable already exists, the field will not be updated. If
+        'variable' is None, only adds and/or updates the specified
+        category.
+
+        Arguments:
+
+        category = { 'id': "category_identifier",
+                     'name': "Category name",
+                     'description': "Category description" }
+
+        variable = { 'id': "variable_identifier",
+                     'type': "variable_type",
+                     'value': "variable_value",
+                     'name': "Variable name",
+                     'description': "Variable description" }
+        """
+
+        if not category.has_key('id') or type(category['id']) not in types.StringTypes:
+            return
+        
+        category_id = category['id'].lower()
+
+        if self._variables.has_key(category_id):
+            # Existing category
+            (old_category, variables) = self._variables[category_id]
+
+            # Merge category attributes
+            for tag in ['name', 'description']:
+                if category.has_key(tag):
+                    old_category[tag] = category[tag]
+                    self._set_text_of_child(old_category['element'], tag, category[tag])
+
+            category_element = old_category['element']
+        else:
+            # Merge into DOM
+            category_element = self._dom.createElement('category')
+            category_element.setAttribute('id', category_id)
+            for tag in ['name', 'description']:
+                if category.has_key(tag):
+                    self._set_text_of_child(category_element, tag, category[tag])
+
+            if self._dom.documentElement.getElementsByTagName('variables'):
+                variables_element = self._dom.documentElement.getElementsByTagName('variables')[0]
+            else:
+                variables_element = self._dom.createElement('variables')
+                self._dom.documentElement.appendChild(variables_element)
+            variables_element.appendChild(category_element)
+
+            # Cache it
+            category['element'] = category_element
+            variables = {}
+            self._variables[category_id] = (category, variables)
+
+        if variable is None or not variable.has_key('id') or type(variable['id']) not in types.StringTypes:
+            return
+
+        variable_id = variable['id'].lower()
+
+        if variables.has_key(variable_id):
+            # Existing variable
+            old_variable = variables[variable_id]
+
+            # Merge variable attributes
+            for attribute in ['type']:
+                if variable.has_key(attribute):
+                    old_variable[attribute] = variable[attribute]
+                    old_variable['element'].setAttribute(attribute, variable[attribute])
+            for tag in ['name', 'value', 'description']:
+                if variable.has_key(tag):
+                    old_variable[tag] = variable[tag]
+                    self._set_text_of_child(old_variable['element'], tag, variable[tag])
+        else:
+            # Merge into DOM
+            variable_element = self._dom.createElement('variable')
+            variable_element.setAttribute('id', variable_id)
+            for attribute in ['type']:
+                if variable.has_key(attribute):
+                    variable_element.setAttribute(attribute, variable[attribute])
+            for tag in ['name', 'value', 'description']:
+                if variable.has_key(tag):
+                    self._set_text_of_child(variable_element, tag, variable[tag])
+                
+            if category_element.getElementsByTagName('variablelist'):
+                variablelist_element = category_element.getElementsByTagName('variablelist')[0]
+            else:
+                variablelist_element = self._dom.createElement('variablelist')
+                category_element.appendChild(variablelist_element)
+            variablelist_element.appendChild(variable_element)
+
+            # Cache it
+            variable['element'] = variable_element
+            variables[variable_id] = variable
+
+
+    def get_package(self, group_id, package_name):
+        """
+        Get the specified package in the specified package group.
+
+        Arguments:
+
+        group_id - unique group id (e.g., 'plc')
+        package_name - unique package name (e.g., 'postgresql')
+
+        Returns:
+
+        package = { 'name': "package_name",
+                    'type': "mandatory|optional" }
+        """
+
+        if self._packages.has_key(group_id.lower()):
+            (group, packages) = self._packages[group_id]
+            if packages.has_key(package_name):
+                package = packages[package_name]
+            else:
+                package = None
+        else:
+            group = None
+            package = None
+
+        return (group, package)
+
+
+    def delete_package(self, group_id, package_name):
+        """
+        Deletes the specified variable from the specified category. If
+        variable_id is None, deletes all variables from the specified
+        category as well as the category itself.
+
+        Arguments:
+
+        group_id - unique group id (e.g., 'plc')
+        package_name - unique package name (e.g., 'postgresql')
+        """
+
+        if self._packages.has_key(group_id):
+            (group, packages) = self._packages[group_id]
+            if package_name is None:
+                group['element'].parentNode.removeChild(group['element'])
+                del self._packages[group_id]
+            elif packages.has_key(package_name.lower()):
+                package = packages[package_name]
+                package['element'].parentNode.removeChild(package['element'])
+                del packages[package_name]
+
+
+    def add_package(self, group, package):
+        """
+        Add and/or update the specified package. The 'id' and 'name'
+        fields are mandatory. If a field is not specified and the
+        package or group already exists, the field will not be
+        updated. If package is None, only adds/or updates the
+        specified group.
+
+        Arguments:
+
+        group = { 'id': "group_identifier",
+                  'name': "Group name",
+                  'default': "true|false",
+                  'description': "Group description",
+                  'uservisible': "true|false" }
+
+        package = { 'name': "package_name",
+                    'type': "mandatory|optional" }
+        """
+
+        if not group.has_key('id'):
+            return
+
+        group_id = group['id']
+
+        if self._packages.has_key(group_id):
+            # Existing group
+            (old_group, packages) = self._packages[group_id]
+
+            # Merge group attributes
+            for tag in ['id', 'name', 'default', 'description', 'uservisible']:
+                if group.has_key(tag):
+                    old_group[tag] = group[tag]
+                    self._set_text_of_child(old_group['element'], tag, group[tag])
+
+            group_element = old_group['element']
+        else:
+            # Merge into DOM
+            group_element = self._dom.createElement('group')
+            for tag in ['id', 'name', 'default', 'description', 'uservisible']:
+                if group.has_key(tag):
+                    self._set_text_of_child(group_element, tag, group[tag])
+
+            if self._dom.documentElement.getElementsByTagName('comps'):
+                comps_element = self._dom.documentElement.getElementsByTagName('comps')[0]
+            else:
+                comps_element = self._dom.createElement('comps')
+                self._dom.documentElement.appendChild(comps_element)
+            comps_element.appendChild(group_element)
+
+            # Cache it
+            group['element'] = group_element
+            packages = {}
+            self._packages[group_id] = (group, packages)
+
+        if package is None or not package.has_key('name'):
+            return
+
+        package_name = package['name']
+        if packages.has_key(package_name):
+            # Existing package
+            old_package = packages[package_name]
+
+            # Merge variable attributes
+            for attribute in ['type']:
+                if package.has_key(attribute):
+                    old_package[attribute] = package[attribute]
+                    old_package['element'].setAttribute(attribute, package[attribute])
+        else:
+            # Merge into DOM
+            packagereq_element = TrimTextElement('packagereq')
+            self._set_text(packagereq_element, package_name)
+            for attribute in ['type']:
+                if package.has_key(attribute):
+                    packagereq_element.setAttribute(attribute, package[attribute])
+                
+            if group_element.getElementsByTagName('packagelist'):
+                packagelist_element = group_element.getElementsByTagName('packagelist')[0]
+            else:
+                packagelist_element = self._dom.createElement('packagelist')
+                group_element.appendChild(packagelist_element)
+            packagelist_element.appendChild(packagereq_element)
+
+            # Cache it
+            package['element'] = packagereq_element
+            packages[package_name] = package
+
+
+    def variables(self):
+        """
+        Return all variables.
+
+        Returns:
+
+        variables = { 'category_id': (category, variablelist) }
+
+        category = { 'id': "category_identifier",
+                     'name': "Category name",
+                     'description': "Category description" }
+
+        variablelist = { 'variable_id': variable }
+
+        variable = { 'id': "variable_identifier",
+                     'type': "variable_type",
+                     'value': "variable_value",
+                     'name': "Variable name",
+                     'description': "Variable description" }
+        """
+
+        return self._variables
+
+
+    def packages(self):
+        """
+        Return all packages.
+
+        Returns:
+
+        packages = { 'group_id': (group, packagelist) }
+
+        group = { 'id': "group_identifier",
+                  'name': "Group name",
+                  'default': "true|false",
+                  'description': "Group description",
+                  'uservisible': "true|false" }
+
+        packagelist = { 'package_name': package }
+
+        package = { 'name': "package_name",
+                    'type': "mandatory|optional" }
+        """
+
+        return self._packages
+
+
+    def _sanitize_variable(self, category_id, variable):
+        assert variable.has_key('id')
+        # Prepend variable name with category label
+        id = category_id + "_" + variable['id']
+        # And uppercase it
+        id = id.upper()
+
+        if variable.has_key('type'):
+            type = variable['type']
+        else:
+            type = None
+
+        if variable.has_key('name'):
+            name = variable['name']
+        else:
+            name = None
+
+        if variable.has_key('value') and variable['value'] is not None:
+            value = variable['value']
+            if type == "int" or type == "double":
+                # bash, Python, and PHP do not require that numbers be quoted
+                pass
+            elif type == "boolean":
+                # bash, Python, and PHP can all agree on 0 and 1
+                if value == "true":
+                    value = "1"
+                else:
+                    value = "0"
+            else:
+                # bash, Python, and PHP all support strong single quoting
+                value = "'" + value.replace("'", "\\'") + "'"
+        else:
+            value = None
+
+        if variable.has_key('description') and variable['description'] is not None:
+            description = variable['description']
+            # Collapse consecutive whitespace
+            description = re.sub(r'\s+', ' ', description)
+            # Wrap comments at 70 columns
+            wrapper = textwrap.TextWrapper()
+            comments = wrapper.wrap(description)
+        else:
+            comments = None
+
+        return (id, name, value, comments)
+
+
+    def _header(self):
+        header = """
+DO NOT EDIT. This file was automatically generated at
+%s from:
+
+%s
+""" % (time.asctime(), os.linesep.join(self._files))
+
+        # Get rid of the surrounding newlines
+        return header.strip().split(os.linesep)
+
+
+    def output_shell(self, encoding = "utf-8"):
+        """
+        Return variables as a shell script.
+        """
+
+        buf = codecs.lookup(encoding)[3](StringIO())
+        buf.writelines(["# " + line + os.linesep for line in self._header()])
+
+        for (category_id, (category, variables)) in self._variables.iteritems():
+            for variable in variables.values():
+                (id, name, value, comments) = self._sanitize_variable(category_id, variable)
+                buf.write(os.linesep)
+                if name is not None:
+                    buf.write("# " + name + os.linesep)
+                if comments is not None:
+                    buf.writelines(["# " + line + os.linesep for line in comments])
+                # bash does not have the concept of NULL
+                if value is not None:
+                    buf.write(id + "=" + value + os.linesep)
+
+        return buf.getvalue()
+
+
+    def output_php(self, encoding = "utf-8"):
+        """
+        Return variables as a PHP script.
+        """
+
+        buf = codecs.lookup(encoding)[3](StringIO())
+        buf.write("<?php" + os.linesep)
+        buf.writelines(["// " + line + os.linesep for line in self._header()])
+
+        for (category_id, (category, variables)) in self._variables.iteritems():
+            for variable in variables.values():
+                (id, name, value, comments) = self._sanitize_variable(category_id, variable)
+                buf.write(os.linesep)
+                if name is not None:
+                    buf.write("// " + name + os.linesep)
+                if comments is not None:
+                    buf.writelines(["// " + line + os.linesep for line in comments])
+                if value is None:
+                    value = 'NULL'
+                buf.write("DEFINE('%s', %s);" % (id, value) + os.linesep)
+
+        buf.write("?>" + os.linesep)
+
+        return buf.getvalue()
+
+
+    def output_xml(self, encoding = "utf-8"):
+        """
+        Return variables in original XML format.
+        """
+
+        buf = codecs.lookup(encoding)[3](StringIO())
+        self._dom.writexml(buf, addindent = "  ", indent = "", newl = "\n", encoding = encoding)
+
+        return buf.getvalue()
+
+
+    def output_variables(self, encoding = "utf-8"):
+        """
+        Return list of all variable names.
+        """
+
+        buf = codecs.lookup(encoding)[3](StringIO())
+
+        for (category_id, (category, variables)) in self._variables.iteritems():
+            for variable in variables.values():
+                (id, name, value, comments) = self._sanitize_variable(category_id, variable)
+                buf.write(id + os.linesep)
+
+        return buf.getvalue()
+
+
+    def output_packages(self, encoding = "utf-8"):
+        """
+        Return list of all packages.
+        """
+
+        buf = codecs.lookup(encoding)[3](StringIO())
+
+        for (group, packages) in self._packages.values():
+            buf.write(os.linesep.join(packages.keys()))
+
+        if buf.tell():
+            buf.write(os.linesep)
+
+        return buf.getvalue()
+
+
+    def output_comps(self, encoding = "utf-8"):
+        """
+        Return <comps> section of configuration.
+        """
+
+        if self._dom is None or \
+           not self._dom.getElementsByTagName("comps"):
+            return
+        comps = self._dom.getElementsByTagName("comps")[0]
+
+        impl = xml.dom.minidom.getDOMImplementation()
+        doc = impl.createDocument(None, "comps", None)
+
+        buf = codecs.lookup(encoding)[3](StringIO())
+
+        # Pop it off the DOM temporarily
+        parent = comps.parentNode
+        parent.removeChild(comps)
+
+        doc.replaceChild(comps, doc.documentElement)
+        doc.writexml(buf, encoding = encoding)
+
+        # Put it back
+        parent.appendChild(comps)
+
+        return buf.getvalue()
+
+
+# xml.dom.minidom.Text.writexml adds surrounding whitespace to textual
+# data when pretty-printing. Override this behavior.
+class TrimText(xml.dom.minidom.Text):
+    def writexml(self, writer, indent="", addindent="", newl=""):
+        xml.dom.minidom.Text.writexml(self, writer, "", "", "")
+
+
+class TrimTextElement(xml.dom.minidom.Element):
+    def writexml(self, writer, indent="", addindent="", newl=""):
+        writer.write(indent)
+        xml.dom.minidom.Element.writexml(self, writer, "", "", "")
+        writer.write(newl)
+
+
+if __name__ == '__main__':
+    import sys
+    if len(sys.argv) > 1 and sys.argv[1] in ['build', 'install']:
+        from distutils.core import setup
+        setup(py_modules=["plc_config"])
diff --git a/plc_config.xml b/plc_config.xml
new file mode 100644 (file)
index 0000000..7c85cf3
--- /dev/null
@@ -0,0 +1,518 @@
+<?xml version="1.0"?>
+<!DOCTYPE configuration PUBLIC "-//PlanetLab Central//DTD PLC configuration//EN" "configuration.dtd">
+
+<configuration>
+  <variables>
+    <category id="plc">
+      <name>System</name>
+      <description>Basic system variables. Be sure that the values of
+      these variables are the same across all machines in your
+      installation.</description>
+
+      <variablelist>
+       <variable id="name" type="string">
+         <name>Name</name>
+         <value>PlanetLab Test</value>
+         <description>The name of this PLC installation. It is used in
+         the name of the default system site (e.g., PlanetLab Central)
+         and in the names of various administrative entities (e.g.,
+         PlanetLab Support).</description>
+       </variable>
+
+       <variable id="slice_prefix" type="string">
+         <name>Slice Prefix</name>
+         <value>pl</value>
+         <description>The abbreviated name of this PLC
+         installation. It is used as the prefix for system slices
+         (e.g., pl_conf). Warning: Currently, this variable should
+         not be changed once set.</description>
+       </variable>
+
+       <variable id="root_user" type="password">
+         <name>Root Account</name>
+         <value>root@test.planet-lab.org</value>
+         <description>The name of the initial administrative
+         account. We recommend that this account be used only to create
+         additional accounts associated with real
+         administrators, then disabled.</description>
+       </variable>
+
+       <variable id="root_password" type="password">
+         <name>Root Password</name>
+         <value>root</value>
+         <description>The password of the initial administrative
+         account. Also the password of the root account on the Boot
+         CD.</description>
+       </variable>
+
+       <!-- The following are not actually meant to be configurable
+            as variables. The web interface should allow the file to
+            be downloaded, or its contents replaced by a file upload,
+            but the actual <value> shouldn't need to be changed.  -->
+
+       <variable id="root_ssh_key_pub" type="file">
+         <name>Root SSH Public Key</name>
+         <value>/etc/planetlab/root_ssh_key.pub</value>
+         <description>The SSH public key used to access the root
+         account on your nodes.</description>
+       </variable>
+
+       <variable id="root_ssh_key" type="file">
+         <name>Root SSH Private Key</name>
+         <value>/etc/planetlab/root_ssh_key.rsa</value>
+         <description>The SSH private key used to access the root
+         account on your nodes.</description>
+       </variable>
+
+       <variable id="debug_ssh_key_pub" type="file">
+         <name>Debug SSH Public Key</name>
+         <value>/etc/planetlab/debug_ssh_key.pub</value>
+         <description>The SSH public key used to access the root
+         account on your nodes when they are in Debug mode.</description>
+       </variable>
+
+       <variable id="debug_ssh_key" type="file">
+         <name>Debug SSH Private Key</name>
+         <value>/etc/planetlab/debug_ssh_key.rsa</value>
+         <description>The SSH private key used to access the root
+         account on your nodes when they are in Debug mode.</description>
+       </variable>
+
+       <variable id="root_gpg_key_pub" type="file">
+         <name>Root GPG Public Keyring</name>
+         <value>/etc/planetlab/pubring.gpg</value>
+         <description>The GPG public keyring used to sign the Boot
+         Manager and all node packages.</description>
+       </variable>
+
+       <variable id="root_gpg_key" type="file">
+         <name>Root GPG Private Keyring</name>
+         <value>/etc/planetlab/secring.gpg</value>
+         <description>The SSH private key used to access the root
+         account on your nodes.</description>
+       </variable>
+      </variablelist>
+    </category>
+
+    <category id="plc_net">
+      <name>Network</name>
+      <description>Network environment.</description>
+
+      <variablelist>
+       <variable id="dns1" type="ip">
+         <name>Primary DNS Server</name>
+         <value>128.112.136.10</value>
+         <description>Primary DNS server address.</description>
+       </variable>
+
+       <variable id="dns2" type="ip">
+         <name>Secondary DNS Server</name>
+         <value>128.112.136.12</value>
+         <description>Secondary DNS server address.</description>
+       </variable>
+      </variablelist>
+    </category>
+
+    <category id="plc_mail">
+      <name>Mail</name>
+      <description>Many maintenance scripts, as well as the API and
+      web site themselves, send e-mail notifications and
+      warnings.</description>
+
+      <variablelist>
+       <variable id="enabled" type="boolean">
+         <name>Enable Mail</name>
+         <value>false</value>
+         <description>Set to false to suppress all e-mail notifications
+         and warnings.</description>
+       </variable>
+
+       <variable id="support_address">
+         <name>Support Address</name>
+         <value>root@localhost</value>
+         <description>This address is used for support
+         requests. Support requests may include traffic complaints,
+         security incident reporting, web site malfunctions, and
+         general requests for information. We recommend that the
+         address be aliased to a ticketing system such as Request
+         Tracker.</description>
+       </variable>
+
+       <variable id="boot_address">
+         <name>Boot Messages Address</name>
+         <value>root@localhost</value>
+         <description>The API will notify this address when a problem
+         occurs during node installation or boot. If a domain is not
+         specified, the default system domain will be used
+         name.</description>
+       </variable>
+      </variablelist>
+    </category>
+
+    <category id="plc_db">
+      <name>Database Server</name>
+      <description>Database server definitions.</description>
+
+      <variablelist>
+       <variable id="enabled" type="boolean">
+         <name>Enabled</name>
+         <value>true</value>
+         <description>Enable the database server on this
+         machine.</description>
+       </variable>
+
+       <variable id="type" type="string">
+         <name>Type</name>
+         <value>postgresql</value>
+         <description>The type of database server. Currently, only
+         postgresql is supported.</description>
+       </variable>
+
+       <variable id="host" type="hostname">
+         <name>Hostname</name>
+         <value>localhost</value>
+         <description>The fully qualified hostname or IP address of
+         the database server. This hostname must be resolvable and
+         reachable by the rest of your installation.</description>
+       </variable>
+
+       <variable id="name" type="string">
+         <name>Database Name</name>
+         <value>planetlab3</value>
+         <description>The name of the database to access.</description>
+       </variable>
+
+       <variable id="user" type="string">
+         <name>Database Username</name>
+         <value>pgsqluser</value>
+         <description>The username to use when accessing the
+         database.</description>
+       </variable>
+
+       <variable id="password" type="password">
+         <name>Database Password</name>
+         <value></value>
+         <description>The password to use when accessing the
+         database. If left blank, one will be
+         generated.</description>
+       </variable>
+      </variablelist>
+    </category>
+
+    <category id="plc_api">
+      <name>API Server</name>
+      <description>API (XML-RPC) server definitions.</description>
+
+      <variablelist>
+       <variable id="enabled" type="boolean">
+         <name>Enabled</name>
+         <value>true</value>
+         <description>Enable the API server on this
+         machine.</description>
+       </variable>
+
+       <variable id="debug" type="boolean">
+         <name>Debug</name>
+         <value>false</value>
+         <description>Enable verbose API debugging. Do not enable on
+         a production system!</description>
+       </variable>
+
+       <variable id="host" type="hostname">
+         <name>Hostname</name>
+         <value>localhost</value>
+         <description>The fully qualified hostname or IP address of
+         the API server. This hostname must be resolvable and
+         reachable by the rest of your installation, as well as your
+         nodes.</description>
+       </variable>
+
+       <variable id="ssl_port" type="int">
+         <name>Port</name>
+         <value>80</value>
+         <description>The TCP port number through which the API
+         should be accessed. Warning: SSL (port 443) access is not
+         fully supported by the website code yet. We recommend that
+         port 80 be used for now and that the API server either run
+         on the same machine as the web server, or that they both be
+         on a secure wired network.</description>
+       </variable>
+
+       <variable id="path" type="string">
+         <name>Path</name>
+         <value>/PLCAPI/</value>
+         <description>The base path of the API URL.</description>
+       </variable>
+
+       <variable id="maintenance_user" type="string">
+         <name>Maintenance User</name>
+         <value>maint@test.planet-lab.org</value>
+         <description>The username of the maintenance account. This
+         account is used by local scripts that perform automated
+         tasks, and cannot be used for normal logins.</description>
+       </variable>
+
+       <variable id="maintenance_password" type="password">
+         <name>Maintenance Password</name>
+         <value></value>
+         <description>The password of the maintenance account. If
+         left blank, one will be generated. We recommend that the
+         password be changed periodically.</description>
+       </variable>
+
+       <variable id="maintenance_sources" type="hostname">
+         <name>Authorized Hosts</name>
+         <value></value>
+         <description>A space-separated list of IP addresses allowed
+         to access the API through the maintenance account. If left
+         blank, the API, web, and boot servers are
+         allowed.</description>
+       </variable>
+
+       <!-- The following are not actually meant to be configurable
+            as variables. The web interface should allow the file to
+            be downloaded, or its contents replaced by a file upload,
+            but the actual <value> shouldn't need to be changed.  -->
+
+       <variable id="ssl_crt" type="file">
+         <name>SSL Certificate</name>
+         <value>/etc/planetlab/api_ssl.crt</value>
+         <description>The signed SSL certificate to use for HTTPS
+         access. If not specified or non-existent, a self-signed
+         certificate will be generated.</description>
+       </variable>
+
+       <variable id="ssl_key" type="file">
+         <name>SSL Key</name>
+         <value>/etc/planetlab/api_ssl.key</value>
+         <description>The corresponding SSL private key. If not
+         specified or non-existent, a self-signed certificate will be
+         generated.</description>
+       </variable>
+
+       <variable id="ticket_key" type="file">
+         <name>Slice Ticket Private Key</name>
+         <value>/etc/planetlab/slice-ticket-key-nopass.pem</value>
+         <description>The private PEM key file used to sign slice
+         tickets.</description>
+       </variable>
+
+       <variable id="ticket_key_pub" type="file">
+         <name>Slice Ticket Public Key</name>
+         <value>/etc/planetlab/slice-ticket-key-public.pem</value>
+         <description>The public PEM key file used to verify signed
+         slice tickets.</description>
+       </variable>
+      </variablelist>
+    </category>
+
+    <category id="plc_www">
+      <name>Web Server</name>
+      <description>Web server definitions.</description>
+
+      <variablelist>
+       <variable id="enabled" type="boolean">
+         <name>Enabled</name>
+         <value>true</value>
+         <description>Enable the web server on this
+         machine.</description>
+       </variable>
+
+       <variable id="debug" type="boolean">
+         <name>Debug</name>
+         <value>false</value>
+         <description>Enable debugging output on web pages. Do not
+         enable on a production system!</description>
+       </variable>
+
+       <variable id="host" type="hostname">
+         <name>Hostname</name>
+         <value>localhost</value>
+         <description>The fully qualified hostname or IP address of
+         the web server. This hostname must be resolvable and
+         reachable by the rest of your installation, as well as your
+         nodes.</description>
+       </variable>
+
+       <variable id="port" type="int">
+         <name>Port</name>
+         <value>80</value>
+         <description>The TCP port number through which the
+         unprotected portions of the web site should be
+         accessed.</description>
+       </variable>
+
+       <variable id="ssl_port" type="int">
+         <name>SSL Port</name>
+         <value>443</value>
+         <description>The TCP port number through which the protected
+         portions of the web site should be accessed.</description>
+       </variable>
+
+       <!-- The following are not actually meant to be configurable
+            as variables. The web interface should allow the file to
+            be downloaded, or its contents replaced by a file upload,
+            but the actual <value> shouldn't need to be changed.  -->
+
+       <variable id="ssl_crt" type="file">
+         <name>SSL Certificate</name>
+         <value>/etc/planetlab/www_ssl.crt</value>
+         <description>The signed SSL certificate to use for HTTPS
+         access. If not specified or non-existent, a self-signed
+         certificate will be generated.</description>
+       </variable>
+
+       <variable id="ssl_key" type="file">
+         <name>SSL Key</name>
+         <value>/etc/planetlab/www_ssl.key</value>
+         <description>The corresponding SSL private key. If not
+         specified or non-existent, a self-signed certificate will be
+         generated.</description>
+       </variable>
+      </variablelist>
+    </category>
+
+    <category id="plc_boot">
+      <name>Boot Server</name>
+      <description>Boot server definitions. Multiple boot servers
+      may be brought up for load balancing, but we recommend that a
+      single DNS round-robin system be implemented so that the
+      following variables are the same across all of
+      them.</description>
+
+      <variablelist>
+       <variable id="enabled" type="boolean">
+         <name>Enabled</name>
+         <value>true</value>
+         <description>Enable the boot server on this
+         machine.</description>
+       </variable>
+
+       <variable id="host" type="hostname">
+         <name>Hostname</name>
+         <value>localhost</value>
+         <description>The fully qualified hostname or IP address of
+         the boot server. This hostname must be resolvable and
+         reachable by the rest of your installation, as well as your
+         nodes.</description>
+       </variable>
+
+       <variable id="port" type="int">
+         <name>Port</name>
+         <value>80</value>
+         <description>The TCP port number through which the
+         unprotected portions of the boot server should be
+         accessed.</description>
+       </variable>
+
+       <variable id="ssl_port" type="int">
+         <name>SSL Port</name>
+         <value>443</value>
+         <description>The TCP port number through which the protected
+         portions of the boot server should be
+         accessed.</description>
+       </variable>
+
+       <!-- The following are not actually meant to be configurable
+            as variables. The web interface should allow the file to
+            be downloaded, or its contents replaced by a file upload,
+            but the actual <value> shouldn't need to be changed.  -->
+
+       <variable id="ssl_crt" type="binary">
+         <name>SSL Certificate</name>
+         <value>/etc/planetlab/boot_ssl.crt</value>
+         <description>The signed SSL certificate to use for HTTPS
+         access. If not specified, or non-existent a self-signed
+         certificate will be generated.</description>
+       </variable>
+
+       <variable id="ssl_key" type="binary">
+         <name>SSL Key</name>
+         <value>/etc/planetlab/boot_ssl.key</value>
+         <description>The corresponding SSL private key. If not
+         specified or non-existent, a self-signed certificate will be
+         generated.</description>
+       </variable>
+      </variablelist>
+    </category>
+  </variables>
+
+  <comps>
+    <group>
+      <id>plc</id>
+      <name>PlanetLab Central</name>
+      <default>true</default>
+      <description>PlanetLab Central Packages</description>
+      <uservisible>true</uservisible>
+      <packagelist>
+       <!-- Sending mail -->
+       <packagereq type="mandatory">sendmail</packagereq>
+       <packagereq type="mandatory">sendmail-cf</packagereq>
+
+       <!-- (Optional) Synchronizing with PLC -->
+       <packagereq type="mandatory">rsync</packagereq>
+
+       <!-- Cron jobs -->
+       <packagereq type="mandatory">vixie-cron</packagereq>    
+
+       <!-- Other utilities -->
+       <packagereq type="mandatory">cvs</packagereq>
+       <packagereq type="mandatory">curl</packagereq>
+       <packagereq type="mandatory">wget</packagereq>
+
+       <!-- yum >=2.2 uses a new repository format -->
+       <packagereq type="mandatory">createrepo</packagereq>
+
+       <!-- For mkpasswd -->
+       <packagereq type="mandatory">expect</packagereq>
+
+       <!-- Almost all scripts are written in Python -->
+       <packagereq type="mandatory">python</packagereq>
+
+       <!-- For various Python scripts that access the API -->
+       <packagereq type="mandatory">plcapilib</packagereq>
+
+       <!-- Database server -->
+       <packagereq type="mandatory">postgresql</packagereq>
+       <packagereq type="mandatory">postgresql-server</packagereq>
+       <packagereq type="mandatory">postgresql-python</packagereq>
+
+       <!-- (Secure) web server -->
+       <packagereq type="mandatory">httpd</packagereq>
+       <packagereq type="mandatory">mod_ssl</packagereq>
+
+       <!-- Web pages are written primarily in PHP. A few pages still
+            access the DB directly. -->
+       <packagereq type="mandatory">php</packagereq>
+       <packagereq type="mandatory">php-pgsql</packagereq>
+       <packagereq type="mandatory">php-xmlrpc</packagereq>
+
+       <!-- Need GD for ImageCreate(), etc. -->
+       <packagereq type="mandatory">gd</packagereq>    
+       <packagereq type="mandatory">php-gd</packagereq>        
+
+       <!-- API server is implemented in mod_python -->
+       <packagereq type="mandatory">mod_python</packagereq>
+
+       <!-- API server uses a few non-standard packages -->
+       <packagereq type="mandatory">PyXML</packagereq>
+
+       <!-- API server uses SSL to sign tickets -->
+       <packagereq type="mandatory">xmlsec1</packagereq>
+       <packagereq type="mandatory">xmlsec1-openssl</packagereq>
+       <packagereq type="mandatory">openssl</packagereq>
+
+       <!-- bootcd is generated using mkisofs -->
+       <packagereq type="mandatory">mkisofs</packagereq>
+
+       <!-- bootcd and bootmanager images are signed using GPG -->
+       <packagereq type="mandatory">gnupg</packagereq>
+
+       <!-- bootmanager requires uuencode -->
+       <packagereq type="mandatory">sharutils</packagereq>
+      </packagelist>
+    </group>
+
+  </comps>
+
+</configuration>