Create a static/ directory
[invirt/packages/invirt-web.git] / templates / main.py
index 642490f..ec1eca5 100755 (executable)
@@ -12,8 +12,10 @@ import base64
 import sha
 import hmac
 import datetime
 import sha
 import hmac
 import datetime
+import StringIO
+import getafsgroups
 
 
-sys.stderr = sys.stdout
+sys.stderr = StringIO.StringIO()
 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 from Cheetah.Template import Template
 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 from Cheetah.Template import Template
@@ -21,9 +23,39 @@ from sipb_xen_database import *
 import random
 
 class MyException(Exception):
 import random
 
 class MyException(Exception):
+    """Base class for my exceptions"""
     pass
 
     pass
 
+class InvalidInput(MyException):
+    """Exception for user-provided input is invalid but maybe in good faith.
+
+    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).
+    """
+    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 Global(object):
+    def __init__(self, user):
+        self.user = user
+
+    def __get_uptimes(self):
+        if not hasattr(self, '_uptimes'):
+            self._uptimes = getUptimes(Machine.select())
+        return self._uptimes
+    uptimes = property(__get_uptimes)
+
+g = None
+
 def helppopup(subj):
 def helppopup(subj):
+    """Return HTML code for a (?) link to a specified help topic"""
     return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
 
 
     return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
 
 
@@ -38,23 +70,70 @@ def randomUUID():
     return [ random.randint(0, 255) for _ in range(0, 16) ]
 
 def uuidToString(u):
     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)
 
     return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
                      "%02x" * 6]) % tuple(u)
 
-def maxMemory(user, machine=None):
-    return 256
+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=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.
+
+    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.
+    """
+    if not on:
+        return MAX_MEMORY_SINGLE
+    machines = getMachinesByOwner(user.username)
+    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):
 
 def maxDisk(user, machine=None):
-    return 10.0
+    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):
+    machines = getMachinesByOwner(user.username)
+    active_machines = [x for x in machines if g.uptimes[x]]
+    return (len(machines) < MAX_VMS_TOTAL and
+            len(active_machines) < MAX_VMS_ACTIVE)
 
 def haveAccess(user, machine):
 
 def haveAccess(user, machine):
+    """Return whether a user has access to 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):
-    d = dict(op=op, user=user, errorMessage=str(err))
-    print Template(file='error.tmpl', searchList=[d, global_dict]);
+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, global_dict]);
+
+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, global_dict]);
 
 def validMachineName(name):
     """Check that name is valid for a machine name"""
 
 def validMachineName(name):
     """Check that name is valid for a machine name"""
@@ -75,7 +154,7 @@ def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
                          stderr=subprocess.PIPE)
     e = p.wait()
     if e:
                          stderr=subprocess.PIPE)
     e = p.wait()
     if e:
-        raise MyException("Error %s in kinit: %s" % (e, p.stderr.read()))
+        raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
 
 def checkKinit():
     """If we lack tickets, kinit."""
 
 def checkKinit():
     """If we lack tickets, kinit."""
@@ -94,15 +173,23 @@ def remctl(*args, **kws):
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
     if kws.get('err'):
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
     if kws.get('err'):
+        p.wait()
         return p.stdout.read(), p.stderr.read()
     if p.wait():
         return p.stdout.read(), p.stderr.read()
     if p.wait():
-        print >> sys.stderr, 'ERROR on remctl ', args
+        print >> sys.stderr, 'Error on remctl', args, ':'
         print >> sys.stderr, p.stderr.read()
         print >> sys.stderr, p.stderr.read()
+        raise CodeError('ERROR on remctl')
     return p.stdout.read()
 
     return p.stdout.read()
 
-def makeDisks():
-    """Update the lvm partitions to include all disks in the database."""
-    remctl('web', 'lvcreate')
+def lvcreate(machine, disk):
+    """Create a single disk for a machine"""
+    remctl('web', 'lvcreate', machine.name,
+           disk.guest_device_name, str(disk.size))
+    
+def makeDisks(machine):
+    """Update the lvm partitions to add a disk."""
+    for disk in machine.disks:
+        lvcreate(machine, disk)
 
 def bootMachine(machine, cdtype):
     """Boot a machine with a given boot CD.
 
 def bootMachine(machine, cdtype):
     """Boot a machine with a given boot CD.
@@ -148,17 +235,20 @@ 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()
     d = {}
     """Return a dictionary mapping machine names to uptime strings"""
     value_string = remctl('web', 'listvms')
     lines = value_string.splitlines()
     d = {}
-    for line in lines[1:]:
+    for line in lines:
         lst = line.split()
         name, id = lst[:2]
         uptime = ' '.join(lst[2:])
         d[name] = uptime
         lst = line.split()
         name, id = lst[:2]
         uptime = ' '.join(lst[2:])
         d[name] = uptime
-    return d
+    ans = {}
+    for m in machines:
+        ans[m] = d.get(m.name)
+    return ans
 
 def statusInfo(machine):
     """Return the status list for a given machine.
 
 def statusInfo(machine):
     """Return the status list for a given machine.
@@ -167,11 +257,11 @@ def statusInfo(machine):
     """
     value_string, err_string = remctl('list-long', machine.name, err=True)
     if 'Unknown command' in err_string:
     """
     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,))
+        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 MyException("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
 
@@ -190,6 +280,14 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
     # put stuff in the table
     transaction = ctx.current.create_transaction()
     try:
     # put stuff in the table
     transaction = ctx.current.create_transaction()
     try:
+        if memory > maxMemory(user):
+            raise InvalidInput('memory', memory,
+                               "Max %s" % maxMemory(user))
+        if disk > maxDisk(user) * 1024:
+            raise InvalidInput('disk', disk,
+                               "Max %s" % maxDisk(user))
+        if not canAddVm(user):
+            raise InvalidInput('create', True, 'Unable to create more VMs')
         res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
         id = res.fetchone()[0]
         machine = Machine()
         res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
         id = res.fetchone()[0]
         machine = Machine()
@@ -197,6 +295,7 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
         machine.name = name
         machine.memory = memory
         machine.owner = user.username
         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
@@ -207,7 +306,7 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
                     'hda', disk)
         open = NIC.select_by(machine_id=None)
         if not open: #No IPs left!
                     '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"
+            raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
         nic = open[0]
         nic.machine_id = machine.machine_id
         nic.hostname = name
         nic = open[0]
         nic.machine_id = machine.machine_id
         nic.hostname = name
@@ -217,106 +316,125 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
     except:
         transaction.rollback()
         raise
     except:
         transaction.rollback()
         raise
-    makeDisks()
     registerMachine(machine)
     registerMachine(machine)
+    makeDisks(machine)
     # tell it to boot with cdrom
     bootMachine(machine, cdrom)
 
     return machine
 
     # tell it to boot with cdrom
     bootMachine(machine, cdrom)
 
     return machine
 
-def validMemory(user, memory, machine=None):
+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)
     try:
         memory = int(memory)
-        if memory <= 0:
+        if memory < MIN_MEMORY_SINGLE:
             raise ValueError
     except ValueError:
             raise ValueError
     except ValueError:
-        raise MyException("Invalid memory amount")
-    if memory > maxMemory(user, machine):
-        raise MyException("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):
+    """Parse and validate limits for disk for a given user and machine."""
     try:
         disk = float(disk)
         if disk > maxDisk(user, machine):
     try:
         disk = float(disk)
         if disk > maxDisk(user, machine):
-            raise MyException("Too much disk requested")
+            raise InvalidInput('disk', disk,
+                               "Maximum %s G" % maxDisk(user, machine))
         disk = int(disk * 1024)
         disk = int(disk * 1024)
-        if disk <= 0:
+        if disk < MIN_DISK_SINGLE * 1024:
             raise ValueError
     except ValueError:
             raise ValueError
     except ValueError:
-        raise MyException("Invalid disk amount")
+        raise InvalidInput('disk', disk,
+                           "Minimum %s GB" % MIN_DISK_SINGLE)
     return disk
 
 def create(user, fields):
     return disk
 
 def create(user, fields):
+    """Handler for create requests."""
     name = fields.getfirst('name')
     if not validMachineName(name):
     name = fields.getfirst('name')
     if not validMachineName(name):
-        raise MyException("Invalid name '%s'" % name)
-    name = user.username + '_' + name.lower()
+        raise InvalidInput('name', 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,
+                           "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)
 
     vm_type = fields.getfirst('vmtype')
     if vm_type not in ('hvm', 'paravm'):
     
     disk = fields.getfirst('disk')
     disk = validDisk(user, disk)
 
     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)    
+        raise CodeError("Invalid cdrom type '%s'" % cdrom)    
     
     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
     
     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
-    if isinstance(machine, basestring):
-        raise MyException(machine)
     d = dict(user=user,
              machine=machine)
     d = dict(user=user,
              machine=machine)
-    print Template(file='create.tmpl',
+    return Template(file='create.tmpl',
                    searchList=[d, global_dict]);
 
 def listVms(user, fields):
                    searchList=[d, global_dict]);
 
 def listVms(user, fields):
+    """Handler for list requests."""
     machines = [m for m in Machine.select() if haveAccess(user, m)]    
     on = {}
     has_vnc = {}
     machines = [m for m in Machine.select() if haveAccess(user, m)]    
     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'
+        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")
+            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)
     #     for m in machines:
     #         status = statusInfo(m)
     #         on[m.name] = status is not None
     #         has_vnc[m.name] = hasVnc(status)
+    max_mem=maxMemory(user)
+    max_disk=maxDisk(user)
     d = dict(user=user,
     d = dict(user=user,
-             maxmem=maxMemory(user),
-             maxdisk=maxDisk(user),
+             can_add_vm=canAddVm(user),
+             max_mem=max_mem,
+             max_disk=max_disk,
+             default_mem=max_mem,
+             default_disk=min(4.0, max_disk),
              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 Template(file='list.tmpl', searchList=[d, global_dict])
 
 def testMachineId(user, machineId, exists=True):
 
 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:
     if machineId is None:
-        raise MyException("No machine ID specified")
+        raise CodeError("No machine ID specified")
     try:
         machineId = int(machineId)
     except ValueError:
     try:
         machineId = int(machineId)
     except ValueError:
-        raise MyException("Invalid machine ID '%s'" % machineId)
+        raise CodeError("Invalid machine ID '%s'" % machineId)
     machine = Machine.get(machineId)
     if exists and machine is None:
     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)
+        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
 
 def vnc(user, fields):
     return machine
 
 def vnc(user, fields):
@@ -332,9 +450,11 @@ def vnc(user, fields):
     -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'))
     """
     machine = testMachineId(user, fields.getfirst('machine_id'))
-    #XXX fix
     
     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
 
     
     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
 
@@ -349,14 +469,24 @@ def vnc(user, fields):
     token = cPickle.dumps(token)
     token = base64.urlsafe_b64encode(token)
     
     token = cPickle.dumps(token)
     token = base64.urlsafe_b64encode(token)
     
+    status = 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',
+    return Template(file='vnc.tmpl',
                    searchList=[d, global_dict])
 
 def getNicInfo(data_dict, machine):
                    searchList=[d, global_dict])
 
 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'),
@@ -373,6 +503,11 @@ 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 = []
     data_dict['num_disks'] = len(machine.disks)
     disk_fields_template = [('%s_size', '%s size')]
     disk_fields = []
@@ -383,6 +518,8 @@ def getDiskInfo(data_dict, machine):
     return disk_fields
 
 def deleteVM(machine):
     return disk_fields
 
 def deleteVM(machine):
+    """Delete a VM."""
+    remctl('destroy', machine.name, err=True)
     transaction = ctx.current.create_transaction()
     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
     try:
     transaction = ctx.current.create_transaction()
     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
     try:
@@ -402,21 +539,25 @@ def deleteVM(machine):
     unregisterMachine(machine)
 
 def command(user, fields):
     unregisterMachine(machine)
 
 def command(user, fields):
-    print time.time()-start_time
+    """Handler for running commands like boot and delete on a VM."""
+    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):
     if cdrom is not None and not CDROM.get(cdrom):
-        raise MyException("Invalid cdrom type '%s'" % 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 MyException("Invalid action '%s'" % action)
+        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 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('action', 'Power on',
+                               "You don't have enough free RAM quota to turn on this machine")
         bootMachine(machine, cdrom)
     elif action == 'Power off':
         remctl('destroy', machine.name)
         bootMachine(machine, cdrom)
     elif action == 'Power off':
         remctl('destroy', machine.name)
@@ -424,17 +565,109 @@ def command(user, fields):
         remctl('shutdown', machine.name)
     elif action == 'Delete VM':
         deleteVM(machine)
         remctl('shutdown', machine.name)
     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 Template(file="command.tmpl", searchList=[d, global_dict])
+
+def testOwner(user, owner, machine=None):
+    if owner == machine.owner:   #XXX What do we do when you lose access to the locker?
+        return owner
+    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 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 is None:
+        return None
+    if not Machine.select_by(name=name):
+        return name
+    if name == machine.name:
+        return name
+    raise InvalidInput('name', name, "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 modify(user, fields):
 def modify(user, fields):
-    machine = testMachineId(user, fields.getfirst('machine_id'))
-    
+    """Handler for modifying attributes of a machine."""
+
+    olddisk = {}
+    transaction = ctx.current.create_transaction()
+    try:
+        machine = testMachineId(user, fields.getfirst('machine_id'))
+        owner = testOwner(user, fields.getfirst('owner'), machine)
+        contact = testContact(user, fields.getfirst('contact'))
+        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 and owner != machine.owner:
+            machine.owner = owner
+        if name is not None and name != machine.name:
+            machine.name = name
+            
+        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 and name != oldname:
+        for disk in machine.disks:
+            if oldname != name:
+                remctl("web", "lvrename", oldname, disk.guest_device_name, name)
+        remctl("web", "moveregister", oldname, name)
+    d = dict(user=user,
+             command=command,
+             machine=machine)
+    return Template(file="command.tmpl", searchList=[d, global_dict])    
+
+
 def help(user, fields):
 def help(user, fields):
+    """Handler for help messages."""
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
     
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
     
@@ -446,29 +679,37 @@ hope that the sipb-xen maintainers add support for serial consoles.""",
 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 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.)""")
     
     d = dict(user=user,
              simple=simple,
              subjects=subjects,
              mapping=mapping)
     
     
     d = dict(user=user,
              simple=simple,
              subjects=subjects,
              mapping=mapping)
     
-    print Template(file="help.tmpl", searchList=[d, global_dict])
+    return Template(file="help.tmpl", searchList=[d, global_dict])
     
 
 def info(user, fields):
     
 
 def info(user, fields):
+    """Handler for info on a single VM."""
     machine = testMachineId(user, fields.getfirst('machine_id'))
     status = statusInfo(machine)
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
                            memory=str(machine.memory))
     machine = testMachineId(user, fields.getfirst('machine_id'))
     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'),
     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'),
@@ -491,6 +732,7 @@ 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['contact'] = machine.contact
     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
     machine_info['owner'] = machine.owner
     machine_info['contact'] = machine.contact
@@ -505,16 +747,17 @@ def info(user, fields):
     
     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 = maxMemory(user, machine)
+    max_disk = maxDisk(user, machine)
     d = dict(user=user,
              cdroms=CDROM.select(),
              on=status is not None,
     d = dict(user=user,
              cdroms=CDROM.select(),
              on=status is not None,
@@ -522,10 +765,11 @@ def info(user, fields):
              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',
+    return Template(file='info.tmpl',
                    searchList=[d, global_dict])
 
 mapping = dict(list=listVms,
                    searchList=[d, global_dict])
 
 mapping = dict(list=listVms,
@@ -543,35 +787,59 @@ if __name__ == '__main__':
         username = "moo"
         email = 'moo@cow.com'
     u = User()
         username = "moo"
         email = 'moo@cow.com'
     u = User()
+    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:
     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 = 'nobody'
-        u.email = None
+        u.username = 'moo'
+        u.email = 'nobody'
     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
     operation = os.environ.get('PATH_INFO', '')
     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
     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))
+
+    def badOperation(u, e):
+        raise CodeError("Unknown operation")
+
+    fun = mapping.get(operation, badOperation)
     if fun not in (help, ):
         connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
     try:
     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)
+        output = fun(u, fields)
+        print 'Content-Type: text/html\n'
+        sys.stderr.seek(0)
+        e = sys.stderr.read()
+        sys.stderr=sys.stdout
+        if e:
+            output = str(output)
+            output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
+        print output
+    except CodeError, err:
+        print 'Content-Type: text/html\n'
+        sys.stderr.seek(0)
+        e = sys.stderr.read()
+        sys.stderr=sys.stdout
+        print error(operation, u, fields, err, e)
+    except InvalidInput, err:
+        print 'Content-Type: text/html\n'
+        sys.stderr.seek(0)
+        e = sys.stderr.read()
+        sys.stderr=sys.stdout
+        print invalidInput(operation, u, fields, err, e)
+    except:
+        print 'Content-Type: text/plain\n'
+        sys.stderr.seek(0)
+        e = sys.stderr.read()
+        print e
+        print '----'
+        sys.stderr = sys.stdout
+        raise