Show hostnames for every NIC
[invirt/packages/invirt-web.git] / code / main.py
index a9e54a8..908d7cd 100755 (executable)
@@ -36,13 +36,13 @@ import templates
 from Cheetah.Template import Template
 import validation
 import cache_acls
 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
 import controls
 from getafsgroups import getAfsGroupMembers
-import sipb_xen_database
 from invirt import database
 from invirt import database
-from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
+from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
 from invirt.config import structs as config
 from invirt.config import structs as config
+from invirt.common import InvalidInput, CodeError
 
 def pathSplit(path):
     if path.startswith('/'):
 
 def pathSplit(path):
     if path.startswith('/'):
@@ -85,7 +85,8 @@ def makeErrorPre(old, addition):
     else:
         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
 
     else:
         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
 
-Template.sipb_xen_database = sipb_xen_database
+Template.database = database
+Template.config = config
 Template.helppopup = staticmethod(helppopup)
 Template.err = None
 
 Template.helppopup = staticmethod(helppopup)
 Template.err = None
 
@@ -120,7 +121,7 @@ class Defaults:
         if max_memory is not None:
             self.memory = min(self.memory, max_memory)
         if max_disk is not None:
         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])
 
         for key in kws:
             setattr(self, key, kws[key])
 
@@ -200,8 +201,7 @@ def getListDict(username, state):
     checkpoint.checkpoint('Got max mem/disk')
     defaults = Defaults(max_memory=max_memory,
                         max_disk=max_disk,
     checkpoint.checkpoint('Got max mem/disk')
     defaults = Defaults(max_memory=max_memory,
                         max_disk=max_disk,
-                        owner=username,
-                        cdrom='gutsy-i386')
+                        owner=username)
     checkpoint.checkpoint('Got defaults')
     def sortkey(machine):
         return (machine.owner != username, machine.owner, machine.name)
     checkpoint.checkpoint('Got defaults')
     def sortkey(machine):
         return (machine.owner != username, machine.owner, machine.name)
@@ -245,18 +245,7 @@ def vnc(username, state, path, fields):
     """
     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
 
     """
     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
 
-    TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
-
-    data = {}
-    data["user"] = username
-    data["machine"] = machine.name
-    data["expires"] = time.time()+(5*60)
-    pickled_data = cPickle.dumps(data)
-    m = hmac.new(TOKEN_KEY, digestmod=sha)
-    m.update(pickled_data)
-    token = {'data': pickled_data, 'digest': m.digest()}
-    token = cPickle.dumps(token)
-    token = base64.urlsafe_b64encode(token)
+    token = controls.vnctoken(machine)
     host = controls.listHost(machine)
     if host:
         port = 10003 + [h.hostname for h in config.hosts].index(host)
     host = controls.listHost(machine)
     if host:
         port = 10003 + [h.hostname for h in config.hosts].index(host)
@@ -280,13 +269,16 @@ def getHostname(nic):
 
     XXX this should be merged with the similar logic in DNS and DHCP.
     """
 
     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:
     elif nic.machine:
-        return nic.machine.name + '.' + config.dns.domains[0]
+        hostname = nic.machine.name
     else:
         return None
     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.
 
 def getNicInfo(data_dict, machine):
     """Helper function for info, get data on nics for a machine.
@@ -302,8 +294,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])
     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:
         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:
@@ -361,7 +352,7 @@ def modifyDict(username, state, fields):
     Return a list of local variables for modify.tmpl.
     """
     olddisk = {}
     Return a list of local variables for modify.tmpl.
     """
     olddisk = {}
-    transaction = ctx.current.create_transaction()
+    session.begin()
     try:
         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
         validate = validation.Validate(username, state, **kws)
     try:
         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
         validate = validation.Validate(username, state, **kws)
@@ -380,7 +371,7 @@ def modifyDict(username, state, fields):
             if disk.size != disksize:
                 olddisk[disk.guest_device_name] = disksize
                 disk.size = disksize
             if disk.size != disksize:
                 olddisk[disk.guest_device_name] = disksize
                 disk.size = disksize
-                ctx.current.save(disk)
+                session.save_or_update(disk)
 
         update_acl = False
         if hasattr(validate, 'owner') and validate.owner != machine.owner:
 
         update_acl = False
         if hasattr(validate, 'owner') and validate.owner != machine.owner:
@@ -396,13 +387,12 @@ def modifyDict(username, state, fields):
         if hasattr(validate, 'contact'):
             machine.contact = validate.contact
 
         if hasattr(validate, 'contact'):
             machine.contact = validate.contact
 
-        ctx.current.save(machine)
+        session.save_or_update(machine)
         if update_acl:
         if update_acl:
-            print >> sys.stderr, machine, machine.administrator
             cache_acls.refreshMachine(machine)
             cache_acls.refreshMachine(machine)
-        transaction.commit()
+        session.commit()
     except:
     except:
-        transaction.rollback()
+        session.rollback()
         raise
     for diskname in olddisk:
         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
         raise
     for diskname in olddisk:
         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
@@ -437,15 +427,39 @@ def helpHandler(username, state, path, fields):
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
 
     simple = fields.getfirst('simple')
     subjects = fields.getlist('subject')
 
-    help_mapping = {'ParaVM Console': """
+    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
 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],
+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
                     '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.""",
+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.
+""",
                     'CPU Weight': """
 Don't ask us!  We're as mystified as you are.""",
                     'Owner': """
                     'CPU Weight': """
 Don't ask us!  We're as mystified as you are.""",
                     'Owner': """
@@ -469,6 +483,10 @@ active machines.""",
 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.
 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.
 """
                     }
 
 """
                     }
 
@@ -595,14 +613,15 @@ def unauthFront(_, _2, _3, fields):
     """Information for unauth'd users."""
     return templates.unauth(searchList=[{'simple' : True}])
 
     """Information for unauth'd users."""
     return templates.unauth(searchList=[{'simple' : True}])
 
-def overlord(username, state, path, fields):
+def admin(username, state, path, fields):
     if path == '':
         return ({'Status': '303 See Other',
     if path == '':
         return ({'Status': '303 See Other',
-                 'Location': 'overlord/'},
+                 'Location': 'admin/'},
                 "You shouldn't see this message.")
                 "You shouldn't see this message.")
-    if not username in getAfsGroupMembers('system:xvm', 'athena.mit.edu'):
-        raise InvalidInput('username', username, 'Not an overlord.')
-    newstate = State(username, overlord=True)
+    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)
 
     newstate.environ = state.environ
     return handler(username, newstate, path, fields)
 
@@ -618,7 +637,8 @@ mapping = dict(list=listVms,
                create=create,
                help=helpHandler,
                unauth=unauthFront,
                create=create,
                help=helpHandler,
                unauth=unauthFront,
-               overlord=overlord,
+               admin=admin,
+               overlord=admin,
                errortest=throwError)
 
 def printHeaders(headers):
                errortest=throwError)
 
 def printHeaders(headers):
@@ -637,7 +657,8 @@ Subject: %s
 
 %s
 """ % (to, config.web.hostname, subject, body)
 
 %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()
     p.stdin.write(mail)
     p.stdin.close()
     p.wait()
@@ -647,7 +668,8 @@ def show_error(op, username, fields, err, emsg, traceback):
     d = dict(op=op, user=username, fields=fields,
              errorMessage=str(err), stderr=emsg, traceback=traceback)
     details = templates.error_raw(searchList=[d])
     d = dict(op=op, user=username, fields=fields,
              errorMessage=str(err), stderr=emsg, traceback=traceback)
     details = templates.error_raw(searchList=[d])
-    if username not in ('price', 'ecprice', 'andersk'): #add yourself at will
+    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
         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
                         details)
     d['details'] = details
@@ -655,7 +677,18 @@ def show_error(op, username, fields, err, emsg, traceback):
 
 def getUser(environ):
     """Return the current user based on the SSL environment variables"""
 
 def getUser(environ):
     """Return the current user based on the SSL environment variables"""
-    return environ.get('REMOTE_USER', None)
+    user = environ.get('REMOTE_USER')
+    if user is None:
+        return
+    
+    if environ.get('AUTH_TYPE') == 'Negotiate':
+        # Convert the krb5 principal into a krb4 username
+        if not user.endswith('@%s' % config.kerberos.realm):
+            return
+        else:
+            return user.split('@')[0].replace('/', '.')
+    else:
+        return user
 
 def handler(username, state, path, fields):
     operation, path = pathSplit(path)
 
 def handler(username, state, path, fields):
     operation, path = pathSplit(path)
@@ -678,7 +711,7 @@ class App:
 
     def __iter__(self):
         start_time = time.time()
 
     def __iter__(self):
         start_time = time.time()
-        sipb_xen_database.clear_cache()
+        database.clear_cache()
         sys.stderr = StringIO()
         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
         operation = self.environ.get('PATH_INFO', '')
         sys.stderr = StringIO()
         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
         operation = self.environ.get('PATH_INFO', '')