Revert punting the "moocow" password in invirt-web and
[invirt/packages/invirt-web.git] / code / main.py
index 9caf7c4..6fdd31a 100755 (executable)
@@ -6,18 +6,20 @@ import cPickle
 import cgi
 import datetime
 import hmac
 import cgi
 import datetime
 import hmac
-import os
+import random
 import sha
 import simplejson
 import sys
 import time
 import sha
 import simplejson
 import sys
 import time
+import urllib
+import socket
 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):
 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):
-        return None
+        return ''
     sys.stderr = sys.stdout
     errio.seek(0)
     return errio.read()
     sys.stderr = sys.stdout
     errio.seek(0)
     return errio.read()
@@ -30,16 +32,26 @@ 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
-from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess
 import validation
 import validation
-from webcommon import InvalidInput, CodeError, g
+import cache_acls
+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
+
+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):
@@ -56,12 +68,15 @@ 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"""
 
 def helppopup(subj):
     """Return HTML code for a (?) link to a specified help topic"""
-    return ('<span class="helplink"><a href="help?subject=' + subj +
-            '&amp;simple=true" target="_blank" ' +
-            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
+    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:
 
 def makeErrorPre(old, addition):
     if addition is None:
@@ -71,6 +86,8 @@ def makeErrorPre(old, addition):
     else:
         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
 
     else:
         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
 
+Template.database = database
+Template.config = config
 Template.helppopup = staticmethod(helppopup)
 Template.err = None
 
 Template.helppopup = staticmethod(helppopup)
 Template.err = None
 
@@ -96,13 +113,16 @@ class Defaults:
     memory = 256
     disk = 4.0
     cdrom = ''
     memory = 256
     disk = 4.0
     cdrom = ''
+    autoinstall = ''
     name = ''
     name = ''
-    vmtype = 'hvm'
+    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:
     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])
 
@@ -110,15 +130,9 @@ class Defaults:
 
 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
 
 
 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
 
-def error(op, user, fields, err, emsg):
-    """Print an error page when a CodeError occurs"""
-    d = dict(op=op, user=user, errorMessage=str(err),
-             stderr=emsg)
-    return templates.error(searchList=[d])
-
-def invalidInput(op, user, fields, err, emsg):
+def invalidInput(op, username, fields, err, emsg):
     """Print an error page when an InvalidInput exception occurs"""
     """Print an error page when an InvalidInput exception occurs"""
-    d = dict(op=op, user=user, err_field=err.err_field,
+    d = dict(op=op, user=username, err_field=err.err_field,
              err_value=str(err.err_value), stderr=emsg,
              errorMessage=str(err))
     return templates.invalid(searchList=[d])
              err_value=str(err.err_value), stderr=emsg,
              errorMessage=str(err))
     return templates.invalid(searchList=[d])
@@ -133,49 +147,25 @@ def hasVnc(status):
             return 'location' in d
     return False
 
             return 'location' in d
     return False
 
-def parseCreate(user, fields):
-    name = fields.getfirst('name')
-    if not validation.validMachineName(name):
-        raise InvalidInput('name', name, 'You must provide a machine name.  Max 22 chars, alnum plus \'-\' and \'_\'.')
-    name = name.lower()
-
-    if Machine.get_by(name=name):
-        raise InvalidInput('name', name,
-                           "Name already exists.")
-
-    owner = validation.testOwner(user, fields.getfirst('owner'))
-
-    memory = fields.getfirst('memory')
-    memory = validation.validMemory(owner, memory, on=True)
-
-    disk_size = fields.getfirst('disk')
-    disk_size = validation.validDisk(owner, disk_size)
-
-    vm_type = fields.getfirst('vmtype')
-    vm_type = validation.validVmType(vm_type)
+def parseCreate(username, state, fields):
+    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)
+    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),
+                autoinstall=getattr(validate, 'autoinstall', None))
 
 
-    cdrom = fields.getfirst('cdrom')
-    if cdrom is not None and not CDROM.get(cdrom):
-        raise CodeError("Invalid cdrom type '%s'" % cdrom)
-
-    clone_from = fields.getfirst('clone_from')
-    if clone_from and clone_from != 'ice3':
-        raise CodeError("Invalid clone image '%s'" % clone_from)
-
-    return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
-                owner=owner, machine_type=vm_type, cdrom=cdrom, clone_from=clone_from)
-
-def create(user, fields):
+def create(username, state, path, fields):
     """Handler for create requests."""
     try:
     """Handler for create requests."""
     try:
-        parsed_fields = parseCreate(user, fields)
-        machine = controls.createVm(**parsed_fields)
+        parsed_fields = parseCreate(username, state, fields)
+        machine = controls.createVm(username, state, **parsed_fields)
     except InvalidInput, err:
         pass
     else:
         err = None
     except InvalidInput, err:
         pass
     else:
         err = None
-    g.clear() #Changed global state
-    d = getListDict(user)
+    state.clear() #Changed global state
+    d = getListDict(username, state)
     d['err'] = err
     if err:
         for field in fields.keys():
     d['err'] = err
     if err:
         for field in fields.keys():
@@ -185,52 +175,56 @@ def create(user, fields):
     return templates.list(searchList=[d])
 
 
     return templates.list(searchList=[d])
 
 
-def getListDict(user):
+def getListDict(username, state):
     """Gets the list of local variables used by list.tmpl."""
     """Gets the list of local variables used by list.tmpl."""
-    machines = g.machines
+    checkpoint.checkpoint('Starting')
+    machines = state.machines
     checkpoint.checkpoint('Got my machines')
     on = {}
     has_vnc = {}
     checkpoint.checkpoint('Got my machines')
     on = {}
     has_vnc = {}
-    on = g.uptimes
+    xmlist = state.xmlist
     checkpoint.checkpoint('Got uptimes')
     checkpoint.checkpoint('Got uptimes')
+    can_clone = 'ice3' not in state.xmlist_raw
     for m in machines:
     for m in machines:
-        m.uptime = g.uptimes.get(m)
-        if not on[m]:
+        if m not in xmlist:
             has_vnc[m] = 'Off'
             has_vnc[m] = 'Off'
-        elif m.type.hvm:
-            has_vnc[m] = True
+            m.uptime = None
         else:
         else:
-            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
-    max_memory = validation.maxMemory(user)
-    max_disk = validation.maxDisk(user)
+            m.uptime = xmlist[m]['uptime']
+            if xmlist[m]['console']:
+                has_vnc[m] = True
+            elif m.type.hvm:
+                has_vnc[m] = "WTF?"
+            else:
+                has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
+    max_memory = validation.maxMemory(username, state)
+    max_disk = validation.maxDisk(username)
     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=user,
-                        cdrom='gutsy-i386')
+                        owner=username)
     checkpoint.checkpoint('Got defaults')
     def sortkey(machine):
     checkpoint.checkpoint('Got defaults')
     def sortkey(machine):
-        return (machine.owner != user, machine.owner, machine.name)
+        return (machine.owner != username, machine.owner, machine.name)
     machines = sorted(machines, key=sortkey)
     machines = sorted(machines, key=sortkey)
-    d = dict(user=user,
-             cant_add_vm=validation.cantAddVm(user),
+    d = dict(user=username,
+             cant_add_vm=validation.cantAddVm(username, state),
              max_memory=max_memory,
              max_disk=max_disk,
              defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
              max_memory=max_memory,
              max_disk=max_disk,
              defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
-             uptimes=g.uptimes,
-             cdroms=CDROM.select())
+             can_clone=can_clone)
     return d
 
     return d
 
-def listVms(user, fields):
+def listVms(username, state, path, fields):
     """Handler for list requests."""
     checkpoint.checkpoint('Getting list dict')
     """Handler for list requests."""
     checkpoint.checkpoint('Getting list dict')
-    d = getListDict(user)
+    d = getListDict(username, state)
     checkpoint.checkpoint('Got list dict')
     return templates.list(searchList=[d])
 
     checkpoint.checkpoint('Got list dict')
     return templates.list(searchList=[d])
 
-def vnc(user, 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
@@ -250,29 +244,24 @@ def vnc(user, fields):
     Remember to enable iptables!
     echo 1 > /proc/sys/net/ipv4/ip_forward
     """
     Remember to enable iptables!
     echo 1 > /proc/sys/net/ipv4/ip_forward
     """
-    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+    machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
 
 
-    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
-
-    data = {}
-    data["user"] = user
-    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)
 
-    d = dict(user=user,
+    d = dict(user=username,
              on=status,
              has_vnc=has_vnc,
              machine=machine,
              on=status,
              has_vnc=has_vnc,
              machine=machine,
-             hostname=os.environ.get('SERVER_NAME', 'localhost'),
+             hostname=state.environ.get('SERVER_NAME', 'localhost'),
+             port=port,
              authtoken=token)
     return templates.vnc(searchList=[d])
 
              authtoken=token)
     return templates.vnc(searchList=[d])
 
@@ -281,13 +270,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 + '.servers.csail.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.
@@ -303,8 +295,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:
@@ -327,101 +318,106 @@ 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(user, 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:
-        d = controls.commandResult(user, fields)
+        d = controls.commandResult(username, state, fields)
         if d['command'] == 'Delete VM':
             back = 'list'
     except InvalidInput, err:
         if not back:
             raise
         if d['command'] == 'Delete VM':
             back = 'list'
     except InvalidInput, err:
         if not back:
             raise
-        #print >> sys.stderr, err
+        print >> sys.stderr, err
         result = err
     else:
         result = 'Success!'
         if not back:
             return templates.command(searchList=[d])
     if back == 'list':
         result = err
     else:
         result = 'Success!'
         if not back:
             return templates.command(searchList=[d])
     if back == 'list':
-        g.clear() #Changed global state
-        d = getListDict(user)
+        state.clear() #Changed global state
+        d = getListDict(username, state)
         d['result'] = result
         return templates.list(searchList=[d])
     elif back == 'info':
         d['result'] = result
         return templates.list(searchList=[d])
     elif back == 'info':
-        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-        return ({'Status': '302',
-                 'Location': '/info?machine_id=%d' % machine.machine_id},
+        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
+        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.')
 
-def modifyDict(user, fields):
+def modifyDict(username, state, fields):
     """Modify a machine as specified by CGI arguments.
 
     Return a list of local variables for modify.tmpl.
     """
     olddisk = {}
     """Modify a machine as specified by CGI arguments.
 
     Return a list of local variables for modify.tmpl.
     """
     olddisk = {}
-    transaction = ctx.current.create_transaction()
+    session.begin()
     try:
     try:
-        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-        owner = validation.testOwner(user, fields.getfirst('owner'), machine)
-        admin = validation.testAdmin(user, fields.getfirst('administrator'),
-                                     machine)
-        contact = validation.testContact(user, fields.getfirst('contact'),
-                                         machine)
-        name = validation.testName(user, fields.getfirst('name'), machine)
+        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
         oldname = machine.name
-        command = "modify"
 
 
-        memory = fields.getfirst('memory')
-        if memory is not None:
-            memory = validation.validMemory(user, memory, machine, on=False)
-            machine.memory = memory
+        if hasattr(validate, 'memory'):
+            machine.memory = validate.memory
 
 
-        disksize = validation.testDisk(user, fields.getfirst('disk'))
-        if disksize is not None:
-            disksize = validation.validDisk(user, disksize, machine)
+        if hasattr(validate, 'vmtype'):
+            machine.type = validate.vmtype
+
+        if hasattr(validate, 'disksize'):
+            disksize = validate.disksize
             disk = machine.disks[0]
             if disk.size != disksize:
                 olddisk[disk.guest_device_name] = disksize
                 disk.size = disksize
             disk = machine.disks[0]
             if disk.size != disksize:
                 olddisk[disk.guest_device_name] = disksize
                 disk.size = disksize
-                ctx.current.save(disk)
-
-        if owner is not None:
-            machine.owner = owner
-        if name is not None:
-            machine.name = name
-        if admin is not None:
-            machine.administrator = admin
-        if contact is not None:
-            machine.contact = contact
-
-        ctx.current.save(machine)
-        transaction.commit()
+                session.save_or_update(disk)
+
+        update_acl = False
+        if hasattr(validate, 'owner') and validate.owner != machine.owner:
+            machine.owner = validate.owner
+            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
+
+        session.save_or_update(machine)
+        if update_acl:
+            cache_acls.refreshMachine(machine)
+        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]))
-    if name is not None:
-        controls.renameMachine(machine, oldname, name)
-    return dict(user=user,
-                command=command,
+    if hasattr(validate, 'name'):
+        controls.renameMachine(machine, oldname, validate.name)
+    return dict(user=username,
+                command="modify",
                 machine=machine)
 
                 machine=machine)
 
-def modify(user, fields):
+def modify(username, state, path, fields):
     """Handler for modifying attributes of a machine."""
     try:
     """Handler for modifying attributes of a machine."""
     try:
-        modify_dict = modifyDict(user, fields)
+        modify_dict = modifyDict(username, state, fields)
     except InvalidInput, err:
         result = None
     except InvalidInput, err:
         result = None
-        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
     else:
         machine = modify_dict['machine']
         result = 'Success!'
         err = None
     else:
         machine = modify_dict['machine']
         result = 'Success!'
         err = None
-    info_dict = infoDict(user, machine)
+    info_dict = infoDict(username, state, machine)
     info_dict['err'] = err
     if err:
         for field in fields.keys():
     info_dict['err'] = err
     if err:
         for field in fields.keys():
@@ -430,50 +426,78 @@ def modify(user, fields):
     return templates.info(searchList=[info_dict])
 
 
     return templates.info(searchList=[info_dict])
 
 
-def helpHandler(user, 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 = dict(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 sipb-xen-console.mit.edu, using the name of the machine as your
-username.""",
-                        hvm_paravm="""
+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 machines use the virtualization features of the processor, while
-ParaVM machines use Xen's emulation of virtualization features.  You
-want an HVM virtualized machine.""",
-                        cpu_weight="""
+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.""",
 Don't ask us!  We're as mystified as you are.""",
-                        owner="""
+                    'Owner': """
 The owner field is used to determine <a
 The owner field is used to determine <a
-href="help?subject=quotas">quotas</a>.  It must be the name of a
+href="help?subject=Quotas">quotas</a>.  It must be the name of a
 locker that you are an AFS administrator of.  In particular, you or an
 AFS group you are a member of must have AFS rlidwka bits on the
 locker.  You can check who administers the LOCKER locker using the
 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
 locker that you are an AFS administrator of.  In particular, you or an
 AFS group you are a member of must have AFS rlidwka bits on the
 locker.  You can check who administers the LOCKER locker using the
 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
-href="help?subject=administrator">administrator</a>.""",
-                        administrator="""
+href="help?subject=Administrator">administrator</a>.""",
+                    'Administrator': """
 The administrator field determines who can access the console and
 power on and off the machine.  This can be either a user or a moira
 group.""",
 The administrator field determines who can access the console and
 power on and off the machine.  This can be either a user or a moira
 group.""",
-                        quotas="""
+                    'Quotas': """
 Quotas are determined on a per-locker basis.  Each locker may have a
 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.""",
 active machines.""",
-                        console="""
+                    '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.
 <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.
 """
 """
-                   )
+                    }
 
     if not subjects:
         subjects = sorted(help_mapping.keys())
 
 
     if not subjects:
         subjects = sorted(help_mapping.keys())
 
-    d = dict(user=user,
+    d = dict(user=username,
              simple=simple,
              subjects=subjects,
              mapping=help_mapping)
              simple=simple,
              subjects=subjects,
              mapping=help_mapping)
@@ -481,11 +505,11 @@ console will suffer artifacts.
     return templates.help(searchList=[d])
 
 
     return templates.help(searchList=[d])
 
 
-def badOperation(u, 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(user, machine):
+def infoDict(username, state, machine):
     """Get the variables used by info.tmpl."""
     status = controls.statusInfo(machine)
     checkpoint.checkpoint('Getting status info')
     """Get the variables used by info.tmpl."""
     status = controls.statusInfo(machine)
     checkpoint.checkpoint('Getting status info')
@@ -497,14 +521,14 @@ def infoDict(user, 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'),
@@ -512,20 +536,16 @@ def infoDict(user, 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)'),
                       ('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'),
+                      ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
                       ]
     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
@@ -556,16 +576,16 @@ def infoDict(user, machine):
     checkpoint.checkpoint('Got fields')
 
 
     checkpoint.checkpoint('Got fields')
 
 
-    max_mem = validation.maxMemory(user, machine, False)
+    max_mem = validation.maxMemory(machine.owner, state, machine, False)
     checkpoint.checkpoint('Got mem')
     checkpoint.checkpoint('Got mem')
-    max_disk = validation.maxDisk(user, machine)
+    max_disk = validation.maxDisk(machine.owner, machine)
     defaults = Defaults()
     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))
         setattr(defaults, name, getattr(machine, name))
+    defaults.type = machine.type.type_id
     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
     checkpoint.checkpoint('Got defaults')
     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
     checkpoint.checkpoint('Got defaults')
-    d = dict(user=user,
-             cdroms=CDROM.select(),
+    d = dict(user=username,
              on=status is not None,
              machine=machine,
              defaults=defaults,
              on=status is not None,
              machine=machine,
              defaults=defaults,
@@ -574,24 +594,49 @@ def infoDict(user, machine):
              ram=machine.memory,
              max_mem=max_mem,
              max_disk=max_disk,
              ram=machine.memory,
              max_mem=max_mem,
              max_disk=max_disk,
-             owner_help=helppopup("owner"),
+             owner_help=helppopup("Owner"),
              fields = fields)
     return d
 
              fields = fields)
     return d
 
-def info(user, fields):
+def info(username, state, path, fields):
     """Handler for info on a single VM."""
     """Handler for info on a single VM."""
-    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
-    d = infoDict(user, machine)
+    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])
 
     checkpoint.checkpoint('Got infodict')
     return templates.info(searchList=[d])
 
+def unauthFront(_, _2, _3, fields):
+    """Information for unauth'd users."""
+    return templates.unauth(searchList=[{'simple' : True, 
+            'hostname' : socket.getfqdn()}])
+
+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,
 mapping = dict(list=listVms,
                vnc=vnc,
                command=command,
                modify=modify,
                info=info,
                create=create,
-               help=helpHandler)
+               help=helpHandler,
+               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."""
@@ -599,73 +644,129 @@ def printHeaders(headers):
         print '%s: %s' % (key, value)
     print
 
         print '%s: %s' % (key, value)
     print
 
+def send_error_mail(subject, body):
+    import subprocess
+
+    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 getUser():
+def getUser(environ):
     """Return the current user based on the SSL environment variables"""
     """Return the current user based on the SSL environment variables"""
-    username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
-    return username
-
-def main(operation, user, fields):
-    start_time = time.time()
-    fun = mapping.get(operation, badOperation)
-
-    if fun not in (helpHandler, ):
-        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
-    try:
-        checkpoint.checkpoint('Before')
-        output = fun(u, fields)
-        checkpoint.checkpoint('After')
-
-        headers = dict(DEFAULT_HEADERS)
-        if isinstance(output, tuple):
-            new_headers, output = output
-            headers.update(new_headers)
-        e = revertStandardError()
-        if e:
-            output.addError(e)
-        printHeaders(headers)
-        output_string =  str(output)
-        checkpoint.checkpoint('output as a string')
-        print output_string
-        print '<!-- <pre>%s</pre> -->' % checkpoint
-    except Exception, err:
-        if not fields.has_key('js'):
-            if isinstance(err, CodeError):
-                print 'Content-Type: text/html\n'
-                e = revertStandardError()
-                print error(operation, u, fields, err, e)
-                sys.exit(1)
-            if isinstance(err, InvalidInput):
-                print 'Content-Type: text/html\n'
-                e = revertStandardError()
-                print invalidInput(operation, u, fields, err, e)
-                sys.exit(1)
-        print 'Content-Type: text/plain\n'
-        print 'Uh-oh!  We experienced an error.'
-        print 'Please email sipb-xen@mit.edu with the contents of this page.'
-        print '----'
-        e = revertStandardError()
-        print e
-        print '----'
-        raise
-
-if __name__ == '__main__':
-    fields = cgi.FieldStorage()
-    u = getUser()
-    g.user = u
-    operation = os.environ.get('PATH_INFO', '')
-    if not operation:
-        print "Status: 301 Moved Permanently"
-        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
-        sys.exit(0)
+    user = environ.get('REMOTE_USER')
+    if user is None:
+        return
+    
+    if environ.get('AUTH_TYPE') == 'Negotiate':
+        # Convert the krb5 principal into a krb4 username
+        if not user.endswith('@%s' % config.kerberos.realm):
+            return
+        else:
+            return user.split('@')[0].replace('/', '.')
+    else:
+        return user
 
 
-    if operation.startswith('/'):
-        operation = operation[1:]
+def handler(username, state, path, fields):
+    operation, path = pathSplit(path)
     if not operation:
         operation = 'list'
     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):
+        self.environ = environ
+        self.start = start_response
+
+        self.username = getUser(environ)
+        self.state = State(self.username)
+        self.state.environ = environ
+
+        random.seed() #sigh
+
+    def __iter__(self):
+        start_time = time.time()
+        database.clear_cache()
+        sys.stderr = StringIO()
+        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
+        operation = self.environ.get('PATH_INFO', '')
+        if not operation:
+            self.start("301 Moved Permanently", [('Location', './')])
+            return
+        if self.username is None:
+            operation = 'unauth'
+
+        try:
+            checkpoint.checkpoint('Before')
+            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)
+            e = revertStandardError()
+            if 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'):
+                if isinstance(err, InvalidInput):
+                    self.start('200 OK', [('Content-Type', 'text/html')])
+                    e = revertStandardError()
+                    yield str(invalidInput(operation, self.username, fields,
+                                           err, e))
+                    return
+            import traceback
+            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())
+        yield output_string
+        if fields.has_key('timedebug'):
+            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
+
+def constructor():
+    connect()
+    return App
+
+def main():
+    from flup.server.fcgi_fork import WSGIServer
+    WSGIServer(constructor()).run()
 
 
-    if os.getenv("SIPB_XEN_PROFILE"):
-        import profile
-        profile.run('main(operation, u, fields)', 'log-'+operation)
-    else:
-        main(operation, u, fields)
+if __name__ == '__main__':
+    main()