A monster checkin, with a variety of changes to the web
[invirt/packages/invirt-web.git] / templates / main.py
index 8de8a37..6ec853d 100755 (executable)
@@ -1,24 +1,47 @@
 #!/usr/bin/python
 #!/usr/bin/python
+"""Main CGI script for web interface"""
 
 
-import sys
+import base64
+import cPickle
 import cgi
 import cgi
+import datetime
+import getafsgroups
+import hmac
 import os
 import os
+import random
+import re
+import sha
+import simplejson
 import string
 import subprocess
 import string
 import subprocess
-import re
+import sys
 import time
 import time
-import cPickle
-import base64
-import sha
-import hmac
-import datetime
+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
+    sys.stderr = sys.stdout
+    errio.seek(0)
+    return errio.read()
+
+def printError():
+    """Revert stderr to stdout, and print the contents of stderr"""
+    if isinstance(sys.stderr, StringIO):
+        print revertStandardError()
+
+if __name__ == '__main__':
+    import atexit
+    atexit.register(printError)
+    sys.stderr = StringIO()
 
 
-sys.stderr = sys.stdout
 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 from Cheetah.Template import Template
 from sipb_xen_database import *
 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 from Cheetah.Template import Template
 from sipb_xen_database import *
-import random
 
 class MyException(Exception):
     """Base class for my exceptions"""
 
 class MyException(Exception):
     """Base class for my exceptions"""
@@ -31,32 +54,92 @@ class InvalidInput(MyException):
     typo) but not setting an invalid boot CD (which requires bypassing
     the select box).
     """
     typo) but not setting an invalid boot CD (which requires bypassing
     the select box).
     """
-    pass
+    def __init__(self, err_field, err_value, expl=None):
+        MyException.__init__(self, expl)
+        self.err_field = err_field
+        self.err_value = err_value
 
 class CodeError(MyException):
     """Exception for internal errors or bad faith input."""
     pass
 
 
 class CodeError(MyException):
     """Exception for internal errors or bad faith input."""
     pass
 
+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>')
+
 class Global(object):
 class Global(object):
+    """Global state of the system, to avoid duplicate remctls to get state"""
     def __init__(self, user):
         self.user = user
 
     def __get_uptimes(self):
         if not hasattr(self, '_uptimes'):
     def __init__(self, user):
         self.user = user
 
     def __get_uptimes(self):
         if not hasattr(self, '_uptimes'):
-            self._uptimes = getUptimes(self.machines)
+            self._uptimes = getUptimes(Machine.select())
         return self._uptimes
     uptimes = property(__get_uptimes)
 
         return self._uptimes
     uptimes = property(__get_uptimes)
 
-g = None
-
-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>'
-
+    def clear(self):
+        """Clear the state so future accesses reload it."""
+        for attr in ('_uptimes', ):
+            if hasattr(self, attr):
+                delattr(self, attr)
 
 
-global_dict = {}
-global_dict['helppopup'] = helppopup
+g = None
 
 
+class User:
+    """User class (sort of useless, I admit)"""
+    def __init__(self, username, email):
+        self.username = username
+        self.email = email
+
+def makeErrorPre(old, addition):
+    if addition is None:
+        return
+    if old:
+        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
+    else:
+        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
+
+Template.helppopup = staticmethod(helppopup)
+Template.err = None
+
+class JsonDict:
+    """Class to store a dictionary that will be converted to JSON"""
+    def __init__(self, **kws):
+        self.data = kws
+        if 'err' in kws:
+            err = kws['err']
+            del kws['err']
+            self.addError(err)
+
+    def __str__(self):
+        return simplejson.dumps(self.data)
+
+    def addError(self, text):
+        """Add stderr text to be displayed on the website."""
+        self.data['err'] = \
+            makeErrorPre(self.data.get('err'), text)
+
+class Defaults:
+    """Class to store default values for fields."""
+    memory = 256
+    disk = 4.0
+    cdrom = ''
+    name = ''
+    vmtype = '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)
+        for key in kws:
+            setattr(self, key, kws[key])
+
+
+
+default_headers = {'Content-Type': 'text/html'}
 
 # ... and stolen from xend/uuid.py
 def randomUUID():
 
 # ... and stolen from xend/uuid.py
 def randomUUID():
@@ -78,47 +161,82 @@ MIN_DISK_SINGLE = 0.1
 MAX_VMS_TOTAL = 10
 MAX_VMS_ACTIVE = 4
 
 MAX_VMS_TOTAL = 10
 MAX_VMS_ACTIVE = 4
 
-def getMachinesByOwner(owner):
-    """Return the machines owned by a given owner."""
+def getMachinesByOwner(user, machine=None):
+    """Return the machines owned by the same as a machine.
+    
+    If the machine is None, return the machines owned by the same
+    user.
+    """
+    if machine:
+        owner = machine.owner
+    else:
+        owner = user.username
     return Machine.select_by(owner=owner)
 
     return Machine.select_by(owner=owner)
 
-def maxMemory(user, machine=None):
+def maxMemory(user, machine=None, on=True):
     """Return the maximum memory for a machine or a user.
 
     If machine is None, return the memory available for a new 
     machine.  Else, return the maximum that machine can have.
 
     """Return the maximum memory for a machine or a user.
 
     If machine is None, return the memory available for a new 
     machine.  Else, return the maximum that machine can have.
 
-    on is a dictionary from machines to booleans, whether a machine is
-    on.  If None, it is recomputed. XXX make this global?
+    on is whether the machine should be turned on.  If false, the max
+    memory for the machine to change to, if it is left off, is
+    returned.
     """
     """
-
-    machines = getMachinesByOwner(user.username)
+    if not on:
+        return MAX_MEMORY_SINGLE
+    machines = getMachinesByOwner(user, machine)
     active_machines = [x for x in machines if g.uptimes[x]]
     mem_usage = sum([x.memory for x in active_machines if x != machine])
     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
 
 def maxDisk(user, machine=None):
     active_machines = [x for x in machines if g.uptimes[x]]
     mem_usage = sum([x.memory for x in active_machines if x != machine])
     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
 
 def maxDisk(user, machine=None):
-    machines = getMachinesByOwner(user.username)
+    machines = getMachinesByOwner(user, machine)
     disk_usage = sum([sum([y.size for y in x.disks])
                       for x in machines if x != machine])
     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
 
     disk_usage = sum([sum([y.size for y in x.disks])
                       for x in machines if x != machine])
     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
 
-def canAddVm(user):
-    machines = getMachinesByOwner(user.username)
+def cantAddVm(user):
+    machines = getMachinesByOwner(user)
     active_machines = [x for x in machines if g.uptimes[x]]
     active_machines = [x for x in machines if g.uptimes[x]]
-    return (len(machines) < MAX_VMS_TOTAL and
-            len(active_machines) < MAX_VMS_ACTIVE)
+    if len(machines) >= MAX_VMS_TOTAL:
+        return 'You have too many VMs to create a new one.'
+    if len(active_machines) >= MAX_VMS_ACTIVE:
+        return ('You already have the maximum number of VMs turned on.  '
+                'To create more, turn one off.')
+    return False
 
 def haveAccess(user, machine):
 
 def haveAccess(user, machine):
-    """Return whether a user has access to a machine"""
+    """Return whether a user has adminstrative access to a machine"""
+    if user.username == 'moo':
+        return True
+    if user.username in (machine.administrator, machine.owner):
+        return True
+    if getafsgroups.checkAfsGroup(user.username, machine.administrator, 
+                                  'athena.mit.edu'): #XXX Cell?
+        return True
+    if getafsgroups.checkLockerOwner(user.username, machine.owner):
+        return True
+    return owns(user, machine)
+
+def owns(user, machine):
+    """Return whether a user owns a machine"""
     if user.username == 'moo':
         return True
     if user.username == 'moo':
         return True
-    return machine.owner == user.username
+    return getafsgroups.checkLockerOwner(user.username, machine.owner)
 
 
-def error(op, user, fields, err):
+def error(op, user, fields, err, emsg):
     """Print an error page when a CodeError occurs"""
     """Print an error page when a CodeError occurs"""
-    d = dict(op=op, user=user, errorMessage=str(err))
-    print Template(file='error.tmpl', searchList=[d, global_dict]);
+    d = dict(op=op, user=user, errorMessage=str(err),
+             stderr=emsg)
+    return Template(file='error.tmpl', searchList=[d]);
+
+def invalidInput(op, user, fields, err, emsg):
+    """Print an error page when an InvalidInput exception occurs"""
+    d = dict(op=op, user=user, err_field=err.err_field,
+             err_value=str(err.err_value), stderr=emsg,
+             errorMessage=str(err))
+    return Template(file='invalid.tmpl', searchList=[d]);
 
 def validMachineName(name):
     """Check that name is valid for a machine name"""
 
 def validMachineName(name):
     """Check that name is valid for a machine name"""
@@ -157,12 +275,13 @@ def remctl(*args, **kws):
                          + list(args),
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
                          + list(args),
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
+    v = p.wait()
     if kws.get('err'):
     if kws.get('err'):
-        p.wait()
         return p.stdout.read(), p.stderr.read()
         return p.stdout.read(), p.stderr.read()
-    if p.wait():
-        raise CodeError('ERROR on remctl %s: %s' %
-                          (args, p.stderr.read()))
+    if v:
+        print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
+        print >> sys.stderr, p.stderr.read()
+        raise CodeError('ERROR on remctl')
     return p.stdout.read()
 
 def lvcreate(machine, disk):
     return p.stdout.read()
 
 def lvcreate(machine, disk):
@@ -182,10 +301,10 @@ def bootMachine(machine, cdtype):
     id of the CD (e.g. 'gutsy_i386')
     """
     if cdtype is not None:
     id of the CD (e.g. 'gutsy_i386')
     """
     if cdtype is not None:
-        remctl('web', 'vmboot', machine.name,
+        remctl('control', machine.name, 'create', 
                cdtype)
     else:
                cdtype)
     else:
-        remctl('web', 'vmboot', machine.name)
+        remctl('control', machine.name, 'create')
 
 def registerMachine(machine):
     """Register a machine to be controlled by the web interface"""
 
 def registerMachine(machine):
     """Register a machine to be controlled by the web interface"""
@@ -219,7 +338,7 @@ def parseStatus(s):
             stack[-1].extend(v.split())
     return stack[-1]
 
             stack[-1].extend(v.split())
     return stack[-1]
 
-def getUptimes(machines):
+def getUptimes(machines=None):
     """Return a dictionary mapping machine names to uptime strings"""
     value_string = remctl('web', 'listvms')
     lines = value_string.splitlines()
     """Return a dictionary mapping machine names to uptime strings"""
     value_string = remctl('web', 'listvms')
     lines = value_string.splitlines()
@@ -239,13 +358,16 @@ def statusInfo(machine):
 
     Gets and parses xm list --long
     """
 
     Gets and parses xm list --long
     """
-    value_string, err_string = remctl('list-long', machine.name, err=True)
+    value_string, err_string = remctl('control', machine.name, 'list-long', 
+                                      err=True)
     if 'Unknown command' in err_string:
     if 'Unknown command' in err_string:
-        raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
+        raise CodeError("ERROR in remctl list-long %s is not registered" % 
+                        (machine.name,))
     elif 'does not exist' in err_string:
         return None
     elif err_string:
     elif 'does not exist' in err_string:
         return None
     elif err_string:
-        raise CodeError("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
+        raise CodeError("ERROR in remctl list-long %s:  %s" % 
+                        (machine.name, err_string))
     status = parseStatus(value_string)
     return status
 
     status = parseStatus(value_string)
     return status
 
@@ -265,18 +387,23 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
     transaction = ctx.current.create_transaction()
     try:
         if memory > maxMemory(user):
     transaction = ctx.current.create_transaction()
     try:
         if memory > maxMemory(user):
-            raise InvalidInput("Too much memory requested")
+            raise InvalidInput('memory', memory,
+                               "Max %s" % maxMemory(user))
         if disk > maxDisk(user) * 1024:
         if disk > maxDisk(user) * 1024:
-            raise InvalidInput("Too much disk requested")
-        if not canAddVm(user):
-            raise InvalidInput("Too many VMs requested")
-        res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
+            raise InvalidInput('disk', disk,
+                               "Max %s" % maxDisk(user))
+        reason = cantAddVm(user)
+        if reason:
+            raise InvalidInput('create', True, reason)
+        res = meta.engine.execute('select nextval('
+                                  '\'"machines_machine_id_seq"\')')
         id = res.fetchone()[0]
         machine = Machine()
         machine.machine_id = id
         machine.name = name
         machine.memory = memory
         machine.owner = user.username
         id = res.fetchone()[0]
         machine = Machine()
         machine.machine_id = id
         machine.name = name
         machine.memory = memory
         machine.owner = user.username
+        machine.administrator = user.username
         machine.contact = user.email
         machine.uuid = uuidToString(randomUUID())
         machine.boot_off_cd = True
         machine.contact = user.email
         machine.uuid = uuidToString(randomUUID())
         machine.boot_off_cd = True
@@ -285,10 +412,11 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
         ctx.current.save(machine)
         disk = Disk(machine.machine_id, 
                     'hda', disk)
         ctx.current.save(machine)
         disk = Disk(machine.machine_id, 
                     'hda', disk)
-        open = NIC.select_by(machine_id=None)
-        if not open: #No IPs left!
-            raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
-        nic = open[0]
+        open_nics = NIC.select_by(machine_id=None)
+        if not open_nics: #No IPs left!
+            raise CodeError("No IP addresses left!  "
+                            "Contact sipb-xen-dev@mit.edu")
+        nic = open_nics[0]
         nic.machine_id = machine.machine_id
         nic.hostname = name
         ctx.current.save(nic)    
         nic.machine_id = machine.machine_id
         nic.hostname = name
         ctx.current.save(nic)    
@@ -304,17 +432,22 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
 
     return machine
 
 
     return machine
 
-def validMemory(user, memory, machine=None):
-    """Parse and validate limits for memory for a given user and machine."""
+def validMemory(user, memory, machine=None, on=True):
+    """Parse and validate limits for memory for a given user and machine.
+
+    on is whether the memory must be valid after the machine is
+    switched on.
+    """
     try:
         memory = int(memory)
         if memory < MIN_MEMORY_SINGLE:
             raise ValueError
     except ValueError:
     try:
         memory = int(memory)
         if memory < MIN_MEMORY_SINGLE:
             raise ValueError
     except ValueError:
-        raise InvalidInput("Invalid memory amount; must be at least %s MB" %
-                          MIN_MEMORY_SINGLE)
-    if memory > maxMemory(user, machine):
-        raise InvalidInput("Too much memory requested")
+        raise InvalidInput('memory', memory, 
+                           "Minimum %s MB" % MIN_MEMORY_SINGLE)
+    if memory > maxMemory(user, machine, on):
+        raise InvalidInput('memory', memory,
+                           'Maximum %s MB' % maxMemory(user, machine))
     return memory
 
 def validDisk(user, disk, machine=None):
     return memory
 
 def validDisk(user, disk, machine=None):
@@ -322,27 +455,28 @@ def validDisk(user, disk, machine=None):
     try:
         disk = float(disk)
         if disk > maxDisk(user, machine):
     try:
         disk = float(disk)
         if disk > maxDisk(user, machine):
-            raise InvalidInput("Too much disk requested")
+            raise InvalidInput('disk', disk,
+                               "Maximum %s G" % maxDisk(user, machine))
         disk = int(disk * 1024)
         if disk < MIN_DISK_SINGLE * 1024:
             raise ValueError
     except ValueError:
         disk = int(disk * 1024)
         if disk < MIN_DISK_SINGLE * 1024:
             raise ValueError
     except ValueError:
-        raise InvalidInput("Invalid disk amount; minimum is %s GB" %
-                          MIN_DISK_SINGLE)
+        raise InvalidInput('disk', disk,
+                           "Minimum %s GB" % MIN_DISK_SINGLE)
     return disk
 
     return disk
 
-def create(user, fields):
-    """Handler for create requests."""
+def parseCreate(user, fields):
     name = fields.getfirst('name')
     if not validMachineName(name):
     name = fields.getfirst('name')
     if not validMachineName(name):
-        raise InvalidInput("Invalid name '%s'" % name)
-    name = user.username + '_' + name.lower()
+        raise InvalidInput('name', name, 'You must provide a machine name.')
+    name = name.lower()
 
     if Machine.get_by(name=name):
 
     if Machine.get_by(name=name):
-        raise InvalidInput("A machine named '%s' already exists" % name)
+        raise InvalidInput('name', name,
+                           "Name already exists.")
     
     memory = fields.getfirst('memory')
     
     memory = fields.getfirst('memory')
-    memory = validMemory(user, memory)
+    memory = validMemory(user, memory, on=True)
     
     disk = fields.getfirst('disk')
     disk = validDisk(user, disk)
     
     disk = fields.getfirst('disk')
     disk = validDisk(user, disk)
@@ -354,21 +488,45 @@ def create(user, fields):
 
     cdrom = fields.getfirst('cdrom')
     if cdrom is not None and not CDROM.get(cdrom):
 
     cdrom = fields.getfirst('cdrom')
     if cdrom is not None and not CDROM.get(cdrom):
-        raise CodeError("Invalid cdrom type '%s'" % cdrom)    
-    
-    machine = createVm(user, name, memory, disk, is_hvm, cdrom)
-    d = dict(user=user,
-             machine=machine)
-    print Template(file='create.tmpl',
-                   searchList=[d, global_dict]);
+        raise CodeError("Invalid cdrom type '%s'" % cdrom)
+    return dict(user=user, name=name, memory=memory, disk=disk,
+                is_hvm=is_hvm, cdrom=cdrom)
 
 
-def listVms(user, fields):
-    """Handler for list requests."""
+def create(user, fields):
+    """Handler for create requests."""
+    js = fields.getfirst('js')
+    try:
+        parsed_fields = parseCreate(user, fields)
+        machine = createVm(**parsed_fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+    else:
+        err = None
+        if not js:
+            d = dict(user=user,
+                     machine=machine)
+            return Template(file='create.tmpl', searchList=[d])
+    g.clear() #Changed global state
+    d = getListDict(user)
+    d['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(d['defaults'], field, fields.getfirst(field))
+    else:
+        d['new_machine'] = parsed_fields['name']
+    t = Template(file='list.tmpl', searchList=[d])
+    return JsonDict(createtable=t.createTable(),
+                    machinelist=t.machineList(d['machines']))
+
+
+def getListDict(user):
     machines = [m for m in Machine.select() if haveAccess(user, m)]    
     on = {}
     has_vnc = {}
     on = g.uptimes
     for m in machines:
     machines = [m for m in Machine.select() if haveAccess(user, m)]    
     on = {}
     has_vnc = {}
     on = g.uptimes
     for m in machines:
+        m.uptime = g.uptimes.get(m)
         if not on[m]:
             has_vnc[m] = 'Off'
         elif m.type.hvm:
         if not on[m]:
             has_vnc[m] = 'Off'
         elif m.type.hvm:
@@ -379,20 +537,38 @@ def listVms(user, fields):
     #         status = statusInfo(m)
     #         on[m.name] = status is not None
     #         has_vnc[m.name] = hasVnc(status)
     #         status = statusInfo(m)
     #         on[m.name] = status is not None
     #         has_vnc[m.name] = hasVnc(status)
-    max_mem=maxMemory(user)
-    max_disk=maxDisk(user)
+    max_memory = maxMemory(user)
+    max_disk = maxDisk(user)
+    defaults = Defaults(max_memory=max_memory,
+                        max_disk=max_disk,
+                        cdrom='gutsy-i386')
     d = dict(user=user,
     d = dict(user=user,
-             can_add_vm=canAddVm(user),
-             max_mem=max_mem,
+             cant_add_vm=cantAddVm(user),
+             max_memory=max_memory,
              max_disk=max_disk,
              max_disk=max_disk,
-             default_mem=max_mem,
-             default_disk=min(4.0, max_disk),
+             defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
              machines=machines,
              has_vnc=has_vnc,
-             uptimes=uptimes,
+             uptimes=g.uptimes,
              cdroms=CDROM.select())
              cdroms=CDROM.select())
-    print Template(file='list.tmpl', searchList=[d, global_dict])
+    return d
 
 
+def listVms(user, fields):
+    """Handler for list requests."""
+    d = getListDict(user)
+    t = Template(file='list.tmpl', searchList=[d])
+    js = fields.getfirst('js')
+    if not js:
+        return t
+    if js == 'machinelist':
+        return t.machineList(d['machines'])
+    elif js.startswith('machinerow-'):
+        request_machine_id = int(js.split('-')[1])
+        m = [x for x in d['machines'] if x.id == request_machine_id]
+        return t.machineRow(m)
+    elif js == 'createtable':
+        return t.createTable()
+            
 def testMachineId(user, machineId, exists=True):
     """Parse, validate and check authorization for a given machineId.
 
 def testMachineId(user, machineId, exists=True):
     """Parse, validate and check authorization for a given machineId.
 
@@ -421,9 +597,12 @@ def vnc(user, fields):
 
     You might want iptables like:
 
 
     You might want iptables like:
 
-    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
-    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142 
-    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
+    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
+      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
+    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
+      --dport 10003 -j SNAT --to-source 18.187.7.142 
+    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
+      --dport 10003 -j ACCEPT
 
     Remember to enable iptables!
     echo 1 > /proc/sys/net/ipv4/ip_forward
 
     Remember to enable iptables!
     echo 1 > /proc/sys/net/ipv4/ip_forward
@@ -434,12 +613,12 @@ def vnc(user, fields):
 
     data = {}
     data["user"] = user.username
 
     data = {}
     data["user"] = user.username
-    data["machine"]=machine.name
-    data["expires"]=time.time()+(5*60)
-    pickledData = cPickle.dumps(data)
+    data["machine"] = machine.name
+    data["expires"] = time.time()+(5*60)
+    pickled_data = cPickle.dumps(data)
     m = hmac.new(TOKEN_KEY, digestmod=sha)
     m = hmac.new(TOKEN_KEY, digestmod=sha)
-    m.update(pickledData)
-    token = {'data': pickledData, 'digest': m.digest()}
+    m.update(pickled_data)
+    token = {'data': pickled_data, 'digest': m.digest()}
     token = cPickle.dumps(token)
     token = base64.urlsafe_b64encode(token)
     
     token = cPickle.dumps(token)
     token = base64.urlsafe_b64encode(token)
     
@@ -452,8 +631,7 @@ def vnc(user, fields):
              machine=machine,
              hostname=os.environ.get('SERVER_NAME', 'localhost'),
              authtoken=token)
              machine=machine,
              hostname=os.environ.get('SERVER_NAME', 'localhost'),
              authtoken=token)
-    print Template(file='vnc.tmpl',
-                   searchList=[d, global_dict])
+    return Template(file='vnc.tmpl', searchList=[d])
 
 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.
@@ -469,7 +647,8 @@ 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])
-        data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
+        data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname + 
+                                           '.servers.csail.mit.edu')
         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:
@@ -487,14 +666,17 @@ def getDiskInfo(data_dict, machine):
     disk_fields = []
     for disk in machine.disks:
         name = disk.guest_device_name
     disk_fields = []
     for disk in machine.disks:
         name = disk.guest_device_name
-        disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
+        disk_fields.extend([(x % name, y % name) for x, y in 
+                            disk_fields_template])
         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
     return disk_fields
 
 def deleteVM(machine):
     """Delete a VM."""
         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
     return disk_fields
 
 def deleteVM(machine):
     """Delete a VM."""
+    remctl('control', machine.name, 'destroy', err=True)
     transaction = ctx.current.create_transaction()
     transaction = ctx.current.create_transaction()
-    delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
+    delete_disk_pairs = [(machine.name, d.guest_device_name) 
+                         for d in machine.disks]
     try:
         for nic in machine.nics:
             nic.machine_id = None
     try:
         for nic in machine.nics:
             nic.machine_id = None
@@ -511,85 +693,305 @@ def deleteVM(machine):
         remctl('web', 'lvremove', mname, dname)
     unregisterMachine(machine)
 
         remctl('web', 'lvremove', mname, dname)
     unregisterMachine(machine)
 
-def command(user, fields):
-    """Handler for running commands like boot and delete on a VM."""
-    print time.time()-start_time
+def commandResult(user, fields):
+    print >> sys.stderr, time.time()-start_time
     machine = testMachineId(user, fields.getfirst('machine_id'))
     action = fields.getfirst('action')
     cdrom = fields.getfirst('cdrom')
     machine = testMachineId(user, fields.getfirst('machine_id'))
     action = fields.getfirst('action')
     cdrom = fields.getfirst('cdrom')
-    print time.time()-start_time
+    print >> sys.stderr, time.time()-start_time
     if cdrom is not None and not CDROM.get(cdrom):
         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
     if cdrom is not None and not CDROM.get(cdrom):
         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
-    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
+    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
+                      'Delete VM'):
         raise CodeError("Invalid action '%s'" % action)
     if action == 'Reboot':
         if cdrom is not None:
         raise CodeError("Invalid action '%s'" % action)
     if action == 'Reboot':
         if cdrom is not None:
-            remctl('reboot', machine.name, cdrom)
+            out, err = remctl('control', machine.name, 'reboot', cdrom,
+                              err=True)
         else:
         else:
-            remctl('reboot', machine.name)
+            out, err = remctl('control', machine.name, 'reboot',
+                              err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "reboot", 
+                                   "Machine is not on")
+            else:
+                print >> sys.stderr, 'Error on reboot:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
+                
     elif action == 'Power on':
         if maxMemory(user) < machine.memory:
     elif action == 'Power on':
         if maxMemory(user) < machine.memory:
-            raise InvalidInput("You don't have enough free RAM quota")
+            raise InvalidInput('action', 'Power on',
+                               "You don't have enough free RAM quota "
+                               "to turn on this machine.")
         bootMachine(machine, cdrom)
     elif action == 'Power off':
         bootMachine(machine, cdrom)
     elif action == 'Power off':
-        remctl('destroy', machine.name)
+        out, err = remctl('control', machine.name, 'destroy', err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "Power off", 
+                                   "Machine is not on.")
+            else:
+                print >> sys.stderr, 'Error on power off:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
     elif action == 'Shutdown':
     elif action == 'Shutdown':
-        remctl('shutdown', machine.name)
+        out, err = remctl('control', machine.name, 'shutdown', err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "Shutdown", 
+                                   "Machine is not on.")
+            else:
+                print >> sys.stderr, 'Error on Shutdown:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
     elif action == 'Delete VM':
         deleteVM(machine)
     elif action == 'Delete VM':
         deleteVM(machine)
-    print time.time()-start_time
+    print >> sys.stderr, time.time()-start_time
 
     d = dict(user=user,
              command=action,
              machine=machine)
 
     d = dict(user=user,
              command=action,
              machine=machine)
-    print Template(file="command.tmpl", searchList=[d, global_dict])
+    return d
+
+def command(user, fields):
+    """Handler for running commands like boot and delete on a VM."""
+    js = fields.getfirst('js')
+    try:
+        d = commandResult(user, fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+        result = None
+    else:
+        err = None
+        result = 'Success!'
+        if not js:
+            return Template(file='command.tmpl', searchList=[d])
+    if js == 'list':
+        g.clear() #Changed global state
+        d = getListDict(user)
+        t = Template(file='list.tmpl', searchList=[d])
+        return JsonDict(createtable=t.createTable(),
+                        machinelist=t.machineList(d['machines']),
+                        result=result,
+                        err=err)
+    elif js == 'info':
+        machine = testMachineId(user, fields.getfirst('machine_id'))
+        d = infoDict(user, machine)
+        t = Template(file='info.tmpl', searchList=[d])
+        return JsonDict(info=t.infoTable(),
+                        commands=t.commands(),
+                        modify=t.modifyForm(),
+                        result=result,
+                        err=err)
+    else:
+        raise InvalidInput('js', js, 'Not a known js type.')
+
+def testAdmin(user, admin, machine):
+    if admin in (None, machine.administrator):
+        return None
+    if admin == user.username:
+        return admin
+    if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
+        return admin
+    if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
+                                  'athena.mit.edu'):
+        return 'system:'+admin
+    raise InvalidInput('administrator', admin, 
+                       'You must control the group you move it to.')
+    
+def testOwner(user, owner, machine):
+    if owner in (None, machine.owner):
+        return None
+    value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
+    if value == True:
+        return owner
+    raise InvalidInput('owner', owner, value)
+
+def testContact(user, contact, machine=None):
+    if contact in (None, machine.contact):
+        return None
+    if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
+        raise InvalidInput('contact', contact, "Not a valid email.")
+    return contact
+
+def testDisk(user, disksize, machine=None):
+    return disksize
+
+def testName(user, name, machine=None):
+    if name in (None, machine.name):
+        return None
+    if not Machine.select_by(name=name):
+        return name
+    raise InvalidInput('name', name, "Name is already taken.")
+
+def testHostname(user, hostname, machine):
+    for nic in machine.nics:
+        if hostname == nic.hostname:
+            return hostname
+    # check if doesn't already exist
+    if NIC.select_by(hostname=hostname):
+        raise InvalidInput('hostname', hostname,
+                           "Already exists")
+    if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
+        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
+                           "must only use number, letters, and dashes.")
+    return hostname
+
+def modifyDict(user, fields):
+    olddisk = {}
+    transaction = ctx.current.create_transaction()
+    try:
+        machine = testMachineId(user, fields.getfirst('machine_id'))
+        owner = testOwner(user, fields.getfirst('owner'), machine)
+        admin = testAdmin(user, fields.getfirst('administrator'), machine)
+        contact = testContact(user, fields.getfirst('contact'), machine)
+        hostname = testHostname(owner, fields.getfirst('hostname'), machine)
+        name = testName(user, fields.getfirst('name'), machine)
+        oldname = machine.name
+        command = "modify"
+
+        memory = fields.getfirst('memory')
+        if memory is not None:
+            memory = validMemory(user, memory, machine, on=False)
+            machine.memory = memory
+        disksize = testDisk(user, fields.getfirst('disk'))
+        if disksize is not None:
+            disksize = validDisk(user, disksize, machine)
+            disk = machine.disks[0]
+            if disk.size != disksize:
+                olddisk[disk.guest_device_name] = disksize
+                disk.size = disksize
+                ctx.current.save(disk)
         
         
+        # XXX first NIC gets hostname on change?  
+        # Interface doesn't support more.
+        for nic in machine.nics[:1]:
+            nic.hostname = hostname
+            ctx.current.save(nic)
+
+        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()
+    except:
+        transaction.rollback()
+        raise
+    for diskname in olddisk:
+        remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
+    if name is not None:
+        for disk in machine.disks:
+            remctl("web", "lvrename", oldname, disk.guest_device_name, name)
+        remctl("web", "moveregister", oldname, name)
+    return dict(user=user,
+                command=command,
+                machine=machine)
+    
 def modify(user, fields):
     """Handler for modifying attributes of a machine."""
 def modify(user, fields):
     """Handler for modifying attributes of a machine."""
-    #XXX not written yet
-    machine = testMachineId(user, fields.getfirst('machine_id'))
+    js = fields.getfirst('js')
+    try:
+        modify_dict = modifyDict(user, fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+        result = ''
+        machine = testMachineId(user, fields.getfirst('machine_id'))
+    else:
+        machine = modify_dict['machine']
+        result='Success!'
+        err = None
+        if not js:
+            return Template(file='command.tmpl', searchList=[modify_dict])
+    info_dict = infoDict(user, machine)
+    info_dict['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(info_dict['defaults'], field, fields.getfirst(field))
+    t = Template(file='info.tmpl', searchList=[info_dict])
+    return JsonDict(info=t.infoTable(),
+                    commands=t.commands(),
+                    modify=t.modifyForm(),
+                    result=result,
+                    err=err)
     
     
-def help(user, fields):
+
+def helpHandler(user, 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')
     
-    mapping = dict(paravm_console="""
+    help_mapping = dict(paravm_console="""
 ParaVM machines do not support console access over VNC.  To access
 these machines, you either need to boot with a liveCD and ssh in or
 hope that the sipb-xen maintainers add support for serial consoles.""",
 ParaVM machines do not support console access over VNC.  To access
 these machines, you either need to boot with a liveCD and ssh in or
 hope that the sipb-xen maintainers add support for serial consoles.""",
-                   hvm_paravm="""
+                        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.""",
 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="""Don't ask us!  We're as mystified as you are.""")
+                        cpu_weight="""
+Don't ask us!  We're as mystified as you are.""",
+                        owner="""
+The owner field is used to determine <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 see who administers the LOCKER locker using the
+command 'fs la /mit/LOCKER' on Athena.)  See also <a
+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.""",
+                        quotas="""
+Quotas are determined on a per-locker basis.  Each quota may have a
+maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
+active machines."""
+                   )
     
     
+    if not subjects:
+        subjects = sorted(help_mapping.keys())
+        
     d = dict(user=user,
              simple=simple,
              subjects=subjects,
     d = dict(user=user,
              simple=simple,
              subjects=subjects,
-             mapping=mapping)
+             mapping=help_mapping)
     
     
-    print Template(file="help.tmpl", searchList=[d, global_dict])
+    return Template(file="help.tmpl", searchList=[d])
     
 
     
 
-def info(user, fields):
-    """Handler for info on a single VM."""
-    machine = testMachineId(user, fields.getfirst('machine_id'))
+def badOperation(u, e):
+    raise CodeError("Unknown operation")
+
+def infoDict(user, machine):
     status = statusInfo(machine)
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
                            memory=str(machine.memory))
     status = statusInfo(machine)
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
                            memory=str(machine.memory))
+        uptime = None
+        cputime = None
     else:
         main_status = dict(status[1:])
     else:
         main_status = dict(status[1:])
-    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))
+        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))
     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'),
                       ('owner', 'Owner'),
     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'),
                       ('owner', 'Owner'),
+                      ('administrator', 'Administrator'),
                       ('contact', 'Contact'),
                       ('type', 'Type'),
                       'NIC_INFO',
                       ('contact', 'Contact'),
                       ('type', 'Type'),
                       'NIC_INFO',
@@ -611,19 +1013,22 @@ def info(user, fields):
     machine_info['name'] = machine.name
     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
     machine_info['owner'] = machine.owner
     machine_info['name'] = machine.name
     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
     machine_info['owner'] = machine.owner
+    machine_info['administrator'] = machine.administrator
     machine_info['contact'] = machine.contact
 
     nic_fields = getNicInfo(machine_info, machine)
     nic_point = display_fields.index('NIC_INFO')
     machine_info['contact'] = machine.contact
 
     nic_fields = getNicInfo(machine_info, machine)
     nic_point = display_fields.index('NIC_INFO')
-    display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
+    display_fields = (display_fields[:nic_point] + nic_fields + 
+                      display_fields[nic_point+1:])
 
     disk_fields = getDiskInfo(machine_info, machine)
     disk_point = display_fields.index('DISK_INFO')
 
     disk_fields = getDiskInfo(machine_info, machine)
     disk_point = display_fields.index('DISK_INFO')
-    display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
+    display_fields = (display_fields[:disk_point] + disk_fields + 
+                      display_fields[disk_point+1:])
     
     main_status['memory'] += ' MB'
     for field, disp in display_fields:
     
     main_status['memory'] += ' MB'
     for field, disp in display_fields:
-        if field in ('uptime', 'cputime'):
+        if field in ('uptime', 'cputime') and locals()[field] is not None:
             fields.append((disp, locals()[field]))
         elif field in machine_info:
             fields.append((disp, machine_info[field]))
             fields.append((disp, locals()[field]))
         elif field in machine_info:
             fields.append((disp, machine_info[field]))
@@ -634,18 +1039,31 @@ def info(user, fields):
             #fields.append((disp, None))
     max_mem = maxMemory(user, machine)
     max_disk = maxDisk(user, machine)
             #fields.append((disp, None))
     max_mem = maxMemory(user, machine)
     max_disk = maxDisk(user, machine)
+    defaults=Defaults()
+    for name in 'machine_id name administrator owner memory contact'.split():
+        setattr(defaults, name, getattr(machine, name))
+    if machine.nics:
+        defaults.hostname = machine.nics[0].hostname
+    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
     d = dict(user=user,
              cdroms=CDROM.select(),
              on=status is not None,
              machine=machine,
     d = dict(user=user,
              cdroms=CDROM.select(),
              on=status is not None,
              machine=machine,
+             defaults=defaults,
              has_vnc=has_vnc,
              uptime=str(uptime),
              ram=machine.memory,
              max_mem=max_mem,
              max_disk=max_disk,
              has_vnc=has_vnc,
              uptime=str(uptime),
              ram=machine.memory,
              max_mem=max_mem,
              max_disk=max_disk,
+             owner_help=helppopup("owner"),
              fields = fields)
              fields = fields)
-    print Template(file='info.tmpl',
-                   searchList=[d, global_dict])
+    return d
+
+def info(user, fields):
+    """Handler for info on a single VM."""
+    machine = testMachineId(user, fields.getfirst('machine_id'))
+    d = infoDict(user, machine)
+    return Template(file='info.tmpl', searchList=[d])
 
 mapping = dict(list=listVms,
                vnc=vnc,
 
 mapping = dict(list=listVms,
                vnc=vnc,
@@ -653,47 +1071,74 @@ mapping = dict(list=listVms,
                modify=modify,
                info=info,
                create=create,
                modify=modify,
                info=info,
                create=create,
-               help=help)
+               help=helpHandler)
+
+def printHeaders(headers):
+    for key, value in headers.iteritems():
+        print '%s: %s' % (key, value)
+    print
+
+
+def getUser():
+    """Return the current user based on the SSL environment variables"""
+    if 'SSL_CLIENT_S_DN_Email' in os.environ:
+        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
+        return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
+    else:
+        return User('moo', 'nobody')
 
 if __name__ == '__main__':
     start_time = time.time()
     fields = cgi.FieldStorage()
 
 if __name__ == '__main__':
     start_time = time.time()
     fields = cgi.FieldStorage()
-    class User:
-        username = "moo"
-        email = 'moo@cow.com'
-    u = User()
+    u = getUser()
     g = Global(u)
     g = Global(u)
-    if 'SSL_CLIENT_S_DN_Email' in os.environ:
-        username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
-        u.username = username
-        u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
-    else:
-        u.username = 'moo'
-        u.email = 'nobody'
-    connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
     operation = os.environ.get('PATH_INFO', '')
     operation = os.environ.get('PATH_INFO', '')
-    #print 'Content-Type: text/plain\n'
-    #print operation
     if not operation:
         print "Status: 301 Moved Permanently"
         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
         sys.exit(0)
     if not operation:
         print "Status: 301 Moved Permanently"
         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
         sys.exit(0)
-    print 'Content-Type: text/html\n'
 
     if operation.startswith('/'):
         operation = operation[1:]
     if not operation:
         operation = 'list'
 
     if operation.startswith('/'):
         operation = operation[1:]
     if not operation:
         operation = 'list'
-    
-    fun = mapping.get(operation, 
-                      lambda u, e:
-                          error(operation, u, e,
-                                "Invalid operation '%s'" % operation))
-    if fun not in (help, ):
-        connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
+
+
+
+    fun = mapping.get(operation, badOperation)
+
+    if fun not in (helpHandler, ):
+        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
     try:
     try:
-        fun(u, fields)
-    except CodeError, err:
-        error(operation, u, fields, err)
-    except InvalidInput, err:
-        error(operation, u, fields, err)
+        output = fun(u, fields)
+
+        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)
+        print output
+    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