Incremented version to not conflict with precise
[invirt/packages/invirt-web.git] / code / main.py
index 786a087..e5722fa 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):
@@ -85,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)
@@ -112,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
 
@@ -158,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
@@ -183,6 +194,7 @@ 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.
 """
@@ -199,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))
 
@@ -213,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:
@@ -222,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
@@ -241,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
@@ -257,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
 
@@ -269,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
 
@@ -307,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:
@@ -326,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
@@ -347,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."""
@@ -402,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)
@@ -443,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):
@@ -507,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
@@ -519,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]
@@ -526,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,
@@ -570,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'),
@@ -620,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():
@@ -632,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,
@@ -661,4 +693,4 @@ Subject: %s
     p.stdin.close()
     p.wait()
 
-random.seed()
+random.seed() #sigh