A monster checkin, with a variety of changes to the web
[invirt/packages/invirt-web.git] / templates / main.py
index ec1eca5..6ec853d 100755 (executable)
@@ -1,26 +1,47 @@
 #!/usr/bin/python
 #!/usr/bin/python
+"""Main CGI script for web interface"""
 
 
-import sys
+import base64
+import cPickle
 import cgi
 import cgi
+import datetime
+import getafsgroups
+import hmac
 import os
 import os
+import random
+import re
+import sha
+import simplejson
 import string
 import subprocess
 import string
 import subprocess
-import re
+import sys
 import time
 import time
-import cPickle
-import base64
-import sha
-import hmac
-import datetime
-import StringIO
-import getafsgroups
+from StringIO import StringIO
+
+
+def revertStandardError():
+    """Move stderr to stdout, and return the contents of the old stderr."""
+    errio = sys.stderr
+    if not isinstance(errio, StringIO):
+        return None
+    sys.stderr = sys.stdout
+    errio.seek(0)
+    return errio.read()
+
+def printError():
+    """Revert stderr to stdout, and print the contents of stderr"""
+    if isinstance(sys.stderr, StringIO):
+        print revertStandardError()
+
+if __name__ == '__main__':
+    import atexit
+    atexit.register(printError)
+    sys.stderr = StringIO()
 
 
-sys.stderr = StringIO.StringIO()
 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 from Cheetah.Template import Template
 from sipb_xen_database import *
 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
 
 from Cheetah.Template import Template
 from sipb_xen_database import *
-import random
 
 class MyException(Exception):
     """Base class for my exceptions"""
 
 class MyException(Exception):
     """Base class for my exceptions"""
@@ -42,7 +63,14 @@ class CodeError(MyException):
     """Exception for internal errors or bad faith input."""
     pass
 
     """Exception for internal errors or bad faith input."""
     pass
 
+def helppopup(subj):
+    """Return HTML code for a (?) link to a specified help topic"""
+    return ('<span class="helplink"><a href="help?subject=' + subj + 
+            '&amp;simple=true" target="_blank" ' + 
+            'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
+
 class Global(object):
 class Global(object):
+    """Global state of the system, to avoid duplicate remctls to get state"""
     def __init__(self, user):
         self.user = user
 
     def __init__(self, user):
         self.user = user
 
@@ -52,16 +80,66 @@ class Global(object):
         return self._uptimes
     uptimes = property(__get_uptimes)
 
         return self._uptimes
     uptimes = property(__get_uptimes)
 
-g = None
-
-def helppopup(subj):
-    """Return HTML code for a (?) link to a specified help topic"""
-    return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
-
+    def clear(self):
+        """Clear the state so future accesses reload it."""
+        for attr in ('_uptimes', ):
+            if hasattr(self, attr):
+                delattr(self, attr)
 
 
-global_dict = {}
-global_dict['helppopup'] = helppopup
+g = None
 
 
+class User:
+    """User class (sort of useless, I admit)"""
+    def __init__(self, username, email):
+        self.username = username
+        self.email = email
+
+def makeErrorPre(old, addition):
+    if addition is None:
+        return
+    if old:
+        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
+    else:
+        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
+
+Template.helppopup = staticmethod(helppopup)
+Template.err = None
+
+class JsonDict:
+    """Class to store a dictionary that will be converted to JSON"""
+    def __init__(self, **kws):
+        self.data = kws
+        if 'err' in kws:
+            err = kws['err']
+            del kws['err']
+            self.addError(err)
+
+    def __str__(self):
+        return simplejson.dumps(self.data)
+
+    def addError(self, text):
+        """Add stderr text to be displayed on the website."""
+        self.data['err'] = \
+            makeErrorPre(self.data.get('err'), text)
+
+class Defaults:
+    """Class to store default values for fields."""
+    memory = 256
+    disk = 4.0
+    cdrom = ''
+    name = ''
+    vmtype = 'hvm'
+    def __init__(self, max_memory=None, max_disk=None, **kws):
+        if max_memory is not None:
+            self.memory = min(self.memory, max_memory)
+        if max_disk is not None:
+            self.max_disk = min(self.disk, max_disk)
+        for key in kws:
+            setattr(self, key, kws[key])
+
+
+
+default_headers = {'Content-Type': 'text/html'}
 
 # ... and stolen from xend/uuid.py
 def randomUUID():
 
 # ... and stolen from xend/uuid.py
 def randomUUID():
@@ -83,8 +161,16 @@ MIN_DISK_SINGLE = 0.1
 MAX_VMS_TOTAL = 10
 MAX_VMS_ACTIVE = 4
 
 MAX_VMS_TOTAL = 10
 MAX_VMS_ACTIVE = 4
 
-def getMachinesByOwner(owner):
-    """Return the machines owned by a given owner."""
+def getMachinesByOwner(user, machine=None):
+    """Return the machines owned by the same as a machine.
+    
+    If the machine is None, return the machines owned by the same
+    user.
+    """
+    if machine:
+        owner = machine.owner
+    else:
+        owner = user.username
     return Machine.select_by(owner=owner)
 
 def maxMemory(user, machine=None, on=True):
     return Machine.select_by(owner=owner)
 
 def maxMemory(user, machine=None, on=True):
@@ -99,25 +185,42 @@ def maxMemory(user, machine=None, on=True):
     """
     if not on:
         return MAX_MEMORY_SINGLE
     """
     if not on:
         return MAX_MEMORY_SINGLE
-    machines = getMachinesByOwner(user.username)
+    machines = getMachinesByOwner(user, machine)
     active_machines = [x for x in machines if g.uptimes[x]]
     mem_usage = sum([x.memory for x in active_machines if x != machine])
     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
 
 def maxDisk(user, machine=None):
     active_machines = [x for x in machines if g.uptimes[x]]
     mem_usage = sum([x.memory for x in active_machines if x != machine])
     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
 
 def maxDisk(user, machine=None):
-    machines = getMachinesByOwner(user.username)
+    machines = getMachinesByOwner(user, machine)
     disk_usage = sum([sum([y.size for y in x.disks])
                       for x in machines if x != machine])
     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
 
     disk_usage = sum([sum([y.size for y in x.disks])
                       for x in machines if x != machine])
     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
 
-def canAddVm(user):
-    machines = getMachinesByOwner(user.username)
+def cantAddVm(user):
+    machines = getMachinesByOwner(user)
     active_machines = [x for x in machines if g.uptimes[x]]
     active_machines = [x for x in machines if g.uptimes[x]]
-    return (len(machines) < MAX_VMS_TOTAL and
-            len(active_machines) < MAX_VMS_ACTIVE)
+    if len(machines) >= MAX_VMS_TOTAL:
+        return 'You have too many VMs to create a new one.'
+    if len(active_machines) >= MAX_VMS_ACTIVE:
+        return ('You already have the maximum number of VMs turned on.  '
+                'To create more, turn one off.')
+    return False
 
 def haveAccess(user, machine):
 
 def haveAccess(user, machine):
-    """Return whether a user has access to a machine"""
+    """Return whether a user has adminstrative access to a machine"""
+    if user.username == 'moo':
+        return True
+    if user.username in (machine.administrator, machine.owner):
+        return True
+    if getafsgroups.checkAfsGroup(user.username, machine.administrator, 
+                                  'athena.mit.edu'): #XXX Cell?
+        return True
+    if getafsgroups.checkLockerOwner(user.username, machine.owner):
+        return True
+    return owns(user, machine)
+
+def owns(user, machine):
+    """Return whether a user owns a machine"""
     if user.username == 'moo':
         return True
     return getafsgroups.checkLockerOwner(user.username, machine.owner)
     if user.username == 'moo':
         return True
     return getafsgroups.checkLockerOwner(user.username, machine.owner)
@@ -126,14 +229,14 @@ 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)
     """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]);
+    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))
 
 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]);
+    return Template(file='invalid.tmpl', searchList=[d]);
 
 def validMachineName(name):
     """Check that name is valid for a machine name"""
 
 def validMachineName(name):
     """Check that name is valid for a machine name"""
@@ -172,11 +275,11 @@ def remctl(*args, **kws):
                          + list(args),
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
                          + list(args),
                          stdout=subprocess.PIPE,
                          stderr=subprocess.PIPE)
+    v = p.wait()
     if kws.get('err'):
     if kws.get('err'):
-        p.wait()
         return p.stdout.read(), p.stderr.read()
         return p.stdout.read(), p.stderr.read()
-    if p.wait():
-        print >> sys.stderr, 'Error on remctl', args, ':'
+    if v:
+        print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
         print >> sys.stderr, p.stderr.read()
         raise CodeError('ERROR on remctl')
     return p.stdout.read()
         print >> sys.stderr, p.stderr.read()
         raise CodeError('ERROR on remctl')
     return p.stdout.read()
@@ -198,10 +301,10 @@ def bootMachine(machine, cdtype):
     id of the CD (e.g. 'gutsy_i386')
     """
     if cdtype is not None:
     id of the CD (e.g. 'gutsy_i386')
     """
     if cdtype is not None:
-        remctl('web', 'vmboot', machine.name,
+        remctl('control', machine.name, 'create', 
                cdtype)
     else:
                cdtype)
     else:
-        remctl('web', 'vmboot', machine.name)
+        remctl('control', machine.name, 'create')
 
 def registerMachine(machine):
     """Register a machine to be controlled by the web interface"""
 
 def registerMachine(machine):
     """Register a machine to be controlled by the web interface"""
@@ -255,13 +358,16 @@ def statusInfo(machine):
 
     Gets and parses xm list --long
     """
 
     Gets and parses xm list --long
     """
-    value_string, err_string = remctl('list-long', machine.name, err=True)
+    value_string, err_string = remctl('control', machine.name, 'list-long', 
+                                      err=True)
     if 'Unknown command' in err_string:
     if 'Unknown command' in err_string:
-        raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
+        raise CodeError("ERROR in remctl list-long %s is not registered" % 
+                        (machine.name,))
     elif 'does not exist' in err_string:
         return None
     elif err_string:
     elif 'does not exist' in err_string:
         return None
     elif err_string:
-        raise CodeError("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
+        raise CodeError("ERROR in remctl list-long %s:  %s" % 
+                        (machine.name, err_string))
     status = parseStatus(value_string)
     return status
 
     status = parseStatus(value_string)
     return status
 
@@ -286,9 +392,11 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
         if disk > maxDisk(user) * 1024:
             raise InvalidInput('disk', disk,
                                "Max %s" % maxDisk(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"\')')
+        reason = cantAddVm(user)
+        if reason:
+            raise InvalidInput('create', True, reason)
+        res = meta.engine.execute('select nextval('
+                                  '\'"machines_machine_id_seq"\')')
         id = res.fetchone()[0]
         machine = Machine()
         machine.machine_id = id
         id = res.fetchone()[0]
         machine = Machine()
         machine.machine_id = id
@@ -304,10 +412,11 @@ def createVm(user, name, memory, disk, is_hvm, cdrom):
         ctx.current.save(machine)
         disk = Disk(machine.machine_id, 
                     'hda', disk)
         ctx.current.save(machine)
         disk = Disk(machine.machine_id, 
                     'hda', disk)
-        open = NIC.select_by(machine_id=None)
-        if not open: #No IPs left!
-            raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
-        nic = open[0]
+        open_nics = NIC.select_by(machine_id=None)
+        if not open_nics: #No IPs left!
+            raise CodeError("No IP addresses left!  "
+                            "Contact sipb-xen-dev@mit.edu")
+        nic = open_nics[0]
         nic.machine_id = machine.machine_id
         nic.hostname = name
         ctx.current.save(nic)    
         nic.machine_id = machine.machine_id
         nic.hostname = name
         ctx.current.save(nic)    
@@ -356,16 +465,15 @@ def validDisk(user, disk, machine=None):
                            "Minimum %s GB" % MIN_DISK_SINGLE)
     return disk
 
                            "Minimum %s GB" % MIN_DISK_SINGLE)
     return disk
 
-def create(user, fields):
-    """Handler for create requests."""
+def parseCreate(user, fields):
     name = fields.getfirst('name')
     if not validMachineName(name):
     name = fields.getfirst('name')
     if not validMachineName(name):
-        raise InvalidInput('name', name)
+        raise InvalidInput('name', name, 'You must provide a machine name.')
     name = name.lower()
 
     if Machine.get_by(name=name):
         raise InvalidInput('name', name,
     name = name.lower()
 
     if Machine.get_by(name=name):
         raise InvalidInput('name', name,
-                           "Already exists")
+                           "Name already exists.")
     
     memory = fields.getfirst('memory')
     memory = validMemory(user, memory, on=True)
     
     memory = fields.getfirst('memory')
     memory = validMemory(user, memory, on=True)
@@ -380,21 +488,45 @@ def create(user, fields):
 
     cdrom = fields.getfirst('cdrom')
     if cdrom is not None and not CDROM.get(cdrom):
 
     cdrom = fields.getfirst('cdrom')
     if cdrom is not None and not CDROM.get(cdrom):
-        raise CodeError("Invalid cdrom type '%s'" % cdrom)    
-    
-    machine = createVm(user, name, memory, disk, is_hvm, cdrom)
-    d = dict(user=user,
-             machine=machine)
-    return Template(file='create.tmpl',
-                   searchList=[d, global_dict]);
+        raise CodeError("Invalid cdrom type '%s'" % cdrom)
+    return dict(user=user, name=name, memory=memory, disk=disk,
+                is_hvm=is_hvm, cdrom=cdrom)
 
 
-def listVms(user, fields):
-    """Handler for list requests."""
+def create(user, fields):
+    """Handler for create requests."""
+    js = fields.getfirst('js')
+    try:
+        parsed_fields = parseCreate(user, fields)
+        machine = createVm(**parsed_fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+    else:
+        err = None
+        if not js:
+            d = dict(user=user,
+                     machine=machine)
+            return Template(file='create.tmpl', searchList=[d])
+    g.clear() #Changed global state
+    d = getListDict(user)
+    d['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(d['defaults'], field, fields.getfirst(field))
+    else:
+        d['new_machine'] = parsed_fields['name']
+    t = Template(file='list.tmpl', searchList=[d])
+    return JsonDict(createtable=t.createTable(),
+                    machinelist=t.machineList(d['machines']))
+
+
+def getListDict(user):
     machines = [m for m in Machine.select() if haveAccess(user, m)]    
     on = {}
     has_vnc = {}
     on = g.uptimes
     for m in machines:
     machines = [m for m in Machine.select() if haveAccess(user, m)]    
     on = {}
     has_vnc = {}
     on = g.uptimes
     for m in machines:
+        m.uptime = g.uptimes.get(m)
         if not on[m]:
             has_vnc[m] = 'Off'
         elif m.type.hvm:
         if not on[m]:
             has_vnc[m] = 'Off'
         elif m.type.hvm:
@@ -405,20 +537,38 @@ def listVms(user, fields):
     #         status = statusInfo(m)
     #         on[m.name] = status is not None
     #         has_vnc[m.name] = hasVnc(status)
     #         status = statusInfo(m)
     #         on[m.name] = status is not None
     #         has_vnc[m.name] = hasVnc(status)
-    max_mem=maxMemory(user)
-    max_disk=maxDisk(user)
+    max_memory = maxMemory(user)
+    max_disk = maxDisk(user)
+    defaults = Defaults(max_memory=max_memory,
+                        max_disk=max_disk,
+                        cdrom='gutsy-i386')
     d = dict(user=user,
     d = dict(user=user,
-             can_add_vm=canAddVm(user),
-             max_mem=max_mem,
+             cant_add_vm=cantAddVm(user),
+             max_memory=max_memory,
              max_disk=max_disk,
              max_disk=max_disk,
-             default_mem=max_mem,
-             default_disk=min(4.0, max_disk),
+             defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
              uptimes=g.uptimes,
              cdroms=CDROM.select())
              machines=machines,
              has_vnc=has_vnc,
              uptimes=g.uptimes,
              cdroms=CDROM.select())
-    return Template(file='list.tmpl', searchList=[d, global_dict])
+    return d
 
 
+def listVms(user, fields):
+    """Handler for list requests."""
+    d = getListDict(user)
+    t = Template(file='list.tmpl', searchList=[d])
+    js = fields.getfirst('js')
+    if not js:
+        return t
+    if js == 'machinelist':
+        return t.machineList(d['machines'])
+    elif js.startswith('machinerow-'):
+        request_machine_id = int(js.split('-')[1])
+        m = [x for x in d['machines'] if x.id == request_machine_id]
+        return t.machineRow(m)
+    elif js == 'createtable':
+        return t.createTable()
+            
 def testMachineId(user, machineId, exists=True):
     """Parse, validate and check authorization for a given machineId.
 
 def testMachineId(user, machineId, exists=True):
     """Parse, validate and check authorization for a given machineId.
 
@@ -447,9 +597,12 @@ def vnc(user, fields):
 
     You might want iptables like:
 
 
     You might want iptables like:
 
-    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
-    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142 
-    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
+    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
+      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 
+    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
+      --dport 10003 -j SNAT --to-source 18.187.7.142 
+    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
+      --dport 10003 -j ACCEPT
 
     Remember to enable iptables!
     echo 1 > /proc/sys/net/ipv4/ip_forward
 
     Remember to enable iptables!
     echo 1 > /proc/sys/net/ipv4/ip_forward
@@ -460,12 +613,12 @@ def vnc(user, fields):
 
     data = {}
     data["user"] = user.username
 
     data = {}
     data["user"] = user.username
-    data["machine"]=machine.name
-    data["expires"]=time.time()+(5*60)
-    pickledData = cPickle.dumps(data)
+    data["machine"] = machine.name
+    data["expires"] = time.time()+(5*60)
+    pickled_data = cPickle.dumps(data)
     m = hmac.new(TOKEN_KEY, digestmod=sha)
     m = hmac.new(TOKEN_KEY, digestmod=sha)
-    m.update(pickledData)
-    token = {'data': pickledData, 'digest': m.digest()}
+    m.update(pickled_data)
+    token = {'data': pickled_data, 'digest': m.digest()}
     token = cPickle.dumps(token)
     token = base64.urlsafe_b64encode(token)
     
     token = cPickle.dumps(token)
     token = base64.urlsafe_b64encode(token)
     
@@ -478,8 +631,7 @@ def vnc(user, fields):
              machine=machine,
              hostname=os.environ.get('SERVER_NAME', 'localhost'),
              authtoken=token)
              machine=machine,
              hostname=os.environ.get('SERVER_NAME', 'localhost'),
              authtoken=token)
-    return 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.
@@ -495,7 +647,8 @@ def getNicInfo(data_dict, machine):
     nic_fields = []
     for i in range(len(machine.nics)):
         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
     nic_fields = []
     for i in range(len(machine.nics)):
         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
-        data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
+        data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname + 
+                                           '.servers.csail.mit.edu')
         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
         data_dict['nic%s_ip' % i] = machine.nics[i].ip
     if len(machine.nics) == 1:
         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
         data_dict['nic%s_ip' % i] = machine.nics[i].ip
     if len(machine.nics) == 1:
@@ -513,15 +666,17 @@ def getDiskInfo(data_dict, machine):
     disk_fields = []
     for disk in machine.disks:
         name = disk.guest_device_name
     disk_fields = []
     for disk in machine.disks:
         name = disk.guest_device_name
-        disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
+        disk_fields.extend([(x % name, y % name) for x, y in 
+                            disk_fields_template])
         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
     return disk_fields
 
 def deleteVM(machine):
     """Delete a VM."""
         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
     return disk_fields
 
 def deleteVM(machine):
     """Delete a VM."""
-    remctl('destroy', machine.name, err=True)
+    remctl('control', machine.name, 'destroy', err=True)
     transaction = ctx.current.create_transaction()
     transaction = ctx.current.create_transaction()
-    delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
+    delete_disk_pairs = [(machine.name, d.guest_device_name) 
+                         for d in machine.disks]
     try:
         for nic in machine.nics:
             nic.machine_id = None
     try:
         for nic in machine.nics:
             nic.machine_id = None
@@ -538,8 +693,7 @@ def deleteVM(machine):
         remctl('web', 'lvremove', mname, dname)
     unregisterMachine(machine)
 
         remctl('web', 'lvremove', mname, dname)
     unregisterMachine(machine)
 
-def command(user, fields):
-    """Handler for running commands like boot and delete on a VM."""
+def commandResult(user, fields):
     print >> sys.stderr, time.time()-start_time
     machine = testMachineId(user, fields.getfirst('machine_id'))
     action = fields.getfirst('action')
     print >> sys.stderr, time.time()-start_time
     machine = testMachineId(user, fields.getfirst('machine_id'))
     action = fields.getfirst('action')
@@ -547,22 +701,51 @@ def command(user, fields):
     print >> sys.stderr, time.time()-start_time
     if cdrom is not None and not CDROM.get(cdrom):
         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
     print >> sys.stderr, time.time()-start_time
     if cdrom is not None and not CDROM.get(cdrom):
         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
-    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
+    if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 
+                      'Delete VM'):
         raise CodeError("Invalid action '%s'" % action)
     if action == 'Reboot':
         if cdrom is not None:
         raise CodeError("Invalid action '%s'" % action)
     if action == 'Reboot':
         if cdrom is not None:
-            remctl('reboot', machine.name, cdrom)
+            out, err = remctl('control', machine.name, 'reboot', cdrom,
+                              err=True)
         else:
         else:
-            remctl('reboot', machine.name)
+            out, err = remctl('control', machine.name, 'reboot',
+                              err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "reboot", 
+                                   "Machine is not on")
+            else:
+                print >> sys.stderr, 'Error on reboot:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
+                
     elif action == 'Power on':
         if maxMemory(user) < machine.memory:
             raise InvalidInput('action', 'Power on',
     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")
+                               "You don't have enough free RAM quota "
+                               "to turn on this machine.")
         bootMachine(machine, cdrom)
     elif action == 'Power off':
         bootMachine(machine, cdrom)
     elif action == 'Power off':
-        remctl('destroy', machine.name)
+        out, err = remctl('control', machine.name, 'destroy', err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "Power off", 
+                                   "Machine is not on.")
+            else:
+                print >> sys.stderr, 'Error on power off:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
     elif action == 'Shutdown':
     elif action == 'Shutdown':
-        remctl('shutdown', machine.name)
+        out, err = remctl('control', machine.name, 'shutdown', err=True)
+        if err:
+            if re.match("Error: Domain '.*' does not exist.", err):
+                raise InvalidInput("action", "Shutdown", 
+                                   "Machine is not on.")
+            else:
+                print >> sys.stderr, 'Error on Shutdown:'
+                print >> sys.stderr, err
+                raise CodeError('ERROR on remctl')
     elif action == 'Delete VM':
         deleteVM(machine)
     print >> sys.stderr, time.time()-start_time
     elif action == 'Delete VM':
         deleteVM(machine)
     print >> sys.stderr, time.time()-start_time
@@ -570,32 +753,79 @@ def command(user, fields):
     d = dict(user=user,
              command=action,
              machine=machine)
     d = dict(user=user,
              command=action,
              machine=machine)
-    return Template(file="command.tmpl", searchList=[d, global_dict])
+    return d
 
 
-def testOwner(user, owner, machine=None):
-    if owner == machine.owner:   #XXX What do we do when you lose access to the locker?
-        return owner
+def command(user, fields):
+    """Handler for running commands like boot and delete on a VM."""
+    js = fields.getfirst('js')
+    try:
+        d = commandResult(user, fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+        result = None
+    else:
+        err = None
+        result = 'Success!'
+        if not js:
+            return Template(file='command.tmpl', searchList=[d])
+    if js == 'list':
+        g.clear() #Changed global state
+        d = getListDict(user)
+        t = Template(file='list.tmpl', searchList=[d])
+        return JsonDict(createtable=t.createTable(),
+                        machinelist=t.machineList(d['machines']),
+                        result=result,
+                        err=err)
+    elif js == 'info':
+        machine = testMachineId(user, fields.getfirst('machine_id'))
+        d = infoDict(user, machine)
+        t = Template(file='info.tmpl', searchList=[d])
+        return JsonDict(info=t.infoTable(),
+                        commands=t.commands(),
+                        modify=t.modifyForm(),
+                        result=result,
+                        err=err)
+    else:
+        raise InvalidInput('js', js, 'Not a known js type.')
+
+def testAdmin(user, admin, machine):
+    if admin in (None, machine.administrator):
+        return None
+    if admin == user.username:
+        return admin
+    if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
+        return admin
+    if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
+                                  'athena.mit.edu'):
+        return 'system:'+admin
+    raise InvalidInput('administrator', admin, 
+                       'You must control the group you move it to.')
+    
+def testOwner(user, owner, machine):
+    if owner in (None, machine.owner):
+        return None
     value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
     if value == True:
         return owner
     raise InvalidInput('owner', owner, value)
 
 def testContact(user, contact, machine=None):
     value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
     if value == True:
         return owner
     raise InvalidInput('owner', owner, value)
 
 def testContact(user, contact, machine=None):
+    if contact in (None, machine.contact):
+        return None
     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
     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")
+        raise InvalidInput('contact', contact, "Not a valid email.")
     return contact
 
 def testDisk(user, disksize, machine=None):
     return disksize
 
 def testName(user, name, machine=None):
     return contact
 
 def testDisk(user, disksize, machine=None):
     return disksize
 
 def testName(user, name, machine=None):
-    if name is None:
+    if name in (None, machine.name):
         return None
     if not Machine.select_by(name=name):
         return name
         return None
     if not Machine.select_by(name=name):
         return name
-    if name == machine.name:
-        return name
-    raise InvalidInput('name', name, "Already taken")
+    raise InvalidInput('name', name, "Name is already taken.")
 
 def testHostname(user, hostname, machine):
     for nic in machine.nics:
 
 def testHostname(user, hostname, machine):
     for nic in machine.nics:
@@ -606,23 +836,22 @@ def testHostname(user, hostname, machine):
         raise InvalidInput('hostname', hostname,
                            "Already exists")
     if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
         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.")
+        raise InvalidInput('hostname', hostname, "Not a valid hostname; "
+                           "must only use number, letters, and dashes.")
     return hostname
 
     return hostname
 
-def modify(user, fields):
-    """Handler for modifying attributes of a machine."""
-
+def modifyDict(user, fields):
     olddisk = {}
     transaction = ctx.current.create_transaction()
     try:
         machine = testMachineId(user, fields.getfirst('machine_id'))
         owner = testOwner(user, fields.getfirst('owner'), machine)
     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)
+        admin = testAdmin(user, fields.getfirst('administrator'), machine)
+        contact = testContact(user, fields.getfirst('contact'), machine)
+        hostname = testHostname(owner, fields.getfirst('hostname'), machine)
         name = testName(user, fields.getfirst('name'), machine)
         oldname = machine.name
         name = testName(user, fields.getfirst('name'), machine)
         oldname = machine.name
-        command="modify"
+        command = "modify"
 
         memory = fields.getfirst('memory')
         if memory is not None:
 
         memory = fields.getfirst('memory')
         if memory is not None:
@@ -638,15 +867,20 @@ def modify(user, fields):
                 disk.size = disksize
                 ctx.current.save(disk)
         
                 disk.size = disksize
                 ctx.current.save(disk)
         
-        # XXX first NIC gets hostname on change?  Interface doesn't support more.
+        # 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)
 
         for nic in machine.nics[:1]:
             nic.hostname = hostname
             ctx.current.save(nic)
 
-        if owner is not None and owner != machine.owner:
+        if owner is not None:
             machine.owner = owner
             machine.owner = owner
-        if name is not None and name != machine.name:
+        if name is not None:
             machine.name = name
             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()
             
         ctx.current.save(machine)
         transaction.commit()
@@ -655,55 +889,98 @@ def modify(user, fields):
         raise
     for diskname in olddisk:
         remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
         raise
     for diskname in olddisk:
         remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
-    if name is not None and name != oldname:
+    if name is not None:
         for disk in machine.disks:
         for disk in machine.disks:
-            if oldname != name:
-                remctl("web", "lvrename", oldname, disk.guest_device_name, name)
+            remctl("web", "lvrename", oldname, disk.guest_device_name, name)
         remctl("web", "moveregister", oldname, name)
         remctl("web", "moveregister", oldname, name)
-    d = dict(user=user,
-             command=command,
-             machine=machine)
-    return Template(file="command.tmpl", searchList=[d, global_dict])    
-
+    return dict(user=user,
+                command=command,
+                machine=machine)
+    
+def modify(user, fields):
+    """Handler for modifying attributes of a machine."""
+    js = fields.getfirst('js')
+    try:
+        modify_dict = modifyDict(user, fields)
+    except InvalidInput, err:
+        if not js:
+            raise
+        result = ''
+        machine = testMachineId(user, fields.getfirst('machine_id'))
+    else:
+        machine = modify_dict['machine']
+        result='Success!'
+        err = None
+        if not js:
+            return Template(file='command.tmpl', searchList=[modify_dict])
+    info_dict = infoDict(user, machine)
+    info_dict['err'] = err
+    if err:
+        for field in fields.keys():
+            setattr(info_dict['defaults'], field, fields.getfirst(field))
+    t = Template(file='info.tmpl', searchList=[info_dict])
+    return JsonDict(info=t.infoTable(),
+                    commands=t.commands(),
+                    modify=t.modifyForm(),
+                    result=result,
+                    err=err)
+    
 
 
-def help(user, fields):
+def helpHandler(user, fields):
     """Handler for help messages."""
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
     
     """Handler for help messages."""
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
     
-    mapping = dict(paravm_console="""
+    help_mapping = dict(paravm_console="""
 ParaVM machines do not support console access over VNC.  To access
 these machines, you either need to boot with a liveCD and ssh in or
 hope that the sipb-xen maintainers add support for serial consoles.""",
 ParaVM machines do not support console access over VNC.  To access
 these machines, you either need to boot with a liveCD and ssh in or
 hope that the sipb-xen maintainers add support for serial consoles.""",
-                   hvm_paravm="""
+                        hvm_paravm="""
 HVM machines use the virtualization features of the processor, while
 ParaVM machines use Xen's emulation of virtualization features.  You
 want an HVM virtualized machine.""",
 HVM machines use the virtualization features of the processor, while
 ParaVM machines use Xen's emulation of virtualization features.  You
 want an HVM virtualized machine.""",
-                   cpu_weight="""Don't ask us!  We're as mystified as you are.""",
-                   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.)""")
+                        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)
     
     
-    return Template(file="help.tmpl", searchList=[d, global_dict])
+    return Template(file="help.tmpl", searchList=[d])
     
 
     
 
-def info(user, fields):
-    """Handler for info on a single VM."""
-    machine = testMachineId(user, fields.getfirst('machine_id'))
+def badOperation(u, e):
+    raise CodeError("Unknown operation")
+
+def infoDict(user, machine):
     status = statusInfo(machine)
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
                            memory=str(machine.memory))
     status = statusInfo(machine)
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
                            memory=str(machine.memory))
-        uptime=None
-        cputime=None
+        uptime = None
+        cputime = None
     else:
         main_status = dict(status[1:])
         start_time = float(main_status.get('start_time', 0))
     else:
         main_status = dict(status[1:])
         start_time = float(main_status.get('start_time', 0))
@@ -714,6 +991,7 @@ def info(user, fields):
      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
     display_fields = [('name', 'Name'),
                       ('owner', 'Owner'),
      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',
@@ -735,15 +1013,18 @@ def info(user, fields):
     machine_info['name'] = machine.name
     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
     machine_info['owner'] = machine.owner
     machine_info['name'] = machine.name
     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
     machine_info['owner'] = machine.owner
+    machine_info['administrator'] = machine.administrator
     machine_info['contact'] = machine.contact
 
     nic_fields = getNicInfo(machine_info, machine)
     nic_point = display_fields.index('NIC_INFO')
     machine_info['contact'] = machine.contact
 
     nic_fields = getNicInfo(machine_info, machine)
     nic_point = display_fields.index('NIC_INFO')
-    display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
+    display_fields = (display_fields[:nic_point] + nic_fields + 
+                      display_fields[nic_point+1:])
 
     disk_fields = getDiskInfo(machine_info, machine)
     disk_point = display_fields.index('DISK_INFO')
 
     disk_fields = getDiskInfo(machine_info, machine)
     disk_point = display_fields.index('DISK_INFO')
-    display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
+    display_fields = (display_fields[:disk_point] + disk_fields + 
+                      display_fields[disk_point+1:])
     
     main_status['memory'] += ' MB'
     for field, disp in display_fields:
     
     main_status['memory'] += ' MB'
     for field, disp in display_fields:
@@ -758,10 +1039,17 @@ def info(user, fields):
             #fields.append((disp, None))
     max_mem = maxMemory(user, machine)
     max_disk = maxDisk(user, machine)
             #fields.append((disp, None))
     max_mem = maxMemory(user, machine)
     max_disk = maxDisk(user, machine)
+    defaults=Defaults()
+    for name in 'machine_id name administrator owner memory contact'.split():
+        setattr(defaults, name, getattr(machine, name))
+    if machine.nics:
+        defaults.hostname = machine.nics[0].hostname
+    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
     d = dict(user=user,
              cdroms=CDROM.select(),
              on=status is not None,
              machine=machine,
     d = dict(user=user,
              cdroms=CDROM.select(),
              on=status is not None,
              machine=machine,
+             defaults=defaults,
              has_vnc=has_vnc,
              uptime=str(uptime),
              ram=machine.memory,
              has_vnc=has_vnc,
              uptime=str(uptime),
              ram=machine.memory,
@@ -769,8 +1057,13 @@ def info(user, fields):
              max_disk=max_disk,
              owner_help=helppopup("owner"),
              fields = fields)
              max_disk=max_disk,
              owner_help=helppopup("owner"),
              fields = fields)
-    return Template(file='info.tmpl',
-                   searchList=[d, global_dict])
+    return d
+
+def info(user, fields):
+    """Handler for info on a single VM."""
+    machine = testMachineId(user, fields.getfirst('machine_id'))
+    d = infoDict(user, machine)
+    return Template(file='info.tmpl', searchList=[d])
 
 mapping = dict(list=listVms,
                vnc=vnc,
 
 mapping = dict(list=listVms,
                vnc=vnc,
@@ -778,24 +1071,27 @@ mapping = dict(list=listVms,
                modify=modify,
                info=info,
                create=create,
                modify=modify,
                info=info,
                create=create,
-               help=help)
+               help=helpHandler)
+
+def printHeaders(headers):
+    for key, value in headers.iteritems():
+        print '%s: %s' % (key, value)
+    print
+
+
+def getUser():
+    """Return the current user based on the SSL environment variables"""
+    if 'SSL_CLIENT_S_DN_Email' in os.environ:
+        username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
+        return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
+    else:
+        return User('moo', 'nobody')
 
 if __name__ == '__main__':
     start_time = time.time()
     fields = cgi.FieldStorage()
 
 if __name__ == '__main__':
     start_time = time.time()
     fields = cgi.FieldStorage()
-    class User:
-        username = "moo"
-        email = 'moo@cow.com'
-    u = User()
+    u = getUser()
     g = Global(u)
     g = Global(u)
-    if 'SSL_CLIENT_S_DN_Email' in os.environ:
-        username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
-        u.username = username
-        u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
-    else:
-        u.username = 'moo'
-        u.email = 'nobody'
-    connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
     operation = os.environ.get('PATH_INFO', '')
     if not operation:
         print "Status: 301 Moved Permanently"
     operation = os.environ.get('PATH_INFO', '')
     if not operation:
         print "Status: 301 Moved Permanently"
@@ -807,39 +1103,42 @@ if __name__ == '__main__':
     if not operation:
         operation = 'list'
 
     if not operation:
         operation = 'list'
 
-    def badOperation(u, e):
-        raise CodeError("Unknown operation")
+
 
     fun = mapping.get(operation, badOperation)
 
     fun = mapping.get(operation, badOperation)
-    if fun not in (help, ):
-        connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
+
+    if fun not in (helpHandler, ):
+        connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
     try:
         output = fun(u, fields)
     try:
         output = fun(u, fields)
-        print 'Content-Type: text/html\n'
-        sys.stderr.seek(0)
-        e = sys.stderr.read()
-        sys.stderr=sys.stdout
+
+        headers = dict(default_headers)
+        if isinstance(output, tuple):
+            new_headers, output = output
+            headers.update(new_headers)
+
+        e = revertStandardError()
         if e:
         if e:
-            output = str(output)
-            output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
+            output.addError(e)
+        printHeaders(headers)
         print output
         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:
+    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 'Content-Type: text/plain\n'
-        sys.stderr.seek(0)
-        e = sys.stderr.read()
+        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 '----'
         print e
         print '----'
-        sys.stderr = sys.stdout
         raise
         raise