A monster checkin, with a variety of changes to the web
[invirt/packages/invirt-web.git] / templates / main.py
index f7468c7..6ec853d 100755 (executable)
@@ -1,27 +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()
 
 
-errio = StringIO.StringIO()
-sys.stderr = errio
 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"""
@@ -43,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
 
@@ -53,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)
 
 
+g = None
 
 
-global_dict = {}
-global_dict['helppopup'] = helppopup
-
+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():
@@ -119,11 +196,15 @@ def maxDisk(user, machine=None):
                       for x in machines if x != machine])
     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
 
                       for x in machines if x != machine])
     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
 
-def canAddVm(user):
+def cantAddVm(user):
     machines = getMachinesByOwner(user)
     active_machines = [x for x in machines if g.uptimes[x]]
     machines = getMachinesByOwner(user)
     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):
     """Return whether a user has adminstrative access to a machine"""
 
 def haveAccess(user, machine):
     """Return whether a user has adminstrative access to a machine"""
@@ -131,7 +212,10 @@ def haveAccess(user, machine):
         return True
     if user.username in (machine.administrator, machine.owner):
         return True
         return True
     if user.username in (machine.administrator, machine.owner):
         return True
-    if checkAfsGroup(user, machine.administrator, 'athena.mit.edu'): #XXX Cell?
+    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)
 
         return True
     return owns(user, machine)
 
@@ -145,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"""
@@ -191,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()
@@ -217,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"""
@@ -274,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
 
@@ -305,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
@@ -323,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)    
@@ -375,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)
@@ -399,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:
@@ -424,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.
 
@@ -466,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
@@ -479,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)
     
@@ -497,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.
@@ -514,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:
@@ -532,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
@@ -557,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')
@@ -566,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
@@ -589,26 +753,58 @@ 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 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
 
 def testAdmin(user, admin, machine):
     if admin in (None, machine.administrator):
         return None
     if admin == user.username:
         return admin
-    if getafsgroups.checkAfsGroup(user, admin, 'athena.mit.edu'):
+    if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
         return admin
         return admin
-    if getafsgroups.checkAfsGroup(user, 'system:'+admin, 'athena.mit.edu'):
+    if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
+                                  'athena.mit.edu'):
         return 'system:'+admin
         return 'system:'+admin
-    raise InvalidInput('admin', admin, 
-                       'You must control the group you move it to')
+    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
     
 def testOwner(user, owner, machine):
     if owner in (None, machine.owner):
         return None
-    #XXX should you be able to transfer ownership if you don't already own it?
-    #if not owns(user, machine):
-    #    raise InvalidInput('owner', owner, "You don't own this machine, so you can't  transfer ownership")
     value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
     if value == True:
         return owner
     value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
     if value == True:
         return owner
@@ -618,7 +814,7 @@ 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 contact in (None, machine.contact):
         return None
     if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
-        raise InvalidInput('contact', contact, "Not a valid email")
+        raise InvalidInput('contact', contact, "Not a valid email.")
     return contact
 
 def testDisk(user, disksize, machine=None):
     return contact
 
 def testDisk(user, disksize, machine=None):
@@ -629,7 +825,7 @@ def testName(user, name, machine=None):
         return None
     if not Machine.select_by(name=name):
         return name
         return None
     if not Machine.select_by(name=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:
@@ -640,12 +836,11 @@ 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:
     olddisk = {}
     transaction = ctx.current.create_transaction()
     try:
@@ -656,7 +851,7 @@ def modify(user, fields):
         hostname = testHostname(owner, fields.getfirst('hostname'), machine)
         name = testName(user, fields.getfirst('name'), machine)
         oldname = machine.name
         hostname = testHostname(owner, fields.getfirst('hostname'), machine)
         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:
@@ -672,7 +867,8 @@ 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)
@@ -697,58 +893,94 @@ def modify(user, fields):
         for disk in machine.disks:
             remctl("web", "lvrename", oldname, disk.guest_device_name, name)
         remctl("web", "moveregister", oldname, name)
         for disk in machine.disks:
             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])    
-
+    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 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
+                        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
 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."""
-
+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:
                    )
     
     if not subjects:
-        subjects = sorted(mapping.keys())
+        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))
@@ -786,11 +1018,13 @@ def info(user, fields):
 
     nic_fields = getNicInfo(machine_info, machine)
     nic_point = display_fields.index('NIC_INFO')
 
     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:
@@ -805,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,
@@ -816,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,
@@ -825,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"
@@ -854,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=sys.stdout
-        errio.seek(0)
-        e = errio.read()
+
+        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=sys.stdout
-        errio.seek(0)
-        e = errio.read()
-        print error(operation, u, fields, err, e)
-    except InvalidInput, err:
-        print 'Content-Type: text/html\n'
-        sys.stderr=sys.stdout
-        errio.seek(0)
-        e = errio.read()
-        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=sys.stdout
-        errio.seek(0)
-        e = errio.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 '----'
         raise
         print e
         print '----'
         raise