Add graphs of network usage by VMs.
[invirt/packages/invirt-web.git] / code / main.py
index 361fbbd..86da50e 100755 (executable)
@@ -6,23 +6,16 @@ import cPickle
 import cgi
 import datetime
 import hmac
 import cgi
 import datetime
 import hmac
+import os
 import random
 import sha
 import random
 import sha
-import simplejson
 import sys
 import time
 import urllib
 import socket
 import cherrypy
 import sys
 import time
 import urllib
 import socket
 import cherrypy
+from cherrypy import _cperror
 from StringIO import StringIO
 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 ''
-    sys.stderr = sys.stdout
-    errio.seek(0)
-    return errio.read()
 
 def printError():
     """Revert stderr to stdout, and print the contents of stderr"""
 
 def printError():
     """Revert stderr to stdout, and print the contents of stderr"""
@@ -33,8 +26,6 @@ if __name__ == '__main__':
     import atexit
     atexit.register(printError)
 
     import atexit
     atexit.register(printError)
 
-import templates
-from Cheetah.Template import Template
 import validation
 import cache_acls
 from webcommon import State
 import validation
 import cache_acls
 from webcommon import State
@@ -45,25 +36,72 @@ from invirt.database import Machine, CDROM, session, connect, MachineAccess, Typ
 from invirt.config import structs as config
 from invirt.common import InvalidInput, CodeError
 
 from invirt.config import structs as config
 from invirt.common import InvalidInput, CodeError
 
-from view import View
+from view import View, revertStandardError
+
+
+static_dir = os.path.join(os.path.dirname(__file__), 'static')
+InvirtStatic = cherrypy.tools.staticdir.handler(
+    root=static_dir,
+    dir=static_dir,
+    section='/static')
 
 class InvirtUnauthWeb(View):
 
 class InvirtUnauthWeb(View):
+    static = InvirtStatic
+
     @cherrypy.expose
     @cherrypy.tools.mako(filename="/unauth.mako")
     def index(self):
     @cherrypy.expose
     @cherrypy.tools.mako(filename="/unauth.mako")
     def index(self):
-        return {'simple': True}
+        return dict(simple=True)
 
 class InvirtWeb(View):
     def __init__(self):
         super(self.__class__,self).__init__()
         connect()
         self._cp_config['tools.require_login.on'] = True
 
 class InvirtWeb(View):
     def __init__(self):
         super(self.__class__,self).__init__()
         connect()
         self._cp_config['tools.require_login.on'] = True
+        self._cp_config['tools.catch_stderr.on'] = True
         self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
                                                  'from invirt import database']
         self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
                                                  'from invirt import database']
+        self._cp_config['request.error_response'] = self.handle_error
+
+    static = InvirtStatic
+
+    @cherrypy.expose
+    @cherrypy.tools.mako(filename="/invalid.mako")
+    def invalidInput(self):
+        """Print an error page when an InvalidInput exception occurs"""
+        err = cherrypy.request.prev.params["err"]
+        emsg = cherrypy.request.prev.params["emsg"]
+        d = dict(err_field=err.err_field,
+                 err_value=str(err.err_value), stderr=emsg,
+                 errorMessage=str(err))
+        return d
+
+    @cherrypy.expose
+    @cherrypy.tools.mako(filename="/error.mako")
+    def error(self):
+        """Print an error page when an exception occurs"""
+        op = cherrypy.request.prev.path_info
+        username = cherrypy.request.login
+        err = cherrypy.request.prev.params["err"]
+        emsg = cherrypy.request.prev.params["emsg"]
+        traceback = cherrypy.request.prev.params["traceback"]
+        d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
+                 errorMessage=str(err), stderr=emsg, traceback=traceback)
+        error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
+        details = error_raw.render(**d)
+        exclude = config.web.errormail_exclude
+        if username not in exclude and '*' not in exclude:
+            send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
+                            details)
+        d['details'] = details
+        return d
 
     def __getattr__(self, name):
 
     def __getattr__(self, name):
+        # At the point __getattr__ is called, tools haven't been run. Make sure the user is logged in.
+        cherrypy.tools.remote_user_login.callable()
+
         if name in ("admin", "overlord"):
         if name in ("admin", "overlord"):
-            if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz[0].cell):
+            if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
                 raise InvalidInput('username', cherrypy.request.login,
                                    'Not in admin group %s.' % config.adminacl)
             cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
                 raise InvalidInput('username', cherrypy.request.login,
                                    'Not in admin group %s.' % config.adminacl)
             cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
@@ -71,15 +109,27 @@ class InvirtWeb(View):
         else:
             return super(InvirtWeb, self).__getattr__(name)
 
         else:
             return super(InvirtWeb, self).__getattr__(name)
 
+    def handle_error(self):
+        err = sys.exc_info()[1]
+        if isinstance(err, InvalidInput):
+            cherrypy.request.params['err'] = err
+            cherrypy.request.params['emsg'] = revertStandardError()
+            raise cherrypy.InternalRedirect('/invalidInput')
+        if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
+            cherrypy.request.params['err'] = err
+            cherrypy.request.params['emsg'] = revertStandardError()
+            cherrypy.request.params['traceback'] = _cperror.format_exc()
+            raise cherrypy.InternalRedirect('/error')
+        # fall back to cherrypy default error page
+        cherrypy.HTTPError(500).set_response()
+
     @cherrypy.expose
     @cherrypy.tools.mako(filename="/list.mako")
     def list(self, result=None):
         """Handler for list requests."""
     @cherrypy.expose
     @cherrypy.tools.mako(filename="/list.mako")
     def list(self, result=None):
         """Handler for list requests."""
-        checkpoint.checkpoint('Getting list dict')
         d = getListDict(cherrypy.request.login, cherrypy.request.state)
         if result is not None:
             d['result'] = result
         d = getListDict(cherrypy.request.login, cherrypy.request.state)
         if result is not None:
             d['result'] = result
-        checkpoint.checkpoint('Got list dict')
         return d
     index=list
 
         return d
     index=list
 
@@ -121,8 +171,6 @@ wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
 
 <p>We recommend using a ParaVM when possible and an HVM when necessary.
 """,
 
 <p>We recommend using a ParaVM when possible and an HVM when necessary.
 """,
-            '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
             'Owner': """
 The owner field is used to determine <a
 href="help?subject=Quotas">quotas</a>.  It must be the name of a
@@ -146,6 +194,7 @@ your machine will run just fine, but the applet's display of the
 console will suffer artifacts.
 """,
             'Windows': """
 console will suffer artifacts.
 """,
             'Windows': """
+<strong>Windows 7:</strong> The Windows 7 image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-7.txt">the licensing agreement</a> for details. The installer requires 512 MiB RAM and at least 15 GiB disk space (20 GiB or more recommended).<br>
 <strong>Windows Vista:</strong> The Vista image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-email.txt">the licensing confirmation e-mail</a> for details. The installer requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).<br>
 <strong>Windows XP:</strong> This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one, or visit <a href="http://msca.mit.edu/">http://msca.mit.edu/</a> if you are staff/faculty to request one.
 """
 <strong>Windows Vista:</strong> The Vista image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-email.txt">the licensing confirmation e-mail</a> for details. The installer requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).<br>
 <strong>Windows XP:</strong> This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one, or visit <a href="http://msca.mit.edu/">http://msca.mit.edu/</a> if you are staff/faculty to request one.
 """
@@ -162,10 +211,16 @@ console will suffer artifacts.
     help._cp_config['tools.require_login.on'] = False
 
     def parseCreate(self, fields):
     help._cp_config['tools.require_login.on'] = False
 
     def parseCreate(self, fields):
-        kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
-        validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
-        return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
-                    disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
+        kws = dict([(kw, fields[kw]) for kw in
+         'name description owner memory disksize vmtype cdrom autoinstall'.split()
+                    if fields[kw]])
+        validate = validation.Validate(cherrypy.request.login,
+                                       cherrypy.request.state,
+                                       strict=True, **kws)
+        return dict(contact=cherrypy.request.login, name=validate.name,
+                    description=validate.description, memory=validate.memory,
+                    disksize=validate.disksize, owner=validate.owner,
+                    machine_type=getattr(validate, 'vmtype', Defaults.type),
                     cdrom=getattr(validate, 'cdrom', None),
                     autoinstall=getattr(validate, 'autoinstall', None))
 
                     cdrom=getattr(validate, 'cdrom', None),
                     autoinstall=getattr(validate, 'autoinstall', None))
 
@@ -176,7 +231,8 @@ console will suffer artifacts.
         """Handler for create requests."""
         try:
             parsed_fields = self.parseCreate(fields)
         """Handler for create requests."""
         try:
             parsed_fields = self.parseCreate(fields)
-            machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
+            machine = controls.createVm(cherrypy.request.login,
+                                        cherrypy.request.state, **parsed_fields)
         except InvalidInput, err:
             pass
         else:
         except InvalidInput, err:
             pass
         else:
@@ -185,8 +241,8 @@ console will suffer artifacts.
         d = getListDict(cherrypy.request.login, cherrypy.request.state)
         d['err'] = err
         if err:
         d = getListDict(cherrypy.request.login, cherrypy.request.state)
         d['err'] = err
         if err:
-            for field in fields.keys():
-                setattr(d['defaults'], field, fields.get(field))
+            for field, value in fields.items():
+                setattr(d['defaults'], field, value)
         else:
             d['new_machine'] = parsed_fields['name']
         return d
         else:
             d['new_machine'] = parsed_fields['name']
         return d
@@ -200,17 +256,23 @@ console will suffer artifacts.
     @cherrypy.expose
     def errortest(self):
         """Throw an error, to test the error-tracing mechanisms."""
     @cherrypy.expose
     def errortest(self):
         """Throw an error, to test the error-tracing mechanisms."""
+        print >>sys.stderr, "look ma, it's a stderr"
         raise RuntimeError("test of the emergency broadcast system")
 
     class MachineView(View):
         raise RuntimeError("test of the emergency broadcast system")
 
     class MachineView(View):
-        # This is hairy. Fix when CherryPy 3.2 is out. (rename to
-        # _cp_dispatch, and parse the argument as a list instead of
-        # string
-
         def __getattr__(self, name):
         def __getattr__(self, name):
+            """Synthesize attributes to allow RESTful URLs like
+            /machine/13/info. This is hairy. CherryPy 3.2 adds a
+            method called _cp_dispatch that allows you to explicitly
+            handle URLs that can't be mapped, and it allows you to
+            rewrite the path components and continue processing.
+
+            This function gets the next path component being resolved
+            as a string. _cp_dispatch will get an array of strings
+            representing any subsequent path components as well."""
+
             try:
             try:
-                machine_id = int(name)
-                cherrypy.request.params['machine_id'] = machine_id
+                cherrypy.request.params['machine_id'] = int(name)
                 return self
             except ValueError:
                 return None
                 return self
             except ValueError:
                 return None
@@ -219,9 +281,10 @@ console will suffer artifacts.
         @cherrypy.tools.mako(filename="/info.mako")
         def info(self, machine_id):
             """Handler for info on a single VM."""
         @cherrypy.tools.mako(filename="/info.mako")
         def info(self, machine_id):
             """Handler for info on a single VM."""
-            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
+            machine = validation.Validate(cherrypy.request.login,
+                                          cherrypy.request.state,
+                                          machine_id=machine_id).machine
             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
-            checkpoint.checkpoint('Got infodict')
             return d
         index = info
 
             return d
         index = info
 
@@ -231,19 +294,24 @@ console will suffer artifacts.
         def modify(self, machine_id, **fields):
             """Handler for modifying attributes of a machine."""
             try:
         def modify(self, machine_id, **fields):
             """Handler for modifying attributes of a machine."""
             try:
-                modify_dict = modifyDict(cherrypy.request.login, cherrypy.request.state, machine_id, fields)
+                modify_dict = modifyDict(cherrypy.request.login,
+                                         cherrypy.request.state,
+                                         machine_id, fields)
             except InvalidInput, err:
                 result = None
             except InvalidInput, err:
                 result = None
-                machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
+                machine = validation.Validate(cherrypy.request.login,
+                                              cherrypy.request.state,
+                                              machine_id=machine_id).machine
             else:
                 machine = modify_dict['machine']
                 result = 'Success!'
                 err = None
             else:
                 machine = modify_dict['machine']
                 result = 'Success!'
                 err = None
-            info_dict = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
+            info_dict = infoDict(cherrypy.request.login,
+                                 cherrypy.request.state, machine)
             info_dict['err'] = err
             if err:
             info_dict['err'] = err
             if err:
-                for field in fields.keys():
-                    setattr(info_dict['defaults'], field, fields.get(field))
+                for field, value in fields.items():
+                    setattr(info_dict['defaults'], field, value)
             info_dict['result'] = result
             return info_dict
 
             info_dict['result'] = result
             return info_dict
 
@@ -269,8 +337,9 @@ console will suffer artifacts.
             Remember to enable iptables!
             echo 1 > /proc/sys/net/ipv4/ip_forward
             """
             Remember to enable iptables!
             echo 1 > /proc/sys/net/ipv4/ip_forward
             """
-            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
-
+            machine = validation.Validate(cherrypy.request.login,
+                                          cherrypy.request.state,
+                                          machine_id=machine_id).machine
             token = controls.vnctoken(machine)
             host = controls.listHost(machine)
             if host:
             token = controls.vnctoken(machine)
             host = controls.listHost(machine)
             if host:
@@ -288,86 +357,41 @@ console will suffer artifacts.
                      port=port,
                      authtoken=token)
             return d
                      port=port,
                      authtoken=token)
             return d
+
         @cherrypy.expose
         @cherrypy.tools.mako(filename="/command.mako")
         @cherrypy.tools.require_POST()
         def command(self, command_name, machine_id, **kwargs):
             """Handler for running commands like boot and delete on a VM."""
         @cherrypy.expose
         @cherrypy.tools.mako(filename="/command.mako")
         @cherrypy.tools.require_POST()
         def command(self, command_name, machine_id, **kwargs):
             """Handler for running commands like boot and delete on a VM."""
-            back = kwargs.get('back', None)
+            back = kwargs.get('back')
+            if command_name == 'delete':
+                back = 'list'
             try:
             try:
-                d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
-                if d['command'] == 'Delete VM':
-                    back = 'list'
+                d = controls.commandResult(cherrypy.request.login,
+                                           cherrypy.request.state,
+                                           command_name, machine_id, kwargs)
             except InvalidInput, err:
                 if not back:
                     raise
                 print >> sys.stderr, err
             except InvalidInput, err:
                 if not back:
                     raise
                 print >> sys.stderr, err
-                result = err
+                result = str(err)
             else:
                 result = 'Success!'
                 if not back:
                     return d
             if back == 'list':
                 cherrypy.request.state.clear() #Changed global state
             else:
                 result = 'Success!'
                 if not back:
                     return d
             if back == 'list':
                 cherrypy.request.state.clear() #Changed global state
-                raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
+                raise cherrypy.InternalRedirect('/list?result=%s'
+                                                % urllib.quote(result))
             elif back == 'info':
             elif back == 'info':
-                raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
+                raise cherrypy.HTTPRedirect(cherrypy.request.base
+                                            + '/machine/%d/' % machine_id,
+                                            status=303)
             else:
                 raise InvalidInput('back', back, 'Not a known back page.')
 
     machine = MachineView()
 
             else:
                 raise InvalidInput('back', back, 'Not a known back page.')
 
     machine = MachineView()
 
-def pathSplit(path):
-    if path.startswith('/'):
-        path = path[1:]
-    i = path.find('/')
-    if i == -1:
-        i = len(path)
-    return path[:i], path[i:]
-
-class Checkpoint:
-    def __init__(self):
-        self.start_time = time.time()
-        self.checkpoints = []
-
-    def checkpoint(self, s):
-        self.checkpoints.append((s, time.time()))
-
-    def __str__(self):
-        return ('Timing info:\n%s\n' %
-                '\n'.join(['%s: %s' % (d, t - self.start_time) for
-                           (d, t) in self.checkpoints]))
-
-checkpoint = Checkpoint()
-
-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.database = database
-Template.config = config
-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."""
 
 class Defaults:
     """Class to store default values for fields."""
@@ -388,17 +412,6 @@ class Defaults:
         for key in kws:
             setattr(self, key, kws[key])
 
         for key in kws:
             setattr(self, key, kws[key])
 
-
-
-DEFAULT_HEADERS = {'Content-Type': 'text/html'}
-
-def invalidInput(op, username, fields, err, emsg):
-    """Print an error page when an InvalidInput exception occurs"""
-    d = dict(op=op, user=username, err_field=err.err_field,
-             err_value=str(err.err_value), stderr=emsg,
-             errorMessage=str(err))
-    return templates.invalid(searchList=[d])
-
 def hasVnc(status):
     """Does the machine with a given status list support VNC?"""
     if status is None:
 def hasVnc(status):
     """Does the machine with a given status list support VNC?"""
     if status is None:
@@ -412,37 +425,29 @@ def hasVnc(status):
 
 def getListDict(username, state):
     """Gets the list of local variables used by list.tmpl."""
 
 def getListDict(username, state):
     """Gets the list of local variables used by list.tmpl."""
-    checkpoint.checkpoint('Starting')
     machines = state.machines
     machines = state.machines
-    checkpoint.checkpoint('Got my machines')
     on = {}
     has_vnc = {}
     installing = {}
     xmlist = state.xmlist
     on = {}
     has_vnc = {}
     installing = {}
     xmlist = state.xmlist
-    checkpoint.checkpoint('Got uptimes')
     for m in machines:
         if m not in xmlist:
             has_vnc[m] = 'Off'
             m.uptime = None
         else:
             m.uptime = xmlist[m]['uptime']
     for m in machines:
         if m not in xmlist:
             has_vnc[m] = 'Off'
             m.uptime = None
         else:
             m.uptime = xmlist[m]['uptime']
+            installing[m] = bool(xmlist[m].get('autoinstall'))
             if xmlist[m]['console']:
                 has_vnc[m] = True
             elif m.type.hvm:
                 has_vnc[m] = "WTF?"
             else:
                 has_vnc[m] = "ParaVM"
             if xmlist[m]['console']:
                 has_vnc[m] = True
             elif m.type.hvm:
                 has_vnc[m] = "WTF?"
             else:
                 has_vnc[m] = "ParaVM"
-            if xmlist[m].get('autoinstall'):
-                installing[m] = True
-            else:
-                installing[m] = False
     max_memory = validation.maxMemory(username, state)
     max_disk = validation.maxDisk(username)
     max_memory = validation.maxMemory(username, state)
     max_disk = validation.maxDisk(username)
-    checkpoint.checkpoint('Got max mem/disk')
     defaults = Defaults(max_memory=max_memory,
                         max_disk=max_disk,
                         owner=username)
     defaults = Defaults(max_memory=max_memory,
                         max_disk=max_disk,
                         owner=username)
-    checkpoint.checkpoint('Got defaults')
     def sortkey(machine):
         return (machine.owner != username, machine.owner, machine.name)
     machines = sorted(machines, key=sortkey)
     def sortkey(machine):
         return (machine.owner != username, machine.owner, machine.name)
     machines = sorted(machines, key=sortkey)
@@ -453,7 +458,8 @@ def getListDict(username, state):
              defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
              defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
-             installing=installing)
+             installing=installing,
+             disable_creation=False)
     return d
 
 def getHostname(nic):
     return d
 
 def getHostname(nic):
@@ -517,7 +523,9 @@ def modifyDict(username, state, machine_id, fields):
     olddisk = {}
     session.begin()
     try:
     olddisk = {}
     session.begin()
     try:
-        kws = dict([(kw, fields.get(kw)) for kw in 'owner admin contact name description memory vmtype disksize'.split() if fields.get(kw)])
+        kws = dict((kw, fields[kw]) for kw in
+         'owner admin contact name description memory vmtype disksize'.split()
+                    if fields.get(kw))
         kws['machine_id'] = machine_id
         validate = validation.Validate(username, state, **kws)
         machine = validate.machine
         kws['machine_id'] = machine_id
         validate = validation.Validate(username, state, **kws)
         machine = validate.machine
@@ -529,23 +537,10 @@ def modifyDict(username, state, machine_id, fields):
         if hasattr(validate, 'vmtype'):
             machine.type = validate.vmtype
 
         if hasattr(validate, 'vmtype'):
             machine.type = validate.vmtype
 
-        if hasattr(validate, 'disksize'):
-            disksize = validate.disksize
-            disk = machine.disks[0]
-            if disk.size != disksize:
-                olddisk[disk.guest_device_name] = disksize
-                disk.size = disksize
-                session.save_or_update(disk)
-
         update_acl = False
         if hasattr(validate, 'owner') and validate.owner != machine.owner:
             machine.owner = validate.owner
             update_acl = True
         update_acl = False
         if hasattr(validate, 'owner') and validate.owner != machine.owner:
             machine.owner = validate.owner
             update_acl = True
-        if hasattr(validate, 'name'):
-            machine.name = validate.name
-            for n in machine.nics:
-                if n.hostname == oldname:
-                    n.hostname = validate.name
         if hasattr(validate, 'description'):
             machine.description = validate.description
         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
         if hasattr(validate, 'description'):
             machine.description = validate.description
         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
@@ -554,28 +549,56 @@ def modifyDict(username, state, machine_id, fields):
         if hasattr(validate, 'contact'):
             machine.contact = validate.contact
 
         if hasattr(validate, 'contact'):
             machine.contact = validate.contact
 
-        session.save_or_update(machine)
-        if update_acl:
-            cache_acls.refreshMachine(machine)
+        session.add(machine)
         session.commit()
     except:
         session.rollback()
         raise
         session.commit()
     except:
         session.rollback()
         raise
-    for diskname in olddisk:
-        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
-    if hasattr(validate, 'name'):
-        controls.renameMachine(machine, oldname, validate.name)
-    return dict(machine=machine)
 
 
+    session.begin()
+    try:
+        if hasattr(validate, 'disksize'):
+            disksize = validate.disksize
+            disk = machine.disks[0]
+            if disk.size != disksize:
+                olddisk[disk.guest_device_name] = disksize
+                disk.size = disksize
+                session.add(disk)
+        for diskname in olddisk:
+            controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
+        session.add(machine)
+        session.commit()
+    except:
+        session.rollback()
+        raise
 
 
-def badOperation(u, s, p, e):
-    """Function called when accessing an unknown URI."""
-    return ({'Status': '404 Not Found'}, 'Invalid operation.')
+    session.begin()
+    try:
+        if hasattr(validate, 'name'):
+            machine.name = validate.name
+            for n in machine.nics:
+                if n.hostname == oldname:
+                    n.hostname = validate.name
+        if hasattr(validate, 'name'):
+            controls.renameMachine(machine, oldname, validate.name)
+        session.add(machine)
+        session.commit()
+    except:
+        session.rollback()
+        raise
+
+    if update_acl:
+        cache_acls.refreshMachine(machine)
+
+    return dict(machine=machine)
 
 def infoDict(username, state, machine):
     """Get the variables used by info.tmpl."""
 
 def infoDict(username, state, machine):
     """Get the variables used by info.tmpl."""
-    status = controls.statusInfo(machine)
-    checkpoint.checkpoint('Getting status info')
+    try:
+        status = controls.statusInfo(machine)
+    except CodeError, e:
+        # machine was shut down in between the call to listInfoDict and this
+        status = None
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
@@ -585,11 +608,14 @@ def infoDict(username, state, machine):
     else:
         main_status = dict(status[1:])
         main_status['host'] = controls.listHost(machine)
     else:
         main_status = dict(status[1:])
         main_status['host'] = controls.listHost(machine)
-        start_time = float(main_status.get('start_time', 0))
-        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
+        start_time = main_status.get('start_time')
+        if start_time is None:
+            uptime = "Still booting?"
+        else:
+            start_time = float(start_time)
+            uptime = datetime.timedelta(seconds=int(time.time()-start_time))
         cpu_time_float = float(main_status.get('cpu_time', 0))
         cputime = datetime.timedelta(seconds=int(cpu_time_float))
         cpu_time_float = float(main_status.get('cpu_time', 0))
         cputime = datetime.timedelta(seconds=int(cpu_time_float))
-    checkpoint.checkpoint('Status')
     display_fields = [('name', 'Name'),
                       ('description', 'Description'),
                       ('owner', 'Owner'),
     display_fields = [('name', 'Name'),
                       ('description', 'Description'),
                       ('owner', 'Owner'),
@@ -635,11 +661,7 @@ def infoDict(username, state, machine):
             pass
             #fields.append((disp, None))
 
             pass
             #fields.append((disp, None))
 
-    checkpoint.checkpoint('Got fields')
-
-
     max_mem = validation.maxMemory(machine.owner, state, machine, False)
     max_mem = validation.maxMemory(machine.owner, state, machine, False)
-    checkpoint.checkpoint('Got mem')
     max_disk = validation.maxDisk(machine.owner, machine)
     defaults = Defaults()
     for name in 'machine_id name description administrator owner memory contact'.split():
     max_disk = validation.maxDisk(machine.owner, machine)
     defaults = Defaults()
     for name in 'machine_id name description administrator owner memory contact'.split():
@@ -647,7 +669,6 @@ def infoDict(username, state, machine):
             setattr(defaults, name, getattr(machine, name))
     defaults.type = machine.type.type_id
     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
             setattr(defaults, name, getattr(machine, name))
     defaults.type = machine.type.type_id
     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
-    checkpoint.checkpoint('Got defaults')
     d = dict(user=username,
              on=status is not None,
              machine=machine,
     d = dict(user=username,
              on=status is not None,
              machine=machine,
@@ -660,14 +681,6 @@ def infoDict(username, state, machine):
              fields = fields)
     return d
 
              fields = fields)
     return d
 
-mapping = dict()
-
-def printHeaders(headers):
-    """Print a dictionary as HTTP headers."""
-    for key, value in headers.iteritems():
-        print '%s: %s' % (key, value)
-    print
-
 def send_error_mail(subject, body):
     import subprocess
 
 def send_error_mail(subject, body):
     import subprocess
 
@@ -684,98 +697,4 @@ Subject: %s
     p.stdin.close()
     p.wait()
 
     p.stdin.close()
     p.wait()
 
-def show_error(op, username, fields, err, emsg, traceback):
-    """Print an error page when an exception occurs"""
-    d = dict(op=op, user=username, fields=fields,
-             errorMessage=str(err), stderr=emsg, traceback=traceback)
-    details = templates.error_raw(searchList=[d])
-    exclude = config.web.errormail_exclude
-    if username not in exclude and '*' not in exclude:
-        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
-                        details)
-    d['details'] = details
-    return templates.error(searchList=[d])
-
-def handler(username, state, path, fields):
-    operation, path = pathSplit(path)
-    if not operation:
-        operation = 'list'
-    print 'Starting', operation
-    fun = mapping.get(operation, badOperation)
-    return fun(username, state, path, fields)
-
-class App:
-    def __init__(self, environ, start_response):
-        self.environ = environ
-        self.start = start_response
-
-        self.username = getUser(environ)
-        self.state = State(self.username)
-        self.state.environ = environ
-
-        random.seed() #sigh
-
-    def __iter__(self):
-        start_time = time.time()
-        database.clear_cache()
-        sys.stderr = StringIO()
-        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
-        operation = self.environ.get('PATH_INFO', '')
-        if not operation:
-            self.start("301 Moved Permanently", [('Location', './')])
-            return
-        if self.username is None:
-            operation = 'unauth'
-
-        try:
-            checkpoint.checkpoint('Before')
-            output = handler(self.username, self.state, operation, fields)
-            checkpoint.checkpoint('After')
-
-            headers = dict(DEFAULT_HEADERS)
-            if isinstance(output, tuple):
-                new_headers, output = output
-                headers.update(new_headers)
-            e = revertStandardError()
-            if e:
-                if hasattr(output, 'addError'):
-                    output.addError(e)
-                else:
-                    # This only happens on redirects, so it'd be a pain to get
-                    # the message to the user.  Maybe in the response is useful.
-                    output = output + '\n\nstderr:\n' + e
-            output_string =  str(output)
-            checkpoint.checkpoint('output as a string')
-        except Exception, err:
-            if not fields.has_key('js'):
-                if isinstance(err, InvalidInput):
-                    self.start('200 OK', [('Content-Type', 'text/html')])
-                    e = revertStandardError()
-                    yield str(invalidInput(operation, self.username, fields,
-                                           err, e))
-                    return
-            import traceback
-            self.start('500 Internal Server Error',
-                       [('Content-Type', 'text/html')])
-            e = revertStandardError()
-            s = show_error(operation, self.username, fields,
-                           err, e, traceback.format_exc())
-            yield str(s)
-            return
-        status = headers.setdefault('Status', '200 OK')
-        del headers['Status']
-        self.start(status, headers.items())
-        yield output_string
-        if fields.has_key('timedebug'):
-            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
-
-def constructor():
-    connect()
-    return App
-
-def main():
-    from flup.server.fcgi_fork import WSGIServer
-    WSGIServer(constructor()).run()
-
-if __name__ == '__main__':
-    main()
+random.seed() #sigh