Show the cherrypy request object on the helloworld page
[invirt/packages/invirt-web.git] / code / main.py
index 10b6d73..3579794 100755 (executable)
@@ -6,18 +6,20 @@ import cPickle
 import cgi
 import datetime
 import hmac
 import cgi
 import datetime
 import hmac
+import random
 import sha
 import simplejson
 import sys
 import time
 import urllib
 import sha
 import simplejson
 import sys
 import time
 import urllib
+import socket
+import cherrypy
 from StringIO import StringIO
 from StringIO import StringIO
-
 def revertStandardError():
     """Move stderr to stdout, and return the contents of the old stderr."""
     errio = sys.stderr
     if not isinstance(errio, StringIO):
 def revertStandardError():
     """Move stderr to stdout, and return the contents of the old stderr."""
     errio = sys.stderr
     if not isinstance(errio, StringIO):
-        return None
+        return ''
     sys.stderr = sys.stdout
     errio.seek(0)
     return errio.read()
     sys.stderr = sys.stdout
     errio.seek(0)
     return errio.read()
@@ -30,18 +32,53 @@ def printError():
 if __name__ == '__main__':
     import atexit
     atexit.register(printError)
 if __name__ == '__main__':
     import atexit
     atexit.register(printError)
-    sys.stderr = StringIO()
-
-sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 import templates
 from Cheetah.Template import Template
 
 import templates
 from Cheetah.Template import Template
-import sipb_xen_database
-from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
 import validation
 import cache_acls
 import validation
 import cache_acls
-from webcommon import InvalidInput, CodeError, State
+from webcommon import State
 import controls
 import controls
+from getafsgroups import getAfsGroupMembers
+from invirt import database
+from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
+from invirt.config import structs as config
+from invirt.common import InvalidInput, CodeError
+
+from view import View
+
+class InvirtWeb(View):
+    def __init__(self):
+        super(self.__class__,self).__init__()
+        connect()
+        self._cp_config['tools.require_login.on'] = True
+        self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
+                                                 'from invirt import database']
+
+
+    @cherrypy.expose
+    @cherrypy.tools.mako(filename="/list.mako")
+    def list(self):
+        """Handler for list requests."""
+        checkpoint.checkpoint('Getting list dict')
+        d = getListDict(cherrypy.request.login, cherrypy.request.state)
+        checkpoint.checkpoint('Got list dict')
+        return d
+    index=list
+
+    @cherrypy.expose
+    @cherrypy.tools.mako(filename="/helloworld.mako")
+    def helloworld(self, **kwargs):
+        return {'request': cherrypy.request, 'kwargs': kwargs}
+    helloworld._cp_config['tools.require_login.on'] = False
+
+def pathSplit(path):
+    if path.startswith('/'):
+        path = path[1:]
+    i = path.find('/')
+    if i == -1:
+        i = len(path)
+    return path[:i], path[i:]
 
 class Checkpoint:
     def __init__(self):
 
 class Checkpoint:
     def __init__(self):
@@ -58,16 +95,6 @@ class Checkpoint:
 
 checkpoint = Checkpoint()
 
 
 checkpoint = Checkpoint()
 
-def jquote(string):
-    return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
-
-def helppopup(subj):
-    """Return HTML code for a (?) link to a specified help topic"""
-    return ('<span class="helplink"><a href="help?' +
-            cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
-            +'" target="_blank" ' +
-            'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
-
 def makeErrorPre(old, addition):
     if addition is None:
         return
 def makeErrorPre(old, addition):
     if addition is None:
         return
@@ -76,8 +103,8 @@ def makeErrorPre(old, addition):
     else:
         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
 
     else:
         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
 
-Template.sipb_xen_database = sipb_xen_database
-Template.helppopup = staticmethod(helppopup)
+Template.database = database
+Template.config = config
 Template.err = None
 
 class JsonDict:
 Template.err = None
 
 class JsonDict:
@@ -104,13 +131,14 @@ class Defaults:
     cdrom = ''
     autoinstall = ''
     name = ''
     cdrom = ''
     autoinstall = ''
     name = ''
+    description = ''
     type = 'linux-hvm'
 
     def __init__(self, max_memory=None, max_disk=None, **kws):
         if max_memory is not None:
             self.memory = min(self.memory, max_memory)
         if max_disk is not None:
     type = 'linux-hvm'
 
     def __init__(self, max_memory=None, max_disk=None, **kws):
         if max_memory is not None:
             self.memory = min(self.memory, max_memory)
         if max_disk is not None:
-            self.max_disk = min(self.disk, max_disk)
+            self.disk = min(self.disk, max_disk)
         for key in kws:
             setattr(self, key, kws[key])
 
         for key in kws:
             setattr(self, key, kws[key])
 
@@ -118,12 +146,6 @@ class Defaults:
 
 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
 
 
 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
 
-def error(op, username, fields, err, emsg):
-    """Print an error page when a CodeError occurs"""
-    d = dict(op=op, user=username, errorMessage=str(err),
-             stderr=emsg)
-    return templates.error(searchList=[d])
-
 def invalidInput(op, username, fields, err, emsg):
     """Print an error page when an InvalidInput exception occurs"""
     d = dict(op=op, user=username, err_field=err.err_field,
 def invalidInput(op, username, fields, err, emsg):
     """Print an error page when an InvalidInput exception occurs"""
     d = dict(op=op, user=username, err_field=err.err_field,
@@ -142,14 +164,14 @@ def hasVnc(status):
     return False
 
 def parseCreate(username, state, fields):
     return False
 
 def parseCreate(username, state, fields):
-    kws = dict([(kw, fields.getfirst(kw)) for kw in 'name owner memory disksize vmtype cdrom clone_from'.split()])
+    kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
     validate = validation.Validate(username, state, strict=True, **kws)
     validate = validation.Validate(username, state, strict=True, **kws)
-    return dict(contact=username, name=validate.name, memory=validate.memory,
-                disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
+    return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
+                disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
                 cdrom=getattr(validate, 'cdrom', None),
                 cdrom=getattr(validate, 'cdrom', None),
-                clone_from=getattr(validate, 'clone_from', None))
+                autoinstall=getattr(validate, 'autoinstall', None))
 
 
-def create(username, state, fields):
+def create(username, state, path, fields):
     """Handler for create requests."""
     try:
         parsed_fields = parseCreate(username, state, fields)
     """Handler for create requests."""
     try:
         parsed_fields = parseCreate(username, state, fields)
@@ -196,8 +218,7 @@ def getListDict(username, state):
     checkpoint.checkpoint('Got max mem/disk')
     defaults = Defaults(max_memory=max_memory,
                         max_disk=max_disk,
     checkpoint.checkpoint('Got max mem/disk')
     defaults = Defaults(max_memory=max_memory,
                         max_disk=max_disk,
-                        owner=username,
-                        cdrom='gutsy-i386')
+                        owner=username)
     checkpoint.checkpoint('Got defaults')
     def sortkey(machine):
         return (machine.owner != username, machine.owner, machine.name)
     checkpoint.checkpoint('Got defaults')
     def sortkey(machine):
         return (machine.owner != username, machine.owner, machine.name)
@@ -212,14 +233,7 @@ def getListDict(username, state):
              can_clone=can_clone)
     return d
 
              can_clone=can_clone)
     return d
 
-def listVms(username, state, fields):
-    """Handler for list requests."""
-    checkpoint.checkpoint('Getting list dict')
-    d = getListDict(username, state)
-    checkpoint.checkpoint('Got list dict')
-    return templates.list(searchList=[d])
-
-def vnc(username, state, fields):
+def vnc(username, state, path, fields):
     """VNC applet page.
 
     Note that due to same-domain restrictions, the applet connects to
     """VNC applet page.
 
     Note that due to same-domain restrictions, the applet connects to
@@ -241,18 +255,12 @@ def vnc(username, state, fields):
     """
     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
 
     """
     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
 
-    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
-
-    data = {}
-    data["user"] = username
-    data["machine"] = machine.name
-    data["expires"] = time.time()+(5*60)
-    pickled_data = cPickle.dumps(data)
-    m = hmac.new(TOKEN_KEY, digestmod=sha)
-    m.update(pickled_data)
-    token = {'data': pickled_data, 'digest': m.digest()}
-    token = cPickle.dumps(token)
-    token = base64.urlsafe_b64encode(token)
+    token = controls.vnctoken(machine)
+    host = controls.listHost(machine)
+    if host:
+        port = 10003 + [h.hostname for h in config.hosts].index(host)
+    else:
+        port = 5900 # dummy
 
     status = controls.statusInfo(machine)
     has_vnc = hasVnc(status)
 
     status = controls.statusInfo(machine)
     has_vnc = hasVnc(status)
@@ -262,6 +270,7 @@ def vnc(username, state, fields):
              has_vnc=has_vnc,
              machine=machine,
              hostname=state.environ.get('SERVER_NAME', 'localhost'),
              has_vnc=has_vnc,
              machine=machine,
              hostname=state.environ.get('SERVER_NAME', 'localhost'),
+             port=port,
              authtoken=token)
     return templates.vnc(searchList=[d])
 
              authtoken=token)
     return templates.vnc(searchList=[d])
 
@@ -270,13 +279,16 @@ def getHostname(nic):
 
     XXX this should be merged with the similar logic in DNS and DHCP.
     """
 
     XXX this should be merged with the similar logic in DNS and DHCP.
     """
-    if nic.hostname and '.' in nic.hostname:
-        return nic.hostname
+    if nic.hostname:
+        hostname = nic.hostname
     elif nic.machine:
     elif nic.machine:
-        return nic.machine.name + '.xvm.mit.edu'
+        hostname = nic.machine.name
     else:
         return None
     else:
         return None
-
+    if '.' in hostname:
+        return hostname
+    else:
+        return hostname + '.' + config.dns.domains[0]
 
 def getNicInfo(data_dict, machine):
     """Helper function for info, get data on nics for a machine.
 
 def getNicInfo(data_dict, machine):
     """Helper function for info, get data on nics for a machine.
@@ -292,8 +304,7 @@ def getNicInfo(data_dict, machine):
     nic_fields = []
     for i in range(len(machine.nics)):
         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
     nic_fields = []
     for i in range(len(machine.nics)):
         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
-        if not i:
-            data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
+        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
         data_dict['nic%s_ip' % i] = machine.nics[i].ip
     if len(machine.nics) == 1:
         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
         data_dict['nic%s_ip' % i] = machine.nics[i].ip
     if len(machine.nics) == 1:
@@ -316,7 +327,7 @@ def getDiskInfo(data_dict, machine):
         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
     return disk_fields
 
         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
     return disk_fields
 
-def command(username, state, fields):
+def command(username, state, path, fields):
     """Handler for running commands like boot and delete on a VM."""
     back = fields.getfirst('back')
     try:
     """Handler for running commands like boot and delete on a VM."""
     back = fields.getfirst('back')
     try:
@@ -339,8 +350,8 @@ def command(username, state, fields):
         return templates.list(searchList=[d])
     elif back == 'info':
         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
         return templates.list(searchList=[d])
     elif back == 'info':
         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
-        return ({'Status': '302',
-                 'Location': '/info?machine_id=%d' % machine.machine_id},
+        return ({'Status': '303 See Other',
+                 'Location': 'info?machine_id=%d' % machine.machine_id},
                 "You shouldn't see this message.")
     else:
         raise InvalidInput('back', back, 'Not a known back page.')
                 "You shouldn't see this message.")
     else:
         raise InvalidInput('back', back, 'Not a known back page.')
@@ -351,9 +362,9 @@ def modifyDict(username, state, fields):
     Return a list of local variables for modify.tmpl.
     """
     olddisk = {}
     Return a list of local variables for modify.tmpl.
     """
     olddisk = {}
-    transaction = ctx.current.create_transaction()
+    session.begin()
     try:
     try:
-        kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name memory vmtype disksize'.split()])
+        kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
         validate = validation.Validate(username, state, **kws)
         machine = validate.machine
         oldname = machine.name
         validate = validation.Validate(username, state, **kws)
         machine = validate.machine
         oldname = machine.name
@@ -370,7 +381,7 @@ def modifyDict(username, state, fields):
             if disk.size != disksize:
                 olddisk[disk.guest_device_name] = disksize
                 disk.size = disksize
             if disk.size != disksize:
                 olddisk[disk.guest_device_name] = disksize
                 disk.size = disksize
-                ctx.current.save(disk)
+                session.save_or_update(disk)
 
         update_acl = False
         if hasattr(validate, 'owner') and validate.owner != machine.owner:
 
         update_acl = False
         if hasattr(validate, 'owner') and validate.owner != machine.owner:
@@ -378,19 +389,23 @@ def modifyDict(username, state, fields):
             update_acl = True
         if hasattr(validate, 'name'):
             machine.name = validate.name
             update_acl = True
         if hasattr(validate, 'name'):
             machine.name = validate.name
+            for n in machine.nics:
+                if n.hostname == oldname:
+                    n.hostname = validate.name
+        if hasattr(validate, 'description'):
+            machine.description = validate.description
         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
             machine.administrator = validate.admin
             update_acl = True
         if hasattr(validate, 'contact'):
             machine.contact = validate.contact
 
         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
             machine.administrator = validate.admin
             update_acl = True
         if hasattr(validate, 'contact'):
             machine.contact = validate.contact
 
-        ctx.current.save(machine)
+        session.save_or_update(machine)
         if update_acl:
         if update_acl:
-            print >> sys.stderr, machine, machine.administrator
             cache_acls.refreshMachine(machine)
             cache_acls.refreshMachine(machine)
-        transaction.commit()
+        session.commit()
     except:
     except:
-        transaction.rollback()
+        session.rollback()
         raise
     for diskname in olddisk:
         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
         raise
     for diskname in olddisk:
         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
@@ -400,7 +415,7 @@ def modifyDict(username, state, fields):
                 command="modify",
                 machine=machine)
 
                 command="modify",
                 machine=machine)
 
-def modify(username, state, fields):
+def modify(username, state, path, fields):
     """Handler for modifying attributes of a machine."""
     try:
         modify_dict = modifyDict(username, state, fields)
     """Handler for modifying attributes of a machine."""
     try:
         modify_dict = modifyDict(username, state, fields)
@@ -420,20 +435,44 @@ def modify(username, state, fields):
     return templates.info(searchList=[info_dict])
 
 
     return templates.info(searchList=[info_dict])
 
 
-def helpHandler(username, state, fields):
+def helpHandler(username, state, path, fields):
     """Handler for help messages."""
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
 
     """Handler for help messages."""
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
 
-    help_mapping = {'ParaVM Console': """
+    help_mapping = {
+                    'Autoinstalls': """
+The autoinstaller builds a minimal Debian or Ubuntu system to run as a
+ParaVM.  You can access the resulting system by logging into the <a
+href="help?simple=true&subject=ParaVM+Console">serial console server</a>
+with your Kerberos tickets; there is no root password so sshd will
+refuse login.</p>
+
+<p>Under the covers, the autoinstaller uses our own patched version of
+xen-create-image, which is a tool based on debootstrap.  If you log
+into the serial console while the install is running, you can watch
+it.
+""",
+                    'ParaVM Console': """
 ParaVM machines do not support local console access over VNC.  To
 access the serial console of these machines, you can SSH with Kerberos
 ParaVM machines do not support local console access over VNC.  To
 access the serial console of these machines, you can SSH with Kerberos
-to console.xvm.mit.edu, using the name of the machine as your
-username.""",
+to %s, using the name of the machine as your
+username.""" % config.console.hostname,
                     'HVM/ParaVM': """
 HVM machines use the virtualization features of the processor, while
                     'HVM/ParaVM': """
 HVM machines use the virtualization features of the processor, while
-ParaVM machines use Xen's emulation of virtualization features.  You
-want an HVM virtualized machine.""",
+ParaVM machines rely on a modified kernel to communicate directly with
+the hypervisor.  HVMs support boot CDs of any operating system, and
+the VNC console applet.  The three-minute autoinstaller produces
+ParaVMs.  ParaVMs typically are more efficient, and always support the
+<a href="help?subject=ParaVM+Console">console server</a>.</p>
+
+<p>More details are <a
+href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
+wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
+(which you can skip by using the autoinstaller to begin with.)</p>
+
+<p>We recommend using a ParaVM when possible and an HVM when necessary.
+""",
                     'CPU Weight': """
 Don't ask us!  We're as mystified as you are.""",
                     'Owner': """
                     'CPU Weight': """
 Don't ask us!  We're as mystified as you are.""",
                     'Owner': """
@@ -450,13 +489,17 @@ power on and off the machine.  This can be either a user or a moira
 group.""",
                     'Quotas': """
 Quotas are determined on a per-locker basis.  Each locker may have a
 group.""",
                     'Quotas': """
 Quotas are determined on a per-locker basis.  Each locker may have a
-maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
+maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
 active machines.""",
                     'Console': """
 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
 your machine will run just fine, but the applet's display of the
 console will suffer artifacts.
 active machines.""",
                     'Console': """
 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
 your machine will run just fine, but the applet's display of the
 console will suffer artifacts.
+""",
+                    'Windows': """
+<strong>Windows Vista:</strong> The Vista image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-email.txt">the licensing confirmation e-mail</a> for details. The installer requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).<br>
+<strong>Windows XP:</strong> This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one.
 """
                     }
 
 """
                     }
 
@@ -471,9 +514,9 @@ console will suffer artifacts.
     return templates.help(searchList=[d])
 
 
     return templates.help(searchList=[d])
 
 
-def badOperation(u, s, e):
+def badOperation(u, s, p, e):
     """Function called when accessing an unknown URI."""
     """Function called when accessing an unknown URI."""
-    raise CodeError("Unknown operation")
+    return ({'Status': '404 Not Found'}, 'Invalid operation.')
 
 def infoDict(username, state, machine):
     """Get the variables used by info.tmpl."""
 
 def infoDict(username, state, machine):
     """Get the variables used by info.tmpl."""
@@ -487,14 +530,14 @@ def infoDict(username, state, machine):
         cputime = None
     else:
         main_status = dict(status[1:])
         cputime = None
     else:
         main_status = dict(status[1:])
+        main_status['host'] = controls.listHost(machine)
         start_time = float(main_status.get('start_time', 0))
         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
         cpu_time_float = float(main_status.get('cpu_time', 0))
         cputime = datetime.timedelta(seconds=int(cpu_time_float))
     checkpoint.checkpoint('Status')
         start_time = float(main_status.get('start_time', 0))
         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
         cpu_time_float = float(main_status.get('cpu_time', 0))
         cputime = datetime.timedelta(seconds=int(cpu_time_float))
     checkpoint.checkpoint('Status')
-    display_fields = """name uptime memory state cpu_weight on_reboot 
-     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
     display_fields = [('name', 'Name'),
     display_fields = [('name', 'Name'),
+                      ('description', 'Description'),
                       ('owner', 'Owner'),
                       ('administrator', 'Administrator'),
                       ('contact', 'Contact'),
                       ('owner', 'Owner'),
                       ('administrator', 'Administrator'),
                       ('contact', 'Contact'),
@@ -502,20 +545,16 @@ def infoDict(username, state, machine):
                       'NIC_INFO',
                       ('uptime', 'uptime'),
                       ('cputime', 'CPU usage'),
                       'NIC_INFO',
                       ('uptime', 'uptime'),
                       ('cputime', 'CPU usage'),
+                      ('host', 'Hosted on'),
                       ('memory', 'RAM'),
                       'DISK_INFO',
                       ('state', 'state (xen format)'),
                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
                       ('memory', 'RAM'),
                       'DISK_INFO',
                       ('state', 'state (xen format)'),
                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
-                      ('on_reboot', 'Action on VM reboot'),
-                      ('on_poweroff', 'Action on VM poweroff'),
-                      ('on_crash', 'Action on VM crash'),
-                      ('on_xend_start', 'Action on Xen start'),
-                      ('on_xend_stop', 'Action on Xen stop'),
-                      ('bootloader', 'Bootloader options'),
                       ]
     fields = []
     machine_info = {}
     machine_info['name'] = machine.name
                       ]
     fields = []
     machine_info = {}
     machine_info['name'] = machine.name
+    machine_info['description'] = machine.description
     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
     machine_info['owner'] = machine.owner
     machine_info['administrator'] = machine.administrator
     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
     machine_info['owner'] = machine.owner
     machine_info['administrator'] = machine.administrator
@@ -550,7 +589,7 @@ def infoDict(username, state, machine):
     checkpoint.checkpoint('Got mem')
     max_disk = validation.maxDisk(machine.owner, machine)
     defaults = Defaults()
     checkpoint.checkpoint('Got mem')
     max_disk = validation.maxDisk(machine.owner, machine)
     defaults = Defaults()
-    for name in 'machine_id name administrator owner memory contact'.split():
+    for name in 'machine_id name description administrator owner memory contact'.split():
         setattr(defaults, name, getattr(machine, name))
     defaults.type = machine.type.type_id
     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
         setattr(defaults, name, getattr(machine, name))
     defaults.type = machine.type.type_id
     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
@@ -568,25 +607,45 @@ def infoDict(username, state, machine):
              fields = fields)
     return d
 
              fields = fields)
     return d
 
-def info(username, state, fields):
+def info(username, state, path, fields):
     """Handler for info on a single VM."""
     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
     d = infoDict(username, state, machine)
     checkpoint.checkpoint('Got infodict')
     return templates.info(searchList=[d])
 
     """Handler for info on a single VM."""
     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
     d = infoDict(username, state, machine)
     checkpoint.checkpoint('Got infodict')
     return templates.info(searchList=[d])
 
-def unauthFront(_, _2, fields):
+def unauthFront(_, _2, _3, fields):
     """Information for unauth'd users."""
     """Information for unauth'd users."""
-    return templates.unauth(searchList=[{'simple' : True}])
+    return templates.unauth(searchList=[{'simple' : True, 
+            'hostname' : socket.getfqdn()}])
 
 
-mapping = dict(list=listVms,
+def admin(username, state, path, fields):
+    if path == '':
+        return ({'Status': '303 See Other',
+                 'Location': 'admin/'},
+                "You shouldn't see this message.")
+    if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
+        raise InvalidInput('username', username,
+                           'Not in admin group %s.' % config.adminacl)
+    newstate = State(username, isadmin=True)
+    newstate.environ = state.environ
+    return handler(username, newstate, path, fields)
+
+def throwError(_, __, ___, ____):
+    """Throw an error, to test the error-tracing mechanisms."""
+    raise RuntimeError("test of the emergency broadcast system")
+
+mapping = dict(#list=listVms,
                vnc=vnc,
                command=command,
                modify=modify,
                info=info,
                create=create,
                help=helpHandler,
                vnc=vnc,
                command=command,
                modify=modify,
                info=info,
                create=create,
                help=helpHandler,
-               unauth=unauthFront)
+               unauth=unauthFront,
+               admin=admin,
+               overlord=admin,
+               errortest=throwError)
 
 def printHeaders(headers):
     """Print a dictionary as HTTP headers."""
 
 def printHeaders(headers):
     """Print a dictionary as HTTP headers."""
@@ -594,15 +653,41 @@ def printHeaders(headers):
         print '%s: %s' % (key, value)
     print
 
         print '%s: %s' % (key, value)
     print
 
+def send_error_mail(subject, body):
+    import subprocess
 
 
-def getUser(environ):
-    """Return the current user based on the SSL environment variables"""
-    email = environ.get('SSL_CLIENT_S_DN_Email', None)
-    if email is None:
-        return None
-    if not email.endswith('@MIT.EDU'):
-        return None
-    return email[:-8]
+    to = config.web.errormail
+    mail = """To: %s
+From: root@%s
+Subject: %s
+
+%s
+""" % (to, config.web.hostname, subject, body)
+    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
+                         stdin=subprocess.PIPE)
+    p.stdin.write(mail)
+    p.stdin.close()
+    p.wait()
+
+def show_error(op, username, fields, err, emsg, traceback):
+    """Print an error page when an exception occurs"""
+    d = dict(op=op, user=username, fields=fields,
+             errorMessage=str(err), stderr=emsg, traceback=traceback)
+    details = templates.error_raw(searchList=[d])
+    exclude = config.web.errormail_exclude
+    if username not in exclude and '*' not in exclude:
+        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
+                        details)
+    d['details'] = details
+    return templates.error(searchList=[d])
+
+def handler(username, state, path, fields):
+    operation, path = pathSplit(path)
+    if not operation:
+        operation = 'list'
+    print 'Starting', operation
+    fun = mapping.get(operation, badOperation)
+    return fun(username, state, path, fields)
 
 class App:
     def __init__(self, environ, start_response):
 
 class App:
     def __init__(self, environ, start_response):
@@ -613,68 +698,55 @@ class App:
         self.state = State(self.username)
         self.state.environ = environ
 
         self.state = State(self.username)
         self.state.environ = environ
 
+        random.seed() #sigh
+
     def __iter__(self):
     def __iter__(self):
+        start_time = time.time()
+        database.clear_cache()
+        sys.stderr = StringIO()
         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
-        print >> sys.stderr, fields
         operation = self.environ.get('PATH_INFO', '')
         if not operation:
         operation = self.environ.get('PATH_INFO', '')
         if not operation:
-            self.start("301 Moved Permanently", [('Location',
-                                                  self.environ['SCRIPT_NAME']+'/')])
+            self.start("301 Moved Permanently", [('Location', './')])
             return
         if self.username is None:
             operation = 'unauth'
             return
         if self.username is None:
             operation = 'unauth'
-        if operation.startswith('/'):
-            operation = operation[1:]
-        if not operation:
-            operation = 'list'
-        print 'Starting', operation
 
 
-        start_time = time.time()
-        fun = mapping.get(operation, badOperation)
         try:
             checkpoint.checkpoint('Before')
         try:
             checkpoint.checkpoint('Before')
-            output = fun(self.username, self.state, fields)
+            output = handler(self.username, self.state, operation, fields)
             checkpoint.checkpoint('After')
 
             headers = dict(DEFAULT_HEADERS)
             if isinstance(output, tuple):
                 new_headers, output = output
                 headers.update(new_headers)
             checkpoint.checkpoint('After')
 
             headers = dict(DEFAULT_HEADERS)
             if isinstance(output, tuple):
                 new_headers, output = output
                 headers.update(new_headers)
-            print 'MOO2'
             e = revertStandardError()
             if e:
             e = revertStandardError()
             if e:
-                if isinstance(output, basestring):
-                    sys.stderr = StringIO()
-                    x = str(output)
-                    print >> sys.stderr, x
-                    print >> sys.stderr, 'XXX'
-                    print >> sys.stderr, e
-                    raise Exception()
-                output.addError(e)
+                if hasattr(output, 'addError'):
+                    output.addError(e)
+                else:
+                    # This only happens on redirects, so it'd be a pain to get
+                    # the message to the user.  Maybe in the response is useful.
+                    output = output + '\n\nstderr:\n' + e
             output_string =  str(output)
             checkpoint.checkpoint('output as a string')
         except Exception, err:
             if not fields.has_key('js'):
             output_string =  str(output)
             checkpoint.checkpoint('output as a string')
         except Exception, err:
             if not fields.has_key('js'):
-                if isinstance(err, CodeError):
-                    self.start('500 Internal Server Error', [('Content-Type', 'text/html')])
-                    e = revertStandardError()
-                    s = error(operation, self.username, fields, err, e)
-                    yield str(s)
-                    return
                 if isinstance(err, InvalidInput):
                     self.start('200 OK', [('Content-Type', 'text/html')])
                     e = revertStandardError()
                 if isinstance(err, InvalidInput):
                     self.start('200 OK', [('Content-Type', 'text/html')])
                     e = revertStandardError()
-                    yield str(invalidInput(operation, self.username, fields, err, e))
+                    yield str(invalidInput(operation, self.username, fields,
+                                           err, e))
                     return
                     return
-            self.start('500 Internal Server Error', [('Content-Type', 'text/plain')])
             import traceback
             import traceback
-            yield '''Uh-oh!  We experienced an error.'
-Please email xvm-dev@mit.edu with the contents of this page.'
-----
-%s
-----
-%s
-----''' % (str(err), traceback.format_exc())
+            self.start('500 Internal Server Error',
+                       [('Content-Type', 'text/html')])
+            e = revertStandardError()
+            s = show_error(operation, self.username, fields,
+                           err, e, traceback.format_exc())
+            yield str(s)
+            return
         status = headers.setdefault('Status', '200 OK')
         del headers['Status']
         self.start(status, headers.items())
         status = headers.setdefault('Status', '200 OK')
         del headers['Status']
         self.start(status, headers.items())
@@ -683,7 +755,7 @@ Please email xvm-dev@mit.edu with the contents of this page.'
             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
 
 def constructor():
             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
 
 def constructor():
-    connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
+    connect()
     return App
 
 def main():
     return App
 
 def main():