Get tokens for a cell before getting a list membership from that cell.
[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.max_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 and '.' in nic.hostname:
273         return nic.hostname
274     elif nic.machine:
275         return nic.machine.name + '.' + config.dns.domains[0]
276     else:
277         return None
278
279
280 def getNicInfo(data_dict, machine):
281     """Helper function for info, get data on nics for a machine.
282
283     Modifies data_dict to include the relevant data, and returns a list
284     of (key, name) pairs to display "name: data_dict[key]" to the user.
285     """
286     data_dict['num_nics'] = len(machine.nics)
287     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
288                            ('nic%s_mac', 'NIC %s MAC Addr'),
289                            ('nic%s_ip', 'NIC %s IP'),
290                            ]
291     nic_fields = []
292     for i in range(len(machine.nics)):
293         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
294         if not i:
295             data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
296         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
297         data_dict['nic%s_ip' % i] = machine.nics[i].ip
298     if len(machine.nics) == 1:
299         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
300     return nic_fields
301
302 def getDiskInfo(data_dict, machine):
303     """Helper function for info, get data on disks for a machine.
304
305     Modifies data_dict to include the relevant data, and returns a list
306     of (key, name) pairs to display "name: data_dict[key]" to the user.
307     """
308     data_dict['num_disks'] = len(machine.disks)
309     disk_fields_template = [('%s_size', '%s size')]
310     disk_fields = []
311     for disk in machine.disks:
312         name = disk.guest_device_name
313         disk_fields.extend([(x % name, y % name) for x, y in
314                             disk_fields_template])
315         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
316     return disk_fields
317
318 def command(username, state, path, fields):
319     """Handler for running commands like boot and delete on a VM."""
320     back = fields.getfirst('back')
321     try:
322         d = controls.commandResult(username, state, fields)
323         if d['command'] == 'Delete VM':
324             back = 'list'
325     except InvalidInput, err:
326         if not back:
327             raise
328         print >> sys.stderr, err
329         result = err
330     else:
331         result = 'Success!'
332         if not back:
333             return templates.command(searchList=[d])
334     if back == 'list':
335         state.clear() #Changed global state
336         d = getListDict(username, state)
337         d['result'] = result
338         return templates.list(searchList=[d])
339     elif back == 'info':
340         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
341         return ({'Status': '303 See Other',
342                  'Location': 'info?machine_id=%d' % machine.machine_id},
343                 "You shouldn't see this message.")
344     else:
345         raise InvalidInput('back', back, 'Not a known back page.')
346
347 def modifyDict(username, state, fields):
348     """Modify a machine as specified by CGI arguments.
349
350     Return a list of local variables for modify.tmpl.
351     """
352     olddisk = {}
353     session.begin()
354     try:
355         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
356         validate = validation.Validate(username, state, **kws)
357         machine = validate.machine
358         oldname = machine.name
359
360         if hasattr(validate, 'memory'):
361             machine.memory = validate.memory
362
363         if hasattr(validate, 'vmtype'):
364             machine.type = validate.vmtype
365
366         if hasattr(validate, 'disksize'):
367             disksize = validate.disksize
368             disk = machine.disks[0]
369             if disk.size != disksize:
370                 olddisk[disk.guest_device_name] = disksize
371                 disk.size = disksize
372                 session.save_or_update(disk)
373
374         update_acl = False
375         if hasattr(validate, 'owner') and validate.owner != machine.owner:
376             machine.owner = validate.owner
377             update_acl = True
378         if hasattr(validate, 'name'):
379             machine.name = validate.name
380         if hasattr(validate, 'description'):
381             machine.description = validate.description
382         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
383             machine.administrator = validate.admin
384             update_acl = True
385         if hasattr(validate, 'contact'):
386             machine.contact = validate.contact
387
388         session.save_or_update(machine)
389         if update_acl:
390             cache_acls.refreshMachine(machine)
391         session.commit()
392     except:
393         session.rollback()
394         raise
395     for diskname in olddisk:
396         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
397     if hasattr(validate, 'name'):
398         controls.renameMachine(machine, oldname, validate.name)
399     return dict(user=username,
400                 command="modify",
401                 machine=machine)
402
403 def modify(username, state, path, fields):
404     """Handler for modifying attributes of a machine."""
405     try:
406         modify_dict = modifyDict(username, state, fields)
407     except InvalidInput, err:
408         result = None
409         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
410     else:
411         machine = modify_dict['machine']
412         result = 'Success!'
413         err = None
414     info_dict = infoDict(username, state, machine)
415     info_dict['err'] = err
416     if err:
417         for field in fields.keys():
418             setattr(info_dict['defaults'], field, fields.getfirst(field))
419     info_dict['result'] = result
420     return templates.info(searchList=[info_dict])
421
422
423 def helpHandler(username, state, path, fields):
424     """Handler for help messages."""
425     simple = fields.getfirst('simple')
426     subjects = fields.getlist('subject')
427
428     help_mapping = {
429                     'Autoinstalls': """
430 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
431 ParaVM.  You can access the resulting system by logging into the <a
432 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
433 with your Kerberos tickets; there is no root password so sshd will
434 refuse login.</p>
435
436 <p>Under the covers, the autoinstaller uses our own patched version of
437 xen-create-image, which is a tool based on debootstrap.  If you log
438 into the serial console while the install is running, you can watch
439 it.
440 """,
441                     'ParaVM Console': """
442 ParaVM machines do not support local console access over VNC.  To
443 access the serial console of these machines, you can SSH with Kerberos
444 to %s, using the name of the machine as your
445 username.""" % config.console.hostname,
446                     'HVM/ParaVM': """
447 HVM machines use the virtualization features of the processor, while
448 ParaVM machines rely on a modified kernel to communicate directly with
449 the hypervisor.  HVMs support boot CDs of any operating system, and
450 the VNC console applet.  The three-minute autoinstaller produces
451 ParaVMs.  ParaVMs typically are more efficient, and always support the
452 <a href="help?subject=ParaVM+Console">console server</a>.</p>
453
454 <p>More details are <a
455 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
456 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
457 (which you can skip by using the autoinstaller to begin with.)</p>
458
459 <p>We recommend using a ParaVM when possible and an HVM when necessary.
460 """,
461                     'CPU Weight': """
462 Don't ask us!  We're as mystified as you are.""",
463                     'Owner': """
464 The owner field is used to determine <a
465 href="help?subject=Quotas">quotas</a>.  It must be the name of a
466 locker that you are an AFS administrator of.  In particular, you or an
467 AFS group you are a member of must have AFS rlidwka bits on the
468 locker.  You can check who administers the LOCKER locker using the
469 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
470 href="help?subject=Administrator">administrator</a>.""",
471                     'Administrator': """
472 The administrator field determines who can access the console and
473 power on and off the machine.  This can be either a user or a moira
474 group.""",
475                     'Quotas': """
476 Quotas are determined on a per-locker basis.  Each locker may have a
477 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
478 active machines.""",
479                     'Console': """
480 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
481 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
482 your machine will run just fine, but the applet's display of the
483 console will suffer artifacts.
484 """,
485                     'Windows': """
486 <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>
487 <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.
488 """
489                     }
490
491     if not subjects:
492         subjects = sorted(help_mapping.keys())
493
494     d = dict(user=username,
495              simple=simple,
496              subjects=subjects,
497              mapping=help_mapping)
498
499     return templates.help(searchList=[d])
500
501
502 def badOperation(u, s, p, e):
503     """Function called when accessing an unknown URI."""
504     return ({'Status': '404 Not Found'}, 'Invalid operation.')
505
506 def infoDict(username, state, machine):
507     """Get the variables used by info.tmpl."""
508     status = controls.statusInfo(machine)
509     checkpoint.checkpoint('Getting status info')
510     has_vnc = hasVnc(status)
511     if status is None:
512         main_status = dict(name=machine.name,
513                            memory=str(machine.memory))
514         uptime = None
515         cputime = None
516     else:
517         main_status = dict(status[1:])
518         main_status['host'] = controls.listHost(machine)
519         start_time = float(main_status.get('start_time', 0))
520         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
521         cpu_time_float = float(main_status.get('cpu_time', 0))
522         cputime = datetime.timedelta(seconds=int(cpu_time_float))
523     checkpoint.checkpoint('Status')
524     display_fields = """name uptime memory state cpu_weight on_reboot 
525      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
526     display_fields = [('name', 'Name'),
527                       ('description', 'Description'),
528                       ('owner', 'Owner'),
529                       ('administrator', 'Administrator'),
530                       ('contact', 'Contact'),
531                       ('type', 'Type'),
532                       'NIC_INFO',
533                       ('uptime', 'uptime'),
534                       ('cputime', 'CPU usage'),
535                       ('host', 'Hosted on'),
536                       ('memory', 'RAM'),
537                       'DISK_INFO',
538                       ('state', 'state (xen format)'),
539                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
540                       ('on_reboot', 'Action on VM reboot'),
541                       ('on_poweroff', 'Action on VM poweroff'),
542                       ('on_crash', 'Action on VM crash'),
543                       ('on_xend_start', 'Action on Xen start'),
544                       ('on_xend_stop', 'Action on Xen stop'),
545                       ('bootloader', 'Bootloader options'),
546                       ]
547     fields = []
548     machine_info = {}
549     machine_info['name'] = machine.name
550     machine_info['description'] = machine.description
551     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
552     machine_info['owner'] = machine.owner
553     machine_info['administrator'] = machine.administrator
554     machine_info['contact'] = machine.contact
555
556     nic_fields = getNicInfo(machine_info, machine)
557     nic_point = display_fields.index('NIC_INFO')
558     display_fields = (display_fields[:nic_point] + nic_fields +
559                       display_fields[nic_point+1:])
560
561     disk_fields = getDiskInfo(machine_info, machine)
562     disk_point = display_fields.index('DISK_INFO')
563     display_fields = (display_fields[:disk_point] + disk_fields +
564                       display_fields[disk_point+1:])
565
566     main_status['memory'] += ' MiB'
567     for field, disp in display_fields:
568         if field in ('uptime', 'cputime') and locals()[field] is not None:
569             fields.append((disp, locals()[field]))
570         elif field in machine_info:
571             fields.append((disp, machine_info[field]))
572         elif field in main_status:
573             fields.append((disp, main_status[field]))
574         else:
575             pass
576             #fields.append((disp, None))
577
578     checkpoint.checkpoint('Got fields')
579
580
581     max_mem = validation.maxMemory(machine.owner, state, machine, False)
582     checkpoint.checkpoint('Got mem')
583     max_disk = validation.maxDisk(machine.owner, machine)
584     defaults = Defaults()
585     for name in 'machine_id name description administrator owner memory contact'.split():
586         setattr(defaults, name, getattr(machine, name))
587     defaults.type = machine.type.type_id
588     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
589     checkpoint.checkpoint('Got defaults')
590     d = dict(user=username,
591              on=status is not None,
592              machine=machine,
593              defaults=defaults,
594              has_vnc=has_vnc,
595              uptime=str(uptime),
596              ram=machine.memory,
597              max_mem=max_mem,
598              max_disk=max_disk,
599              owner_help=helppopup("Owner"),
600              fields = fields)
601     return d
602
603 def info(username, state, path, fields):
604     """Handler for info on a single VM."""
605     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
606     d = infoDict(username, state, machine)
607     checkpoint.checkpoint('Got infodict')
608     return templates.info(searchList=[d])
609
610 def unauthFront(_, _2, _3, fields):
611     """Information for unauth'd users."""
612     return templates.unauth(searchList=[{'simple' : True}])
613
614 def admin(username, state, path, fields):
615     if path == '':
616         return ({'Status': '303 See Other',
617                  'Location': 'admin/'},
618                 "You shouldn't see this message.")
619     if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
620         raise InvalidInput('username', username,
621                            'Not in admin group %s.' % config.web.adminacl)
622     newstate = State(username, isadmin=True)
623     newstate.environ = state.environ
624     return handler(username, newstate, path, fields)
625
626 def throwError(_, __, ___, ____):
627     """Throw an error, to test the error-tracing mechanisms."""
628     raise RuntimeError("test of the emergency broadcast system")
629
630 mapping = dict(list=listVms,
631                vnc=vnc,
632                command=command,
633                modify=modify,
634                info=info,
635                create=create,
636                help=helpHandler,
637                unauth=unauthFront,
638                admin=admin,
639                overlord=admin,
640                errortest=throwError)
641
642 def printHeaders(headers):
643     """Print a dictionary as HTTP headers."""
644     for key, value in headers.iteritems():
645         print '%s: %s' % (key, value)
646     print
647
648 def send_error_mail(subject, body):
649     import subprocess
650
651     to = config.web.errormail
652     mail = """To: %s
653 From: root@%s
654 Subject: %s
655
656 %s
657 """ % (to, config.web.hostname, subject, body)
658     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
659                          stdin=subprocess.PIPE)
660     p.stdin.write(mail)
661     p.stdin.close()
662     p.wait()
663
664 def show_error(op, username, fields, err, emsg, traceback):
665     """Print an error page when an exception occurs"""
666     d = dict(op=op, user=username, fields=fields,
667              errorMessage=str(err), stderr=emsg, traceback=traceback)
668     details = templates.error_raw(searchList=[d])
669     exclude = config.web.errormail_exclude
670     if username not in exclude and '*' not in exclude:
671         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
672                         details)
673     d['details'] = details
674     return templates.error(searchList=[d])
675
676 def getUser(environ):
677     """Return the current user based on the SSL environment variables"""
678     user = environ.get('REMOTE_USER')
679     if user is None:
680         return
681     
682     if environ.get('AUTH_TYPE') == 'Negotiate':
683         # Convert the krb5 principal into a krb4 username
684         if not user.endswith('@%s' % config.kerberos.realm):
685             return
686         else:
687             return user.split('@')[0].replace('/', '.')
688     else:
689         return user
690
691 def handler(username, state, path, fields):
692     operation, path = pathSplit(path)
693     if not operation:
694         operation = 'list'
695     print 'Starting', operation
696     fun = mapping.get(operation, badOperation)
697     return fun(username, state, path, fields)
698
699 class App:
700     def __init__(self, environ, start_response):
701         self.environ = environ
702         self.start = start_response
703
704         self.username = getUser(environ)
705         self.state = State(self.username)
706         self.state.environ = environ
707
708         random.seed() #sigh
709
710     def __iter__(self):
711         start_time = time.time()
712         database.clear_cache()
713         sys.stderr = StringIO()
714         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
715         operation = self.environ.get('PATH_INFO', '')
716         if not operation:
717             self.start("301 Moved Permanently", [('Location', './')])
718             return
719         if self.username is None:
720             operation = 'unauth'
721
722         try:
723             checkpoint.checkpoint('Before')
724             output = handler(self.username, self.state, operation, fields)
725             checkpoint.checkpoint('After')
726
727             headers = dict(DEFAULT_HEADERS)
728             if isinstance(output, tuple):
729                 new_headers, output = output
730                 headers.update(new_headers)
731             e = revertStandardError()
732             if e:
733                 if hasattr(output, 'addError'):
734                     output.addError(e)
735                 else:
736                     # This only happens on redirects, so it'd be a pain to get
737                     # the message to the user.  Maybe in the response is useful.
738                     output = output + '\n\nstderr:\n' + e
739             output_string =  str(output)
740             checkpoint.checkpoint('output as a string')
741         except Exception, err:
742             if not fields.has_key('js'):
743                 if isinstance(err, InvalidInput):
744                     self.start('200 OK', [('Content-Type', 'text/html')])
745                     e = revertStandardError()
746                     yield str(invalidInput(operation, self.username, fields,
747                                            err, e))
748                     return
749             import traceback
750             self.start('500 Internal Server Error',
751                        [('Content-Type', 'text/html')])
752             e = revertStandardError()
753             s = show_error(operation, self.username, fields,
754                            err, e, traceback.format_exc())
755             yield str(s)
756             return
757         status = headers.setdefault('Status', '200 OK')
758         del headers['Status']
759         self.start(status, headers.items())
760         yield output_string
761         if fields.has_key('timedebug'):
762             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
763
764 def constructor():
765     connect()
766     return App
767
768 def main():
769     from flup.server.fcgi_fork import WSGIServer
770     WSGIServer(constructor()).run()
771
772 if __name__ == '__main__':
773     main()