Removed outdated CPU weight and added Windows 7 help.
[invirt/packages/invirt-web.git] / code / main.py
index de0a739..cf0aca2 100755 (executable)
@@ -6,6 +6,7 @@ import cPickle
 import cgi
 import datetime
 import hmac
+import os
 import random
 import sha
 import sys
@@ -37,11 +38,20 @@ 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
+
     @cherrypy.expose
     @cherrypy.tools.mako(filename="/unauth.mako")
     def index(self):
-        return {'simple': True}
+        return dict(simple=True)
 
 class InvirtWeb(View):
     def __init__(self):
@@ -53,6 +63,8 @@ class InvirtWeb(View):
                                                  '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):
@@ -67,14 +79,13 @@ class InvirtWeb(View):
     @cherrypy.expose
     @cherrypy.tools.mako(filename="/error.mako")
     def error(self):
-        #op, username, fields, err, emsg, traceback):
         """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,
+        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)
@@ -86,8 +97,11 @@ class InvirtWeb(View):
         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[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)
@@ -98,14 +112,12 @@ class InvirtWeb(View):
     def handle_error(self):
         err = sys.exc_info()[1]
         if isinstance(err, InvalidInput):
-            e = revertStandardError()
             cherrypy.request.params['err'] = err
-            cherrypy.request.params['emsg'] = e
+            cherrypy.request.params['emsg'] = revertStandardError()
             raise cherrypy.InternalRedirect('/invalidInput')
         if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
-            e = revertStandardError()
             cherrypy.request.params['err'] = err
-            cherrypy.request.params['emsg'] = e
+            cherrypy.request.params['emsg'] = revertStandardError()
             cherrypy.request.params['traceback'] = _cperror.format_exc()
             raise cherrypy.InternalRedirect('/error')
         # fall back to cherrypy default error page
@@ -115,11 +127,9 @@ class InvirtWeb(View):
     @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
-        checkpoint.checkpoint('Got list dict')
         return d
     index=list
 
@@ -161,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.
 """,
-            '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
@@ -186,8 +194,9 @@ 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. 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 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. 
 """
             }
 
@@ -202,10 +211,16 @@ console will suffer artifacts.
     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))
 
@@ -216,7 +231,8 @@ console will suffer artifacts.
         """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:
@@ -225,8 +241,8 @@ console will suffer artifacts.
         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
@@ -244,14 +260,19 @@ console will suffer artifacts.
         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):
+            """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:
-                machine_id = int(name)
-                cherrypy.request.params['machine_id'] = machine_id
+                cherrypy.request.params['machine_id'] = int(name)
                 return self
             except ValueError:
                 return None
@@ -260,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."""
-            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)
-            checkpoint.checkpoint('Got infodict')
             return d
         index = info
 
@@ -272,19 +294,24 @@ console will suffer artifacts.
         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
-                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
-            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:
-                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
 
@@ -310,8 +337,9 @@ console will suffer artifacts.
             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:
@@ -329,16 +357,19 @@ console will suffer artifacts.
                      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', None)
+            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)
-                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
@@ -350,28 +381,17 @@ console will suffer artifacts.
                     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':
-                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()
 
-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()
 
 class Defaults:
     """Class to store default values for fields."""
@@ -405,37 +425,29 @@ def hasVnc(status):
 
 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')
     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].get('autoinstall'):
-                installing[m] = True
-            else:
-                installing[m] = False
     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)
-    checkpoint.checkpoint('Got defaults')
     def sortkey(machine):
         return (machine.owner != username, machine.owner, machine.name)
     machines = sorted(machines, key=sortkey)
@@ -446,7 +458,8 @@ def getListDict(username, state):
              defaults=defaults,
              machines=machines,
              has_vnc=has_vnc,
-             installing=installing)
+             installing=installing,
+             disable_creation=False)
     return d
 
 def getHostname(nic):
@@ -510,7 +523,9 @@ def modifyDict(username, state, machine_id, fields):
     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
@@ -522,6 +537,26 @@ def modifyDict(username, state, machine_id, fields):
         if hasattr(validate, 'vmtype'):
             machine.type = validate.vmtype
 
+        update_acl = False
+        if hasattr(validate, 'owner') and validate.owner != machine.owner:
+            machine.owner = validate.owner
+            update_acl = True
+        if hasattr(validate, 'description'):
+            machine.description = validate.description
+        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
+            machine.administrator = validate.admin
+            update_acl = True
+        if hasattr(validate, 'contact'):
+            machine.contact = validate.contact
+
+        session.save_or_update(machine)
+        session.commit()
+    except:
+        session.rollback()
+        raise
+
+    session.begin()
+    try:
         if hasattr(validate, 'disksize'):
             disksize = validate.disksize
             disk = machine.disks[0]
@@ -529,41 +564,37 @@ def modifyDict(username, state, machine_id, fields):
                 olddisk[disk.guest_device_name] = disksize
                 disk.size = disksize
                 session.save_or_update(disk)
+        for diskname in olddisk:
+            controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
+        session.save_or_update(machine)
+        session.commit()
+    except:
+        session.rollback()
+        raise
 
-        update_acl = False
-        if hasattr(validate, 'owner') and validate.owner != machine.owner:
-            machine.owner = validate.owner
-            update_acl = True
+    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, 'description'):
-            machine.description = validate.description
-        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
-            machine.administrator = validate.admin
-            update_acl = True
-        if hasattr(validate, 'contact'):
-            machine.contact = validate.contact
-
+        if hasattr(validate, 'name'):
+            controls.renameMachine(machine, oldname, validate.name)
         session.save_or_update(machine)
-        if update_acl:
-            cache_acls.refreshMachine(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)
+
+    if update_acl:
+        cache_acls.refreshMachine(machine)
+
     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,
@@ -573,11 +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', 'Name'),
                       ('description', 'Description'),
                       ('owner', 'Owner'),
@@ -623,11 +657,7 @@ 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():
@@ -635,7 +665,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.)
-    checkpoint.checkpoint('Got defaults')
     d = dict(user=username,
              on=status is not None,
              machine=machine,
@@ -664,4 +693,4 @@ Subject: %s
     p.stdin.close()
     p.wait()
 
-random.seed()
+random.seed() #sigh