Fix-up several packages to include the correct Conflicts and Replaces
[invirt/packages/invirt-web.git] / code / main.py
1 #!/usr/bin/python
2 """Main CGI script for web interface"""
3
4 import base64
5 import cPickle
6 import cgi
7 import datetime
8 import hmac
9 import random
10 import sha
11 import simplejson
12 import sys
13 import time
14 import urllib
15 from StringIO import StringIO
16
17 def revertStandardError():
18     """Move stderr to stdout, and return the contents of the old stderr."""
19     errio = sys.stderr
20     if not isinstance(errio, StringIO):
21         return ''
22     sys.stderr = sys.stdout
23     errio.seek(0)
24     return errio.read()
25
26 def printError():
27     """Revert stderr to stdout, and print the contents of stderr"""
28     if isinstance(sys.stderr, StringIO):
29         print revertStandardError()
30
31 if __name__ == '__main__':
32     import atexit
33     atexit.register(printError)
34
35 import templates
36 from Cheetah.Template import Template
37 import validation
38 import cache_acls
39 from webcommon import State
40 import controls
41 from getafsgroups import getAfsGroupMembers
42 from invirt import database
43 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
44 from invirt.config import structs as config
45 from invirt.common import InvalidInput, CodeError
46
47 def pathSplit(path):
48     if path.startswith('/'):
49         path = path[1:]
50     i = path.find('/')
51     if i == -1:
52         i = len(path)
53     return path[:i], path[i:]
54
55 class Checkpoint:
56     def __init__(self):
57         self.start_time = time.time()
58         self.checkpoints = []
59
60     def checkpoint(self, s):
61         self.checkpoints.append((s, time.time()))
62
63     def __str__(self):
64         return ('Timing info:\n%s\n' %
65                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
66                            (d, t) in self.checkpoints]))
67
68 checkpoint = Checkpoint()
69
70 def jquote(string):
71     return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
72
73 def helppopup(subj):
74     """Return HTML code for a (?) link to a specified help topic"""
75     return ('<span class="helplink"><a href="help?' +
76             cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
77             +'" target="_blank" ' +
78             'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
79
80 def makeErrorPre(old, addition):
81     if addition is None:
82         return
83     if old:
84         return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
85     else:
86         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
87
88 Template.database = database
89 Template.config = config
90 Template.helppopup = staticmethod(helppopup)
91 Template.err = None
92
93 class JsonDict:
94     """Class to store a dictionary that will be converted to JSON"""
95     def __init__(self, **kws):
96         self.data = kws
97         if 'err' in kws:
98             err = kws['err']
99             del kws['err']
100             self.addError(err)
101
102     def __str__(self):
103         return simplejson.dumps(self.data)
104
105     def addError(self, text):
106         """Add stderr text to be displayed on the website."""
107         self.data['err'] = \
108             makeErrorPre(self.data.get('err'), text)
109
110 class Defaults:
111     """Class to store default values for fields."""
112     memory = 256
113     disk = 4.0
114     cdrom = ''
115     autoinstall = ''
116     name = ''
117     description = ''
118     type = 'linux-hvm'
119
120     def __init__(self, max_memory=None, max_disk=None, **kws):
121         if max_memory is not None:
122             self.memory = min(self.memory, max_memory)
123         if max_disk is not None:
124             self.disk = min(self.disk, max_disk)
125         for key in kws:
126             setattr(self, key, kws[key])
127
128
129
130 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
131
132 def invalidInput(op, username, fields, err, emsg):
133     """Print an error page when an InvalidInput exception occurs"""
134     d = dict(op=op, user=username, err_field=err.err_field,
135              err_value=str(err.err_value), stderr=emsg,
136              errorMessage=str(err))
137     return templates.invalid(searchList=[d])
138
139 def hasVnc(status):
140     """Does the machine with a given status list support VNC?"""
141     if status is None:
142         return False
143     for l in status:
144         if l[0] == 'device' and l[1][0] == 'vfb':
145             d = dict(l[1][1:])
146             return 'location' in d
147     return False
148
149 def parseCreate(username, state, fields):
150     kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
151     validate = validation.Validate(username, state, strict=True, **kws)
152     return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
153                 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
154                 cdrom=getattr(validate, 'cdrom', None),
155                 autoinstall=getattr(validate, 'autoinstall', None))
156
157 def create(username, state, path, fields):
158     """Handler for create requests."""
159     try:
160         parsed_fields = parseCreate(username, state, fields)
161         machine = controls.createVm(username, state, **parsed_fields)
162     except InvalidInput, err:
163         pass
164     else:
165         err = None
166     state.clear() #Changed global state
167     d = getListDict(username, state)
168     d['err'] = err
169     if err:
170         for field in fields.keys():
171             setattr(d['defaults'], field, fields.getfirst(field))
172     else:
173         d['new_machine'] = parsed_fields['name']
174     return templates.list(searchList=[d])
175
176
177 def getListDict(username, state):
178     """Gets the list of local variables used by list.tmpl."""
179     checkpoint.checkpoint('Starting')
180     machines = state.machines
181     checkpoint.checkpoint('Got my machines')
182     on = {}
183     has_vnc = {}
184     xmlist = state.xmlist
185     checkpoint.checkpoint('Got uptimes')
186     can_clone = 'ice3' not in state.xmlist_raw
187     for m in machines:
188         if m not in xmlist:
189             has_vnc[m] = 'Off'
190             m.uptime = None
191         else:
192             m.uptime = xmlist[m]['uptime']
193             if xmlist[m]['console']:
194                 has_vnc[m] = True
195             elif m.type.hvm:
196                 has_vnc[m] = "WTF?"
197             else:
198                 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
199     max_memory = validation.maxMemory(username, state)
200     max_disk = validation.maxDisk(username)
201     checkpoint.checkpoint('Got max mem/disk')
202     defaults = Defaults(max_memory=max_memory,
203                         max_disk=max_disk,
204                         owner=username)
205     checkpoint.checkpoint('Got defaults')
206     def sortkey(machine):
207         return (machine.owner != username, machine.owner, machine.name)
208     machines = sorted(machines, key=sortkey)
209     d = dict(user=username,
210              cant_add_vm=validation.cantAddVm(username, state),
211              max_memory=max_memory,
212              max_disk=max_disk,
213              defaults=defaults,
214              machines=machines,
215              has_vnc=has_vnc,
216              can_clone=can_clone)
217     return d
218
219 def listVms(username, state, path, fields):
220     """Handler for list requests."""
221     checkpoint.checkpoint('Getting list dict')
222     d = getListDict(username, state)
223     checkpoint.checkpoint('Got list dict')
224     return templates.list(searchList=[d])
225
226 def vnc(username, state, path, fields):
227     """VNC applet page.
228
229     Note that due to same-domain restrictions, the applet connects to
230     the webserver, which needs to forward those requests to the xen
231     server.  The Xen server runs another proxy that (1) authenticates
232     and (2) finds the correct port for the VM.
233
234     You might want iptables like:
235
236     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
237       --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
238     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
239       --dport 10003 -j SNAT --to-source 18.187.7.142
240     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
241       --dport 10003 -j ACCEPT
242
243     Remember to enable iptables!
244     echo 1 > /proc/sys/net/ipv4/ip_forward
245     """
246     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
247
248     token = controls.vnctoken(machine)
249     host = controls.listHost(machine)
250     if host:
251         port = 10003 + [h.hostname for h in config.hosts].index(host)
252     else:
253         port = 5900 # dummy
254
255     status = controls.statusInfo(machine)
256     has_vnc = hasVnc(status)
257
258     d = dict(user=username,
259              on=status,
260              has_vnc=has_vnc,
261              machine=machine,
262              hostname=state.environ.get('SERVER_NAME', 'localhost'),
263              port=port,
264              authtoken=token)
265     return templates.vnc(searchList=[d])
266
267 def getHostname(nic):
268     """Find the hostname associated with a NIC.
269
270     XXX this should be merged with the similar logic in DNS and DHCP.
271     """
272     if nic.hostname:
273         hostname = nic.hostname
274     elif nic.machine:
275         hostname = nic.machine.name
276     else:
277         return None
278     if '.' in hostname:
279         return hostname
280     else:
281         return hostname + '.' + config.dns.domains[0]
282
283 def getNicInfo(data_dict, machine):
284     """Helper function for info, get data on nics for a machine.
285
286     Modifies data_dict to include the relevant data, and returns a list
287     of (key, name) pairs to display "name: data_dict[key]" to the user.
288     """
289     data_dict['num_nics'] = len(machine.nics)
290     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
291                            ('nic%s_mac', 'NIC %s MAC Addr'),
292                            ('nic%s_ip', 'NIC %s IP'),
293                            ]
294     nic_fields = []
295     for i in range(len(machine.nics)):
296         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
297         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
298         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
299         data_dict['nic%s_ip' % i] = machine.nics[i].ip
300     if len(machine.nics) == 1:
301         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
302     return nic_fields
303
304 def getDiskInfo(data_dict, machine):
305     """Helper function for info, get data on disks for a machine.
306
307     Modifies data_dict to include the relevant data, and returns a list
308     of (key, name) pairs to display "name: data_dict[key]" to the user.
309     """
310     data_dict['num_disks'] = len(machine.disks)
311     disk_fields_template = [('%s_size', '%s size')]
312     disk_fields = []
313     for disk in machine.disks:
314         name = disk.guest_device_name
315         disk_fields.extend([(x % name, y % name) for x, y in
316                             disk_fields_template])
317         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
318     return disk_fields
319
320 def command(username, state, path, fields):
321     """Handler for running commands like boot and delete on a VM."""
322     back = fields.getfirst('back')
323     try:
324         d = controls.commandResult(username, state, fields)
325         if d['command'] == 'Delete VM':
326             back = 'list'
327     except InvalidInput, err:
328         if not back:
329             raise
330         print >> sys.stderr, err
331         result = err
332     else:
333         result = 'Success!'
334         if not back:
335             return templates.command(searchList=[d])
336     if back == 'list':
337         state.clear() #Changed global state
338         d = getListDict(username, state)
339         d['result'] = result
340         return templates.list(searchList=[d])
341     elif back == 'info':
342         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
343         return ({'Status': '303 See Other',
344                  'Location': 'info?machine_id=%d' % machine.machine_id},
345                 "You shouldn't see this message.")
346     else:
347         raise InvalidInput('back', back, 'Not a known back page.')
348
349 def modifyDict(username, state, fields):
350     """Modify a machine as specified by CGI arguments.
351
352     Return a list of local variables for modify.tmpl.
353     """
354     olddisk = {}
355     session.begin()
356     try:
357         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
358         validate = validation.Validate(username, state, **kws)
359         machine = validate.machine
360         oldname = machine.name
361
362         if hasattr(validate, 'memory'):
363             machine.memory = validate.memory
364
365         if hasattr(validate, 'vmtype'):
366             machine.type = validate.vmtype
367
368         if hasattr(validate, 'disksize'):
369             disksize = validate.disksize
370             disk = machine.disks[0]
371             if disk.size != disksize:
372                 olddisk[disk.guest_device_name] = disksize
373                 disk.size = disksize
374                 session.save_or_update(disk)
375
376         update_acl = False
377         if hasattr(validate, 'owner') and validate.owner != machine.owner:
378             machine.owner = validate.owner
379             update_acl = True
380         if hasattr(validate, 'name'):
381             machine.name = validate.name
382             for n in machine.nics:
383                 if n.hostname == oldname:
384                     n.hostname = validate.name
385         if hasattr(validate, 'description'):
386             machine.description = validate.description
387         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
388             machine.administrator = validate.admin
389             update_acl = True
390         if hasattr(validate, 'contact'):
391             machine.contact = validate.contact
392
393         session.save_or_update(machine)
394         if update_acl:
395             cache_acls.refreshMachine(machine)
396         session.commit()
397     except:
398         session.rollback()
399         raise
400     for diskname in olddisk:
401         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
402     if hasattr(validate, 'name'):
403         controls.renameMachine(machine, oldname, validate.name)
404     return dict(user=username,
405                 command="modify",
406                 machine=machine)
407
408 def modify(username, state, path, fields):
409     """Handler for modifying attributes of a machine."""
410     try:
411         modify_dict = modifyDict(username, state, fields)
412     except InvalidInput, err:
413         result = None
414         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
415     else:
416         machine = modify_dict['machine']
417         result = 'Success!'
418         err = None
419     info_dict = infoDict(username, state, machine)
420     info_dict['err'] = err
421     if err:
422         for field in fields.keys():
423             setattr(info_dict['defaults'], field, fields.getfirst(field))
424     info_dict['result'] = result
425     return templates.info(searchList=[info_dict])
426
427
428 def helpHandler(username, state, path, fields):
429     """Handler for help messages."""
430     simple = fields.getfirst('simple')
431     subjects = fields.getlist('subject')
432
433     help_mapping = {
434                     'Autoinstalls': """
435 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
436 ParaVM.  You can access the resulting system by logging into the <a
437 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
438 with your Kerberos tickets; there is no root password so sshd will
439 refuse login.</p>
440
441 <p>Under the covers, the autoinstaller uses our own patched version of
442 xen-create-image, which is a tool based on debootstrap.  If you log
443 into the serial console while the install is running, you can watch
444 it.
445 """,
446                     'ParaVM Console': """
447 ParaVM machines do not support local console access over VNC.  To
448 access the serial console of these machines, you can SSH with Kerberos
449 to %s, using the name of the machine as your
450 username.""" % config.console.hostname,
451                     'HVM/ParaVM': """
452 HVM machines use the virtualization features of the processor, while
453 ParaVM machines rely on a modified kernel to communicate directly with
454 the hypervisor.  HVMs support boot CDs of any operating system, and
455 the VNC console applet.  The three-minute autoinstaller produces
456 ParaVMs.  ParaVMs typically are more efficient, and always support the
457 <a href="help?subject=ParaVM+Console">console server</a>.</p>
458
459 <p>More details are <a
460 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
461 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
462 (which you can skip by using the autoinstaller to begin with.)</p>
463
464 <p>We recommend using a ParaVM when possible and an HVM when necessary.
465 """,
466                     'CPU Weight': """
467 Don't ask us!  We're as mystified as you are.""",
468                     'Owner': """
469 The owner field is used to determine <a
470 href="help?subject=Quotas">quotas</a>.  It must be the name of a
471 locker that you are an AFS administrator of.  In particular, you or an
472 AFS group you are a member of must have AFS rlidwka bits on the
473 locker.  You can check who administers the LOCKER locker using the
474 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
475 href="help?subject=Administrator">administrator</a>.""",
476                     'Administrator': """
477 The administrator field determines who can access the console and
478 power on and off the machine.  This can be either a user or a moira
479 group.""",
480                     'Quotas': """
481 Quotas are determined on a per-locker basis.  Each locker may have a
482 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
483 active machines.""",
484                     'Console': """
485 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
486 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
487 your machine will run just fine, but the applet's display of the
488 console will suffer artifacts.
489 """,
490                     'Windows': """
491 <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>
492 <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.
493 """
494                     }
495
496     if not subjects:
497         subjects = sorted(help_mapping.keys())
498
499     d = dict(user=username,
500              simple=simple,
501              subjects=subjects,
502              mapping=help_mapping)
503
504     return templates.help(searchList=[d])
505
506
507 def badOperation(u, s, p, e):
508     """Function called when accessing an unknown URI."""
509     return ({'Status': '404 Not Found'}, 'Invalid operation.')
510
511 def infoDict(username, state, machine):
512     """Get the variables used by info.tmpl."""
513     status = controls.statusInfo(machine)
514     checkpoint.checkpoint('Getting status info')
515     has_vnc = hasVnc(status)
516     if status is None:
517         main_status = dict(name=machine.name,
518                            memory=str(machine.memory))
519         uptime = None
520         cputime = None
521     else:
522         main_status = dict(status[1:])
523         main_status['host'] = controls.listHost(machine)
524         start_time = float(main_status.get('start_time', 0))
525         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
526         cpu_time_float = float(main_status.get('cpu_time', 0))
527         cputime = datetime.timedelta(seconds=int(cpu_time_float))
528     checkpoint.checkpoint('Status')
529     display_fields = """name uptime memory state cpu_weight on_reboot 
530      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
531     display_fields = [('name', 'Name'),
532                       ('description', 'Description'),
533                       ('owner', 'Owner'),
534                       ('administrator', 'Administrator'),
535                       ('contact', 'Contact'),
536                       ('type', 'Type'),
537                       'NIC_INFO',
538                       ('uptime', 'uptime'),
539                       ('cputime', 'CPU usage'),
540                       ('host', 'Hosted on'),
541                       ('memory', 'RAM'),
542                       'DISK_INFO',
543                       ('state', 'state (xen format)'),
544                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
545                       ('on_reboot', 'Action on VM reboot'),
546                       ('on_poweroff', 'Action on VM poweroff'),
547                       ('on_crash', 'Action on VM crash'),
548                       ('on_xend_start', 'Action on Xen start'),
549                       ('on_xend_stop', 'Action on Xen stop'),
550                       ('bootloader', 'Bootloader options'),
551                       ]
552     fields = []
553     machine_info = {}
554     machine_info['name'] = machine.name
555     machine_info['description'] = machine.description
556     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
557     machine_info['owner'] = machine.owner
558     machine_info['administrator'] = machine.administrator
559     machine_info['contact'] = machine.contact
560
561     nic_fields = getNicInfo(machine_info, machine)
562     nic_point = display_fields.index('NIC_INFO')
563     display_fields = (display_fields[:nic_point] + nic_fields +
564                       display_fields[nic_point+1:])
565
566     disk_fields = getDiskInfo(machine_info, machine)
567     disk_point = display_fields.index('DISK_INFO')
568     display_fields = (display_fields[:disk_point] + disk_fields +
569                       display_fields[disk_point+1:])
570
571     main_status['memory'] += ' MiB'
572     for field, disp in display_fields:
573         if field in ('uptime', 'cputime') and locals()[field] is not None:
574             fields.append((disp, locals()[field]))
575         elif field in machine_info:
576             fields.append((disp, machine_info[field]))
577         elif field in main_status:
578             fields.append((disp, main_status[field]))
579         else:
580             pass
581             #fields.append((disp, None))
582
583     checkpoint.checkpoint('Got fields')
584
585
586     max_mem = validation.maxMemory(machine.owner, state, machine, False)
587     checkpoint.checkpoint('Got mem')
588     max_disk = validation.maxDisk(machine.owner, machine)
589     defaults = Defaults()
590     for name in 'machine_id name description administrator owner memory contact'.split():
591         setattr(defaults, name, getattr(machine, name))
592     defaults.type = machine.type.type_id
593     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
594     checkpoint.checkpoint('Got defaults')
595     d = dict(user=username,
596              on=status is not None,
597              machine=machine,
598              defaults=defaults,
599              has_vnc=has_vnc,
600              uptime=str(uptime),
601              ram=machine.memory,
602              max_mem=max_mem,
603              max_disk=max_disk,
604              owner_help=helppopup("Owner"),
605              fields = fields)
606     return d
607
608 def info(username, state, path, fields):
609     """Handler for info on a single VM."""
610     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
611     d = infoDict(username, state, machine)
612     checkpoint.checkpoint('Got infodict')
613     return templates.info(searchList=[d])
614
615 def unauthFront(_, _2, _3, fields):
616     """Information for unauth'd users."""
617     return templates.unauth(searchList=[{'simple' : True}])
618
619 def admin(username, state, path, fields):
620     if path == '':
621         return ({'Status': '303 See Other',
622                  'Location': 'admin/'},
623                 "You shouldn't see this message.")
624     if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
625         raise InvalidInput('username', username,
626                            'Not in admin group %s.' % config.web.adminacl)
627     newstate = State(username, isadmin=True)
628     newstate.environ = state.environ
629     return handler(username, newstate, path, fields)
630
631 def throwError(_, __, ___, ____):
632     """Throw an error, to test the error-tracing mechanisms."""
633     raise RuntimeError("test of the emergency broadcast system")
634
635 mapping = dict(list=listVms,
636                vnc=vnc,
637                command=command,
638                modify=modify,
639                info=info,
640                create=create,
641                help=helpHandler,
642                unauth=unauthFront,
643                admin=admin,
644                overlord=admin,
645                errortest=throwError)
646
647 def printHeaders(headers):
648     """Print a dictionary as HTTP headers."""
649     for key, value in headers.iteritems():
650         print '%s: %s' % (key, value)
651     print
652
653 def send_error_mail(subject, body):
654     import subprocess
655
656     to = config.web.errormail
657     mail = """To: %s
658 From: root@%s
659 Subject: %s
660
661 %s
662 """ % (to, config.web.hostname, subject, body)
663     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
664                          stdin=subprocess.PIPE)
665     p.stdin.write(mail)
666     p.stdin.close()
667     p.wait()
668
669 def show_error(op, username, fields, err, emsg, traceback):
670     """Print an error page when an exception occurs"""
671     d = dict(op=op, user=username, fields=fields,
672              errorMessage=str(err), stderr=emsg, traceback=traceback)
673     details = templates.error_raw(searchList=[d])
674     exclude = config.web.errormail_exclude
675     if username not in exclude and '*' not in exclude:
676         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
677                         details)
678     d['details'] = details
679     return templates.error(searchList=[d])
680
681 def getUser(environ):
682     """Return the current user based on the SSL environment variables"""
683     user = environ.get('REMOTE_USER')
684     if user is None:
685         return
686     
687     if environ.get('AUTH_TYPE') == 'Negotiate':
688         # Convert the krb5 principal into a krb4 username
689         if not user.endswith('@%s' % config.kerberos.realm):
690             return
691         else:
692             return user.split('@')[0].replace('/', '.')
693     else:
694         return user
695
696 def handler(username, state, path, fields):
697     operation, path = pathSplit(path)
698     if not operation:
699         operation = 'list'
700     print 'Starting', operation
701     fun = mapping.get(operation, badOperation)
702     return fun(username, state, path, fields)
703
704 class App:
705     def __init__(self, environ, start_response):
706         self.environ = environ
707         self.start = start_response
708
709         self.username = getUser(environ)
710         self.state = State(self.username)
711         self.state.environ = environ
712
713         random.seed() #sigh
714
715     def __iter__(self):
716         start_time = time.time()
717         database.clear_cache()
718         sys.stderr = StringIO()
719         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
720         operation = self.environ.get('PATH_INFO', '')
721         if not operation:
722             self.start("301 Moved Permanently", [('Location', './')])
723             return
724         if self.username is None:
725             operation = 'unauth'
726
727         try:
728             checkpoint.checkpoint('Before')
729             output = handler(self.username, self.state, operation, fields)
730             checkpoint.checkpoint('After')
731
732             headers = dict(DEFAULT_HEADERS)
733             if isinstance(output, tuple):
734                 new_headers, output = output
735                 headers.update(new_headers)
736             e = revertStandardError()
737             if e:
738                 if hasattr(output, 'addError'):
739                     output.addError(e)
740                 else:
741                     # This only happens on redirects, so it'd be a pain to get
742                     # the message to the user.  Maybe in the response is useful.
743                     output = output + '\n\nstderr:\n' + e
744             output_string =  str(output)
745             checkpoint.checkpoint('output as a string')
746         except Exception, err:
747             if not fields.has_key('js'):
748                 if isinstance(err, InvalidInput):
749                     self.start('200 OK', [('Content-Type', 'text/html')])
750                     e = revertStandardError()
751                     yield str(invalidInput(operation, self.username, fields,
752                                            err, e))
753                     return
754             import traceback
755             self.start('500 Internal Server Error',
756                        [('Content-Type', 'text/html')])
757             e = revertStandardError()
758             s = show_error(operation, self.username, fields,
759                            err, e, traceback.format_exc())
760             yield str(s)
761             return
762         status = headers.setdefault('Status', '200 OK')
763         del headers['Status']
764         self.start(status, headers.items())
765         yield output_string
766         if fields.has_key('timedebug'):
767             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
768
769 def constructor():
770     connect()
771     return App
772
773 def main():
774     from flup.server.fcgi_fork import WSGIServer
775     WSGIServer(constructor()).run()
776
777 if __name__ == '__main__':
778     main()