Split main.py in four.
[invirt/packages/invirt-web.git] / templates / main.py
index b528b3e..ab70cdd 100755 (executable)
 #!/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 hmac
 import os
 import os
-import string
-import subprocess
-import re
-import time
-import cPickle
-import base64
 import sha
 import sha
-import hmac
-import datetime
-
-print 'Content-Type: text/html\n'
-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 *
-import random
-
-class MyException(Exception):
-    pass
-
-def helppopup(subj):
-    return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
-
-
-global_dict = {}
-global_dict['helppopup'] = helppopup
-
-
-# ... and stolen from xend/uuid.py
-def randomUUID():
-    """Generate a random UUID."""
-
-    return [ random.randint(0, 255) for _ in range(0, 16) ]
-
-def uuidToString(u):
-    return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
-                     "%02x" * 6]) % tuple(u)
-
-def maxMemory(user, machine=None):
-    return 256
-
-def maxDisk(user, machine=None):
-    return 10.0
-
-def haveAccess(user, machine):
-    if user.username == 'moo':
-        return True
-    return machine.owner == user.username
+import simplejson
+import sys
+import time
+from StringIO import StringIO
 
 
-def error(op, user, fields, err):
-    d = dict(op=op, user=user, errorMessage=str(err))
-    print Template(file='error.tmpl', searchList=[d, global_dict]);
 
 
-def validMachineName(name):
-    """Check that name is valid for a machine name"""
-    if not name:
-        return False
-    charset = string.ascii_letters + string.digits + '-_'
-    if name[0] in '-_' or len(name) > 22:
-        return False
-    return all(x in charset for x in name)
+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 kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
-    """Kinit with a given username and keytab"""
+def printError():
+    """Revert stderr to stdout, and print the contents of stderr"""
+    if isinstance(sys.stderr, StringIO):
+        print revertStandardError()
 
 
-    p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
-                         stderr=subprocess.PIPE)
-    e = p.wait()
-    if e:
-        raise MyException("Error %s in kinit: %s" % (e, p.stderr.read()))
+if __name__ == '__main__':
+    import atexit
+    atexit.register(printError)
+    sys.stderr = StringIO()
 
 
-def checkKinit():
-    """If we lack tickets, kinit."""
-    p = subprocess.Popen(['klist', '-s'])
-    if p.wait():
-        kinit()
+sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 
-def remctl(*args, **kws):
-    """Perform a remctl and return the output.
+from Cheetah.Template import Template
+from sipb_xen_database import Machine, CDROM, ctx, connect
+import validation
+from webcommon import InvalidInput, CodeError, g
+import controls
 
 
-    kinits if necessary, and outputs errors to stderr.
-    """
-    checkKinit()
-    p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
-                         + list(args),
-                         stdout=subprocess.PIPE,
-                         stderr=subprocess.PIPE)
-    if kws.get('err'):
-        return p.stdout.read(), p.stderr.read()
-    if p.wait():
-        print >> sys.stderr, 'ERROR on remctl ', args
-        print >> sys.stderr, p.stderr.read()
-    return p.stdout.read()
-
-def makeDisks():
-    """Update the lvm partitions to include all disks in the database."""
-    remctl('web', 'lvcreate')
-
-def bootMachine(machine, cdtype):
-    """Boot a machine with a given boot CD.
-
-    If cdtype is None, give no boot cd.  Otherwise, it is the string
-    id of the CD (e.g. 'gutsy_i386')
-    """
-    if cdtype is not None:
-        remctl('web', 'vmboot', machine.name,
-               cdtype)
+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 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:
     else:
-        remctl('web', 'vmboot', machine.name)
-
-def registerMachine(machine):
-    """Register a machine to be controlled by the web interface"""
-    remctl('web', 'register', machine.name)
-
-def unregisterMachine(machine):
-    """Unregister a machine to not be controlled by the web interface"""
-    remctl('web', 'unregister', machine.name)
-
-def parseStatus(s):
-    """Parse a status string into nested tuples of strings.
-
-    s = output of xm list --long <machine_name>
-    """
-    values = re.split('([()])', s)
-    stack = [[]]
-    for v in values[2:-2]: #remove initial and final '()'
-        if not v:
-            continue
-        v = v.strip()
-        if v == '(':
-            stack.append([])
-        elif v == ')':
-            if len(stack[-1]) == 1:
-                stack[-1].append('')
-            stack[-2].append(stack[-1])
-            stack.pop()
-        else:
-            if not v:
-                continue
-            stack[-1].extend(v.split())
-    return stack[-1]
-
-def getUptimes(machines):
-    """Return a dictionary mapping machine names to uptime strings"""
-    value_string = remctl('web', 'listvms')
-    lines = value_string.splitlines()
-    d = {}
-    for line in lines[1:]:
-        lst = line.split()
-        name, id = lst[:2]
-        uptime = ' '.join(lst[2:])
-        d[name] = uptime
-    return d
-
-def statusInfo(machine):
-    """Return the status list for a given machine.
-
-    Gets and parses xm list --long
-    """
-    value_string, err_string = remctl('list-long', machine.name, err=True)
-    if 'Unknown command' in err_string:
-        raise MyException("ERROR in remctl list-long %s is not registered" % (machine.name,))
-    elif 'does not exist' in err_string:
-        return None
-    elif err_string:
-        raise MyException("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
-    status = parseStatus(value_string)
-    return status
+        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'}
+
+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 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 hasVnc(status):
     """Does the machine with a given status list support VNC?"""
 
 def hasVnc(status):
     """Does the machine with a given status list support VNC?"""
@@ -183,140 +123,88 @@ def hasVnc(status):
             return 'location' in d
     return False
 
             return 'location' in d
     return False
 
-def createVm(user, name, memory, disk, is_hvm, cdrom):
-    """Create a VM and put it in the database"""
-    # put stuff in the table
-    transaction = ctx.current.create_transaction()
-    try:
-        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
-        machine.contact = user.email
-        machine.uuid = uuidToString(randomUUID())
-        machine.boot_off_cd = True
-        machine_type = Type.get_by(hvm=is_hvm)
-        machine.type_id = machine_type.type_id
-        ctx.current.save(machine)
-        disk = Disk(machine.machine_id, 
-                    'hda', disk)
-        open = NIC.select_by(machine_id=None)
-        if not open: #No IPs left!
-            return "No IP addresses left!  Contact sipb-xen-dev@mit.edu"
-        nic = open[0]
-        nic.machine_id = machine.machine_id
-        nic.hostname = name
-        ctx.current.save(nic)    
-        ctx.current.save(disk)
-        transaction.commit()
-    except:
-        transaction.rollback()
-        raise
-    makeDisks()
-    registerMachine(machine)
-    # tell it to boot with cdrom
-    bootMachine(machine, cdrom)
-
-    return machine
-
-def validMemory(user, memory, machine=None):
-    try:
-        memory = int(memory)
-        if memory <= 0:
-            raise ValueError
-    except ValueError:
-        raise MyException("Invalid memory amount")
-    if memory > maxMemory(user, machine):
-        raise MyException("Too much memory requested")
-    return memory
-
-def validDisk(user, disk, machine=None):
-    try:
-        disk = float(disk)
-        if disk > maxDisk(user, machine):
-            raise MyException("Too much disk requested")
-        disk = int(disk * 1024)
-        if disk <= 0:
-            raise ValueError
-    except ValueError:
-        raise MyException("Invalid disk amount")
-    return disk
-
-def create(user, fields):
+def parseCreate(user, fields):
     name = fields.getfirst('name')
     name = fields.getfirst('name')
-    if not validMachineName(name):
-        raise MyException("Invalid name '%s'" % name)
-    name = user.username + '_' + name.lower()
+    if not validation.validMachineName(name):
+        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 MyException("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 = validation.validMemory(user, memory, on=True)
     
     disk = fields.getfirst('disk')
     
     disk = fields.getfirst('disk')
-    disk = validDisk(user, disk)
+    disk = validation.validDisk(user, disk)
 
     vm_type = fields.getfirst('vmtype')
     if vm_type not in ('hvm', 'paravm'):
 
     vm_type = fields.getfirst('vmtype')
     if vm_type not in ('hvm', 'paravm'):
-        raise MyException("Invalid vm type '%s'"  % vm_type)    
+        raise CodeError("Invalid vm type '%s'"  % vm_type)    
     is_hvm = (vm_type == 'hvm')
 
     cdrom = fields.getfirst('cdrom')
     if cdrom is not None and not CDROM.get(cdrom):
     is_hvm = (vm_type == 'hvm')
 
     cdrom = fields.getfirst('cdrom')
     if cdrom is not None and not CDROM.get(cdrom):
-        raise MyException("Invalid cdrom type '%s'" % cdrom)    
-    
-    machine = createVm(user, name, memory, disk, is_hvm, cdrom)
-    if isinstance(machine, basestring):
-        raise MyException(machine)
-    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):
-    machines = [m for m in Machine.select() if haveAccess(user, m)]    
+def create(user, fields):
+    """Handler for create requests."""
+    try:
+        parsed_fields = parseCreate(user, fields)
+        machine = controls.createVm(**parsed_fields)
+    except InvalidInput, err:
+        pass
+    else:
+        err = None
+    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']
+    return Template(file='list.tmpl', searchList=[d])
+
+
+def getListDict(user):
+    machines = [m for m in Machine.select() 
+                if validation.haveAccess(user, m)]    
     on = {}
     has_vnc = {}
     on = {}
     has_vnc = {}
-    uptimes = getUptimes(machines)
-    on = uptimes
+    on = g.uptimes
     for m in machines:
     for m in machines:
-        if not on.get(m.name):
-            has_vnc[m.name] = 'Off'
+        m.uptime = g.uptimes.get(m)
+        if not on[m]:
+            has_vnc[m] = 'Off'
         elif m.type.hvm:
         elif m.type.hvm:
-            has_vnc[m.name] = True
+            has_vnc[m] = True
         else:
         else:
-            has_vnc[m.name] = "ParaVM"+helppopup("paravm_console")
-    #     for m in machines:
-    #         status = statusInfo(m)
-    #         on[m.name] = status is not None
-    #         has_vnc[m.name] = hasVnc(status)
+            has_vnc[m] = "ParaVM"+helppopup("paravm_console")
+    max_memory = validation.maxMemory(user)
+    max_disk = validation.maxDisk(user)
+    defaults = Defaults(max_memory=max_memory,
+                        max_disk=max_disk,
+                        cdrom='gutsy-i386')
     d = dict(user=user,
     d = dict(user=user,
-             maxmem=maxMemory(user),
-             maxdisk=maxDisk(user),
+             cant_add_vm=validation.cantAddVm(user),
+             max_memory=max_memory,
+             max_disk=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])
-
-def testMachineId(user, machineId, exists=True):
-    if machineId is None:
-        raise MyException("No machine ID specified")
-    try:
-        machineId = int(machineId)
-    except ValueError:
-        raise MyException("Invalid machine ID '%s'" % machineId)
-    machine = Machine.get(machineId)
-    if exists and machine is None:
-        raise MyException("No such machine ID '%s'" % machineId)
-    if not haveAccess(user, machine):
-        raise MyException("No access to machine ID '%s'" % machineId)
-    return machine
+    return d
 
 
+def listVms(user, fields):
+    """Handler for list requests."""
+    d = getListDict(user)
+    return Template(file='list.tmpl', searchList=[d])
+            
 def vnc(user, fields):
     """VNC applet page.
 
 def vnc(user, fields):
     """VNC applet page.
 
@@ -327,34 +215,48 @@ 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
     """
     """
-    machine = testMachineId(user, fields.getfirst('machine_id'))
-    #XXX fix
+    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
     
     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
 
     data = {}
     data["user"] = user.username
     
     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
 
     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)
     
+    status = controls.statusInfo(machine)
+    has_vnc = hasVnc(status)
+    
     d = dict(user=user,
     d = dict(user=user,
+             on=status,
+             has_vnc=has_vnc,
              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):
 
 def getNicInfo(data_dict, machine):
+    """Helper function for info, get data on nics for a machine.
+
+    Modifies data_dict to include the relevant data, and returns a list
+    of (key, name) pairs to display "name: data_dict[key]" to the user.
+    """
     data_dict['num_nics'] = len(machine.nics)
     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
                            ('nic%s_mac', 'NIC %s MAC Addr'),
     data_dict['num_nics'] = len(machine.nics)
     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
                            ('nic%s_mac', 'NIC %s MAC Addr'),
@@ -363,7 +265,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:
@@ -371,106 +274,194 @@ def getNicInfo(data_dict, machine):
     return nic_fields
 
 def getDiskInfo(data_dict, machine):
     return nic_fields
 
 def getDiskInfo(data_dict, machine):
+    """Helper function for info, get data on disks for a machine.
+
+    Modifies data_dict to include the relevant data, and returns a list
+    of (key, name) pairs to display "name: data_dict[key]" to the user.
+    """
     data_dict['num_disks'] = len(machine.disks)
     disk_fields_template = [('%s_size', '%s size')]
     disk_fields = []
     for disk in machine.disks:
         name = disk.guest_device_name
     data_dict['num_disks'] = len(machine.disks)
     disk_fields_template = [('%s_size', '%s size')]
     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
 
         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
     return disk_fields
 
-def deleteVM(machine):
+def command(user, fields):
+    """Handler for running commands like boot and delete on a VM."""
+    back = fields.getfirst('back')
+    try:
+        d = controls.commandResult(user, fields)
+        if d['command'] == 'Delete VM':
+            back = 'list'
+    except InvalidInput, err:
+        if not back:
+            raise
+        print >> sys.stderr, err
+        result = None
+    else:
+        result = 'Success!'
+        if not back:
+            return Template(file='command.tmpl', searchList=[d])
+    if back == 'list':
+        g.clear() #Changed global state
+        d = getListDict(user)
+        d['result'] = result
+        return Template(file='list.tmpl', searchList=[d])
+    elif back == 'info':
+        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+        d = infoDict(user, machine)
+        d['result'] = result
+        return Template(file='info.tmpl', searchList=[d])
+    else:
+        raise InvalidInput('back', back, 'Not a known back page.')
+
+def modifyDict(user, fields):
+    olddisk = {}
     transaction = ctx.current.create_transaction()
     transaction = ctx.current.create_transaction()
-    delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
     try:
     try:
-        for nic in machine.nics:
-            nic.machine_id = None
-            nic.hostname = None
+        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)
+        hostname = validation.testHostname(owner, fields.getfirst('hostname'),
+                                           machine)
+        name = validation.testName(user, fields.getfirst('name'), machine)
+        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
+        disksize = validation.testDisk(user, fields.getfirst('disk'))
+        if disksize is not None:
+            disksize = validation.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)
             ctx.current.save(nic)
-        for disk in machine.disks:
-            ctx.current.delete(disk)
-        ctx.current.delete(machine)
+
+        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
         transaction.commit()
     except:
         transaction.rollback()
         raise
-    for mname, dname in delete_disk_pairs:
-        remctl('web', 'lvremove', mname, dname)
-    unregisterMachine(machine)
-
-def command(user, fields):
-    print time.time()-start_time
-    machine = testMachineId(user, fields.getfirst('machine_id'))
-    action = fields.getfirst('action')
-    cdrom = fields.getfirst('cdrom')
-    print time.time()-start_time
-    if cdrom is not None and not CDROM.get(cdrom):
-        raise MyException("Invalid cdrom type '%s'" % cdrom)    
-    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
-        raise MyException("Invalid action '%s'" % action)
-    if action == 'Reboot':
-        if cdrom is not None:
-            remctl('reboot', machine.name, cdrom)
-        else:
-            remctl('reboot', machine.name)
-    elif action == 'Power on':
-        bootMachine(machine, cdrom)
-    elif action == 'Power off':
-        remctl('destroy', machine.name)
-    elif action == 'Shutdown':
-        remctl('shutdown', machine.name)
-    elif action == 'Delete VM':
-        deleteVM(machine)
-    print time.time()-start_time
-
-    d = dict(user=user,
-             command=action,
-             machine=machine)
-    print Template(file="command.tmpl", searchList=[d, global_dict])
-        
+    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,
+                machine=machine)
+    
 def modify(user, fields):
 def modify(user, fields):
-    machine = testMachineId(user, fields.getfirst('machine_id'))
+    """Handler for modifying attributes of a machine."""
+    try:
+        modify_dict = modifyDict(user, fields)
+    except InvalidInput, err:
+        result = None
+        machine = validation.testMachineId(user, fields.getfirst('machine_id'))
+    else:
+        machine = modify_dict['machine']
+        result = 'Success!'
+        err = None
+    info_dict = infoDict(user, machine)
+    info_dict['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(info_dict['defaults'], field, fields.getfirst(field))
+    info_dict['result'] = result
+    return Template(file='info.tmpl', searchList=[info_dict])
     
     
-def help(user, fields):
+
+def helpHandler(user, fields):
+    """Handler for help messages."""
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
     
     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):
-    machine = testMachineId(user, fields.getfirst('machine_id'))
-    status = statusInfo(machine)
+def badOperation(u, e):
+    raise CodeError("Unknown operation")
+
+def infoDict(user, machine):
+    status = controls.statusInfo(machine)
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
                            memory=str(machine.memory))
     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',
@@ -489,42 +480,60 @@ def info(user, fields):
                       ]
     fields = []
     machine_info = {}
                       ]
     fields = []
     machine_info = {}
+    machine_info['name'] = machine.name
     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
     machine_info['owner'] = machine.owner
     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]))
             fields.append((disp, locals()[field]))
-        elif field in main_status:
-            fields.append((disp, main_status[field]))
         elif field in machine_info:
             fields.append((disp, machine_info[field]))
         elif field in machine_info:
             fields.append((disp, machine_info[field]))
+        elif field in main_status:
+            fields.append((disp, main_status[field]))
         else:
             pass
             #fields.append((disp, None))
         else:
             pass
             #fields.append((disp, None))
-
+    max_mem = validation.maxMemory(user, machine)
+    max_disk = validation.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,
              has_vnc=has_vnc,
              uptime=str(uptime),
              ram=machine.memory,
-             maxmem=maxMemory(user, machine),
-             maxdisk=maxDisk(user, machine),
+             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 = validation.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,
@@ -532,32 +541,76 @@ 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')
+
+def main(operation, user, fields):    
+    fun = mapping.get(operation, badOperation)
+
+    if fun not in (helpHandler, ):
+        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
+    try:
+        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
 
 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.user = u
     operation = os.environ.get('PATH_INFO', '')
     if not operation:
     operation = os.environ.get('PATH_INFO', '')
     if not operation:
-        pass
-        #XXX do redirect
+        print "Status: 301 Moved Permanently"
+        print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
+        sys.exit(0)
 
     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')
-    try:
-        fun(u, fields)
-    except MyException, err:
-        error(operation, u, fields, err)
+
+    main(operation, u, fields)
+