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