disable default cherrypy http listener
[invirt/packages/invirt-web.git] / code / main.py
index 452a285..b712696 100755 (executable)
@@ -6,23 +6,17 @@ import cPickle
 import cgi
 import datetime
 import hmac
+import os
 import random
 import sha
-import simplejson
 import sys
 import time
 import urllib
+import socket
+import cherrypy
+from cherrypy import _cperror
 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"""
     if isinstance(sys.stderr, StringIO):
@@ -32,79 +26,372 @@ if __name__ == '__main__':
     import atexit
     atexit.register(printError)
 
-import templates
-from Cheetah.Template import Template
 import validation
 import cache_acls
-from webcommon import InvalidInput, CodeError, State
+from webcommon import State
 import controls
 from getafsgroups import getAfsGroupMembers
 from invirt import database
 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
 from invirt.config import structs as config
+from invirt.common import InvalidInput, CodeError
+
+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):
+    static = InvirtStatic
 
-def pathSplit(path):
-    if path.startswith('/'):
-        path = path[1:]
-    i = path.find('/')
-    if i == -1:
-        i = len(path)
-    return path[:i], path[i:]
+    @cherrypy.expose
+    @cherrypy.tools.mako(filename="/unauth.mako")
+    def index(self):
+        return dict(simple=True)
 
-class Checkpoint:
+class InvirtWeb(View):
     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 jquote(string):
-    return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
-
-def helppopup(subj):
-    """Return HTML code for a (?) link to a specified help topic"""
-    return ('<span class="helplink"><a href="help?' +
-            cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
-            +'" target="_blank" ' +
-            'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
-
-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.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)
+        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['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):
+        # 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 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)
+            return self
+        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."""
+        d = getListDict(cherrypy.request.login, cherrypy.request.state)
+        if result is not None:
+            d['result'] = result
+        return d
+    index=list
+
+    @cherrypy.expose
+    @cherrypy.tools.mako(filename="/help.mako")
+    def help(self, subject=None, simple=False):
+        """Handler for help messages."""
+
+        help_mapping = {
+            'Autoinstalls': """
+The autoinstaller builds a minimal Debian or Ubuntu system to run as a
+ParaVM.  You can access the resulting system by logging into the <a
+href="help?simple=true&subject=ParaVM+Console">serial console server</a>
+with your Kerberos tickets; there is no root password so sshd will
+refuse login.</p>
+
+<p>Under the covers, the autoinstaller uses our own patched version of
+xen-create-image, which is a tool based on debootstrap.  If you log
+into the serial console while the install is running, you can watch
+it.
+""",
+            'ParaVM Console': """
+ParaVM machines do not support local console access over VNC.  To
+access the serial console of these machines, you can SSH with Kerberos
+to %s, using the name of the machine as your
+username.""" % config.console.hostname,
+            'HVM/ParaVM': """
+HVM machines use the virtualization features of the processor, while
+ParaVM machines rely on a modified kernel to communicate directly with
+the hypervisor.  HVMs support boot CDs of any operating system, and
+the VNC console applet.  The three-minute autoinstaller produces
+ParaVMs.  ParaVMs typically are more efficient, and always support the
+<a href="help?subject=ParaVM+Console">console server</a>.</p>
+
+<p>More details are <a
+href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
+wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
+(which you can skip by using the autoinstaller to begin with.)</p>
+
+<p>We recommend using a ParaVM when possible and an HVM when necessary.
+""",
+            '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 who administers the LOCKER locker using the
+commands 'attach LOCKER; 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 locker may have a
+maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
+active machines.""",
+            'Console': """
+<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
+setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
+your machine will run just fine, but the applet's display of the
+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.
+"""
+            }
+
+        if not subject:
+            subject = sorted(help_mapping.keys())
+        if not isinstance(subject, list):
+            subject = [subject]
+
+        return dict(simple=simple,
+                    subjects=subject,
+                    mapping=help_mapping)
+    help._cp_config['tools.require_login.on'] = False
+
+    def parseCreate(self, fields):
+        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))
+
+    @cherrypy.expose
+    @cherrypy.tools.mako(filename="/list.mako")
+    @cherrypy.tools.require_POST()
+    def create(self, **fields):
+        """Handler for create requests."""
+        try:
+            parsed_fields = self.parseCreate(fields)
+            machine = controls.createVm(cherrypy.request.login,
+                                        cherrypy.request.state, **parsed_fields)
+        except InvalidInput, err:
+            pass
+        else:
+            err = None
+        cherrypy.request.state.clear() #Changed global state
+        d = getListDict(cherrypy.request.login, cherrypy.request.state)
+        d['err'] = err
+        if err:
+            for field, value in fields.items():
+                setattr(d['defaults'], field, value)
+        else:
+            d['new_machine'] = parsed_fields['name']
+        return d
+
+    @cherrypy.expose
+    @cherrypy.tools.mako(filename="/helloworld.mako")
+    def helloworld(self, **kwargs):
+        return {'request': cherrypy.request, 'kwargs': kwargs}
+    helloworld._cp_config['tools.require_login.on'] = False
+
+    @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):
+        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:
+                cherrypy.request.params['machine_id'] = int(name)
+                return self
+            except ValueError:
+                return None
+
+        @cherrypy.expose
+        @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
+            d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
+            return d
+        index = info
+
+        @cherrypy.expose
+        @cherrypy.tools.mako(filename="/info.mako")
+        @cherrypy.tools.require_POST()
+        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)
+            except InvalidInput, err:
+                result = None
+                machine = validation.Validate(cherrypy.request.login,
+                                              cherrypy.request.state,
+                                              machine_id=machine_id).machine
+            else:
+                machine = modify_dict['machine']
+                result = 'Success!'
+                err = None
+            info_dict = infoDict(cherrypy.request.login,
+                                 cherrypy.request.state, machine)
+            info_dict['err'] = err
+            if err:
+                for field, value in fields.items():
+                    setattr(info_dict['defaults'], field, value)
+            info_dict['result'] = result
+            return info_dict
+
+        @cherrypy.expose
+        @cherrypy.tools.mako(filename="/vnc.mako")
+        def vnc(self, machine_id):
+            """VNC applet page.
+
+            Note that due to same-domain restrictions, the applet connects to
+            the webserver, which needs to forward those requests to the xen
+            server.  The Xen server runs another proxy that (1) authenticates
+            and (2) finds the correct port for the VM.
+
+            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
+
+            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
+            token = controls.vnctoken(machine)
+            host = controls.listHost(machine)
+            if host:
+                port = 10003 + [h.hostname for h in config.hosts].index(host)
+            else:
+                port = 5900 # dummy
+
+            status = controls.statusInfo(machine)
+            has_vnc = hasVnc(status)
+
+            d = dict(on=status,
+                     has_vnc=has_vnc,
+                     machine=machine,
+                     hostname=cherrypy.request.local.name,
+                     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."""
+            back = kwargs.get('back')
+            if command_name == 'delete':
+                back = 'list'
+            try:
+                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
+                result = str(err)
+            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))
+            elif back == 'info':
+                raise cherrypy.HTTPRedirect(cherrypy.request.base
+                                            + '/machine/%d/' % machine_id,
+                                            status=303)
+            else:
+                raise InvalidInput('back', back, 'Not a known back page.')
+
+    machine = MachineView()
+
 
 class Defaults:
     """Class to store default values for fields."""
@@ -114,27 +401,17 @@ class Defaults:
     autoinstall = ''
     name = ''
     description = ''
+    administrator = ''
     type = 'linux-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)
+            self.disk = min(self.disk, max_disk)
         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:
@@ -145,64 +422,32 @@ def hasVnc(status):
             return 'location' in d
     return False
 
-def parseCreate(username, state, fields):
-    kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
-    validate = validation.Validate(username, state, strict=True, **kws)
-    return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
-                disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
-                cdrom=getattr(validate, 'cdrom', None),
-                autoinstall=getattr(validate, 'autoinstall', None))
-
-def create(username, state, path, fields):
-    """Handler for create requests."""
-    try:
-        parsed_fields = parseCreate(username, state, fields)
-        machine = controls.createVm(username, state, **parsed_fields)
-    except InvalidInput, err:
-        pass
-    else:
-        err = None
-    state.clear() #Changed global state
-    d = getListDict(username, state)
-    d['err'] = err
-    if err:
-        for field in fields.keys():
-            setattr(d['defaults'], field, fields.getfirst(field))
-    else:
-        d['new_machine'] = parsed_fields['name']
-    return templates.list(searchList=[d])
-
 
 def getListDict(username, state):
     """Gets the list of local variables used by list.tmpl."""
-    checkpoint.checkpoint('Starting')
     machines = state.machines
-    checkpoint.checkpoint('Got my machines')
     on = {}
     has_vnc = {}
+    installing = {}
     xmlist = state.xmlist
-    checkpoint.checkpoint('Got uptimes')
-    can_clone = 'ice3' not in state.xmlist_raw
     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"+helppopup("ParaVM Console")
+                has_vnc[m] = "ParaVM"
     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,
-                        cdrom='gutsy-i386')
-    checkpoint.checkpoint('Got defaults')
+                        owner=username)
     def sortkey(machine):
         return (machine.owner != username, machine.owner, machine.name)
     machines = sorted(machines, key=sortkey)
@@ -213,69 +458,25 @@ def getListDict(username, state):
              defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
-             can_clone=can_clone)
+             installing=installing,
+             disable_creation=False)
     return d
 
-def listVms(username, state, path, fields):
-    """Handler for list requests."""
-    checkpoint.checkpoint('Getting list dict')
-    d = getListDict(username, state)
-    checkpoint.checkpoint('Got list dict')
-    return templates.list(searchList=[d])
-
-def vnc(username, state, path, fields):
-    """VNC applet page.
-
-    Note that due to same-domain restrictions, the applet connects to
-    the webserver, which needs to forward those requests to the xen
-    server.  The Xen server runs another proxy that (1) authenticates
-    and (2) finds the correct port for the VM.
-
-    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
-
-    Remember to enable iptables!
-    echo 1 > /proc/sys/net/ipv4/ip_forward
-    """
-    machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
-
-    token = controls.remctl('control', machine.name, 'vnctoken')
-    host = controls.listHost(machine)
-    if host:
-        port = 10003 + [h.hostname for h in config.hosts].index(host)
-    else:
-        port = 5900 # dummy
-
-    status = controls.statusInfo(machine)
-    has_vnc = hasVnc(status)
-
-    d = dict(user=username,
-             on=status,
-             has_vnc=has_vnc,
-             machine=machine,
-             hostname=state.environ.get('SERVER_NAME', 'localhost'),
-             port=port,
-             authtoken=token)
-    return templates.vnc(searchList=[d])
-
 def getHostname(nic):
     """Find the hostname associated with a NIC.
 
     XXX this should be merged with the similar logic in DNS and DHCP.
     """
-    if nic.hostname and '.' in nic.hostname:
-        return nic.hostname
+    if nic.hostname:
+        hostname = nic.hostname
     elif nic.machine:
-        return nic.machine.name + '.' + config.dns.domains[0]
+        hostname = nic.machine.name
     else:
         return None
-
+    if '.' in hostname:
+        return hostname
+    else:
+        return hostname + '.' + config.dns.domains[0]
 
 def getNicInfo(data_dict, machine):
     """Helper function for info, get data on nics for a machine.
@@ -291,8 +492,7 @@ 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])
-        if not i:
-            data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
+        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
         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:
@@ -315,44 +515,18 @@ def getDiskInfo(data_dict, machine):
         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
     return disk_fields
 
-def command(username, state, path, fields):
-    """Handler for running commands like boot and delete on a VM."""
-    back = fields.getfirst('back')
-    try:
-        d = controls.commandResult(username, state, fields)
-        if d['command'] == 'Delete VM':
-            back = 'list'
-    except InvalidInput, err:
-        if not back:
-            raise
-        print >> sys.stderr, err
-        result = err
-    else:
-        result = 'Success!'
-        if not back:
-            return templates.command(searchList=[d])
-    if back == 'list':
-        state.clear() #Changed global state
-        d = getListDict(username, state)
-        d['result'] = result
-        return templates.list(searchList=[d])
-    elif back == 'info':
-        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
-        return ({'Status': '303 See Other',
-                 'Location': 'info?machine_id=%d' % machine.machine_id},
-                "You shouldn't see this message.")
-    else:
-        raise InvalidInput('back', back, 'Not a known back page.')
-
-def modifyDict(username, state, fields):
+def modifyDict(username, state, machine_id, fields):
     """Modify a machine as specified by CGI arguments.
 
-    Return a list of local variables for modify.tmpl.
+    Return a dict containing the machine that was modified.
     """
     olddisk = {}
     session.begin()
     try:
-        kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
+        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
         oldname = machine.name
@@ -363,20 +537,10 @@ def modifyDict(username, state, fields):
         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
-        if hasattr(validate, 'name'):
-            machine.name = validate.name
         if hasattr(validate, 'description'):
             machine.description = validate.description
         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
@@ -385,105 +549,52 @@ def modifyDict(username, state, fields):
         if hasattr(validate, 'contact'):
             machine.contact = validate.contact
 
-        session.save_or_update(machine)
-        if update_acl:
-            print >> sys.stderr, machine, machine.administrator
-            cache_acls.refreshMachine(machine)
+        session.add(machine)
         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(user=username,
-                command="modify",
-                machine=machine)
-
-def modify(username, state, path, fields):
-    """Handler for modifying attributes of a machine."""
-    try:
-        modify_dict = modifyDict(username, state, fields)
-    except InvalidInput, err:
-        result = None
-        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
-    else:
-        machine = modify_dict['machine']
-        result = 'Success!'
-        err = None
-    info_dict = infoDict(username, state, machine)
-    info_dict['err'] = err
-    if err:
-        for field in fields.keys():
-            setattr(info_dict['defaults'], field, fields.getfirst(field))
-    info_dict['result'] = result
-    return templates.info(searchList=[info_dict])
-
-
-def helpHandler(username, state, path, fields):
-    """Handler for help messages."""
-    simple = fields.getfirst('simple')
-    subjects = fields.getlist('subject')
-
-    help_mapping = {'ParaVM Console': """
-ParaVM machines do not support local console access over VNC.  To
-access the serial console of these machines, you can SSH with Kerberos
-to console.%s, using the name of the machine as your
-username.""" % config.dns.domains[0],
-                    '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.""",
-                    '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 who administers the LOCKER locker using the
-commands 'attach LOCKER; 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 locker may have a
-maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
-active machines.""",
-                    'Console': """
-<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
-setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
-your machine will run just fine, but the applet's display of the
-console will suffer artifacts.
-""",
-                    'Windows': """
-<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 req    uires 512 MB RAM and at least 7.5 GB disk space (15 GB 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.
-"""
-                    }
-
-    if not subjects:
-        subjects = sorted(help_mapping.keys())
 
-    d = dict(user=username,
-             simple=simple,
-             subjects=subjects,
-             mapping=help_mapping)
+    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
 
-    return templates.help(searchList=[d])
+    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)
 
-def badOperation(u, s, p, e):
-    """Function called when accessing an unknown URI."""
-    return ({'Status': '404 Not Found'}, 'Invalid operation.')
+    return dict(machine=machine)
 
 def infoDict(username, state, machine):
     """Get the variables used by info.tmpl."""
     status = controls.statusInfo(machine)
-    checkpoint.checkpoint('Getting status info')
     has_vnc = hasVnc(status)
     if status is None:
         main_status = dict(name=machine.name,
@@ -493,13 +604,14 @@ def infoDict(username, state, 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))
-    checkpoint.checkpoint('Status')
-    display_fields = """name uptime memory state cpu_weight on_reboot 
-     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
     display_fields = [('name', 'Name'),
                       ('description', 'Description'),
                       ('owner', 'Owner'),
@@ -513,13 +625,6 @@ def infoDict(username, state, machine):
                       ('memory', 'RAM'),
                       'DISK_INFO',
                       ('state', 'state (xen format)'),
-                      ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
-                      ('on_reboot', 'Action on VM reboot'),
-                      ('on_poweroff', 'Action on VM poweroff'),
-                      ('on_crash', 'Action on VM crash'),
-                      ('on_xend_start', 'Action on Xen start'),
-                      ('on_xend_stop', 'Action on Xen stop'),
-                      ('bootloader', 'Bootloader options'),
                       ]
     fields = []
     machine_info = {}
@@ -552,18 +657,14 @@ def infoDict(username, state, machine):
             pass
             #fields.append((disp, None))
 
-    checkpoint.checkpoint('Got fields')
-
-
     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():
-        setattr(defaults, name, getattr(machine, name))
+        if getattr(machine, name):
+            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,
@@ -573,55 +674,9 @@ def infoDict(username, state, machine):
              ram=machine.memory,
              max_mem=max_mem,
              max_disk=max_disk,
-             owner_help=helppopup("Owner"),
              fields = fields)
     return d
 
-def info(username, state, path, fields):
-    """Handler for info on a single VM."""
-    machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
-    d = infoDict(username, state, machine)
-    checkpoint.checkpoint('Got infodict')
-    return templates.info(searchList=[d])
-
-def unauthFront(_, _2, _3, fields):
-    """Information for unauth'd users."""
-    return templates.unauth(searchList=[{'simple' : True}])
-
-def admin(username, state, path, fields):
-    if path == '':
-        return ({'Status': '303 See Other',
-                 'Location': 'admin/'},
-                "You shouldn't see this message.")
-    if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
-        raise InvalidInput('username', username,
-                           'Not in admin group %s.' % config.web.adminacl)
-    newstate = State(username, isadmin=True)
-    newstate.environ = state.environ
-    return handler(username, newstate, path, fields)
-
-def throwError(_, __, ___, ____):
-    """Throw an error, to test the error-tracing mechanisms."""
-    raise RuntimeError("test of the emergency broadcast system")
-
-mapping = dict(list=listVms,
-               vnc=vnc,
-               command=command,
-               modify=modify,
-               info=info,
-               create=create,
-               help=helpHandler,
-               unauth=unauthFront,
-               admin=admin,
-               overlord=admin,
-               errortest=throwError)
-
-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
 
@@ -632,107 +687,10 @@ Subject: %s
 
 %s
 """ % (to, config.web.hostname, subject, body)
-    p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
+    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
+                         stdin=subprocess.PIPE)
     p.stdin.write(mail)
     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 getUser(environ):
-    """Return the current user based on the SSL environment variables"""
-    return environ.get('REMOTE_USER', None)
-
-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