Remove the useless User class (now user is a string)
[invirt/packages/invirt-web.git] / templates / main.py
index 152cd75..0bf16b9 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
-
-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
+import simplejson
+import sys
+import time
+from StringIO import StringIO
 
 
-class MyException(Exception):
-    """Base class for my exceptions"""
-    pass
 
 
-class InvalidInput(MyException):
-    """Exception for user-provided input is invalid but maybe in good faith.
+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()
 
 
-    This would include setting memory to negative (which might be a
-    typo) but not setting an invalid boot CD (which requires bypassing
-    the select box).
-    """
-    pass
+def printError():
+    """Revert stderr to stdout, and print the contents of stderr"""
+    if isinstance(sys.stderr, StringIO):
+        print revertStandardError()
 
 
-class CodeError(MyException):
-    """Exception for internal errors or bad faith input."""
-    pass
+if __name__ == '__main__':
+    import atexit
+    atexit.register(printError)
+    sys.stderr = StringIO()
 
 
+sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 
+from Cheetah.Template import Template
+from sipb_xen_database import Machine, CDROM, ctx, connect
+import validation
+from webcommon import InvalidInput, CodeError, g
+import controls
 
 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>'
-
-
-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):
-    """Turn a numeric UUID to a hyphen-seperated one."""
-    return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
-                     "%02x" * 6]) % tuple(u)
-
-MAX_MEMORY_TOTAL = 512
-MAX_MEMORY_SINGLE = 256
-MIN_MEMORY_SINGLE = 16
-MAX_DISK_TOTAL = 50
-MAX_DISK_SINGLE = 50
-MIN_DISK_SINGLE = 0.1
-MAX_VMS_TOTAL = 10
-MAX_VMS_ACTIVE = 4
-
-def getMachinesByOwner(owner):
-    """Return the machines owned by a given owner."""
-    return Machine.select_by(owner=owner)
-
-def maxMemory(user, machine=None, on=None):
-    """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?
-    """
-
-    machines = getMachinesByOwner(user.username)
-    if on is None:
-        on = getUptimes(machines)
-    active_machines = [x for x in machines if on[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)
-    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, on=None):
-    machines = getMachinesByOwner(user.username)
-    if on is None:
-        on = getUptimes(machines)
-    active_machines = [x for x in machines if on[x]]
-    return (len(machines) < MAX_VMS_TOTAL and
-            len(active_machines) < MAX_VMS_ACTIVE)
-
-def haveAccess(user, machine):
-    """Return whether a user has access to a machine"""
-    if user.username == 'moo':
-        return True
-    return machine.owner == user.username
-
-def error(op, user, fields, err):
-    """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]);
-
-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
-    for x in name:
-        if x not in charset:
-            return False
-    return True
-
-def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
-    """Kinit with a given username and keytab"""
-
-    p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
-                         stderr=subprocess.PIPE)
-    e = p.wait()
-    if e:
-        raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
-
-def checkKinit():
-    """If we lack tickets, kinit."""
-    p = subprocess.Popen(['klist', '-s'])
-    if p.wait():
-        kinit()
-
-def remctl(*args, **kws):
-    """Perform a remctl and return the output.
-
-    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'):
-        p.wait()
-        return p.stdout.read(), p.stderr.read()
-    if p.wait():
-        raise CodeError('ERROR on remctl %s: %s' %
-                          (args, 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)
+    return ('<span class="helplink"><a href="help?subject=' + subj + 
+            '&amp;simple=true" target="_blank" ' + 
+            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
+
+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
-    ans = {}
-    for m in machines:
-        ans[m] = d.get(m.name)
-    return ans
-
-def statusInfo(machine):
-    """Return the status list for a given machine.
+        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])
 
 
-    Gets and parses xm list --long
-    """
-    value_string, err_string = remctl('list-long', machine.name, err=True)
-    if 'Unknown command' in err_string:
-        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:
-        raise CodeError("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
-    status = parseStatus(value_string)
-    return status
+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?"""
@@ -247,93 +117,23 @@ 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:
-        if memory > maxMemory(user):
-            raise InvalidInput("Too much memory requested")
-        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"\')')
-        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!
-            raise CodeError("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
-    registerMachine(machine)
-    makeDisks()
-    # tell it to boot with cdrom
-    bootMachine(machine, cdrom)
-
-    return machine
-
-def validMemory(user, memory, machine=None):
-    """Parse and validate limits for memory for a given user and machine."""
-    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")
-    return memory
-
-def validDisk(user, disk, machine=None):
-    """Parse and validate limits for disk for a given user and machine."""
-    try:
-        disk = float(disk)
-        if disk > maxDisk(user, machine):
-            raise InvalidInput("Too much disk requested")
-        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)
-    return disk
-
-def create(user, fields):
-    """Handler for create requests."""
+def parseCreate(user, fields):
     name = fields.getfirst('name')
     name = fields.getfirst('name')
-    if not validMachineName(name):
-        raise InvalidInput("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 InvalidInput("A machine named '%s' already exists" % name)
+        raise InvalidInput('name', name,
+                           "Name already exists.")
     
     
+    owner = validation.testOwner(user, fields.getfirst('owner'))
+
     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'):
@@ -342,64 +142,66 @@ 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(contact=user, name=name, memory=memory, disk=disk,
+                owner=owner, is_hvm=is_hvm, cdrom=cdrom)
 
 
-def listVms(user, fields):
-    """Handler for list requests."""
-    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:
+        m.uptime = g.uptimes.get(m)
         if not on[m]:
             has_vnc[m] = 'Off'
         elif m.type.hvm:
             has_vnc[m] = True
         else:
             has_vnc[m] = "ParaVM"+helppopup("paravm_console")
         if not on[m]:
             has_vnc[m] = 'Off'
         elif m.type.hvm:
             has_vnc[m] = True
         else:
             has_vnc[m] = "ParaVM"+helppopup("paravm_console")
-    #     for m in machines:
-    #         status = statusInfo(m)
-    #         on[m.name] = status is not None
-    #         has_vnc[m.name] = hasVnc(status)
-    max_mem=maxMemory(user, on=on)
-    max_disk=maxDisk(user)
+    max_memory = validation.maxMemory(user)
+    max_disk = validation.maxDisk(user)
+    defaults = Defaults(max_memory=max_memory,
+                        max_disk=max_disk,
+                        owner=user,
+                        cdrom='gutsy-i386')
     d = dict(user=user,
     d = dict(user=user,
-             can_add_vm=canAddVm(user, on=on),
-             max_mem=max_mem,
+             cant_add_vm=validation.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])
-
-def testMachineId(user, machineId, exists=True):
-    """Parse, validate and check authorization for a given machineId.
-
-    If exists is False, don't check that it exists.
-    """
-    if machineId is None:
-        raise CodeError("No machine ID specified")
-    try:
-        machineId = int(machineId)
-    except ValueError:
-        raise CodeError("Invalid machine ID '%s'" % machineId)
-    machine = Machine.get(machineId)
-    if exists and machine is None:
-        raise CodeError("No such machine ID '%s'" % machineId)
-    if machine is not None and not haveAccess(user, machine):
-        raise CodeError("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.
 
@@ -410,34 +212,41 @@ 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
     """
-    machine = testMachineId(user, fields.getfirst('machine_id'))
+    machine = validation.testMachineId(user, fields.getfirst('machine_id'))
     
     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
 
     data = {}
     
     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
 
     data = {}
-    data["user"] = user.username
-    data["machine"]=machine.name
-    data["expires"]=time.time()+(5*60)
-    pickledData = cPickle.dumps(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 = 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):
     """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.
@@ -446,14 +255,16 @@ def getNicInfo(data_dict, machine):
     of (key, name) pairs to display "name: data_dict[key]" to the user.
     """
     data_dict['num_nics'] = len(machine.nics)
     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_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
                            ('nic%s_mac', 'NIC %s MAC Addr'),
                            ('nic%s_ip', 'NIC %s IP'),
                            ]
     nic_fields = []
     for i in range(len(machine.nics)):
         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
                            ('nic%s_mac', 'NIC %s MAC Addr'),
                            ('nic%s_ip', 'NIC %s IP'),
                            ]
     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'
+        if not i:
+            data_dict['nic%s_hostname' % i] = (machine.name + 
+                                               '.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:
@@ -471,109 +282,176 @@ 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])
-        data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
+        disk_fields.extend([(x % name, y % name) for x, y in 
+                            disk_fields_template])
+        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
     return disk_fields
 
     return disk_fields
 
-def deleteVM(machine):
-    """Delete a VM."""
+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
-            ctx.current.save(nic)
-        for disk in machine.disks:
-            ctx.current.delete(disk)
-        ctx.current.delete(machine)
+        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)
+        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)
+        
+        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):
-    """Handler for running commands like boot and delete on a VM."""
-    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 CodeError("Invalid cdrom type '%s'" % cdrom)    
-    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:
-            remctl('reboot', machine.name, cdrom)
-        else:
-            remctl('reboot', machine.name)
-    elif action == 'Power on':
-        if maxMemory(user) < machine.memory:
-            raise InvalidInput("You don't have enough free RAM quota")
-        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):
     """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'))
+    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')
     
     """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'))
-    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',
@@ -592,43 +470,58 @@ 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'
+    main_status['memory'] += ' MiB'
     for field, disp in display_fields:
     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 = maxMemory(user, machine)
-    max_disk = maxDisk(user, machine)
+    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))
+    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 = 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,
@@ -636,46 +529,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 username
+    else:
+        return 'moo'
+
+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()
-    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')
+    u = getUser()
+    g.user = u
     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')
-    try:
-        fun(u, fields)
-    except CodeError, err:
-        error(operation, u, fields, err)
-    except InvalidInput, err:
-        error(operation, u, fields, err)
+
+    main(operation, u, fields)
+