Don't confuse {KB, MB, GB} with {KiB, MiB, GiB}.
[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 mebibytes of active ram, 50 gibibytes 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 requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB 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', 'Name'),
530                       ('description', 'Description'),
531                       ('owner', 'Owner'),
532                       ('administrator', 'Administrator'),
533                       ('contact', 'Contact'),
534                       ('type', 'Type'),
535                       'NIC_INFO',
536                       ('uptime', 'uptime'),
537                       ('cputime', 'CPU usage'),
538                       ('host', 'Hosted on'),
539                       ('memory', 'RAM'),
540                       'DISK_INFO',
541                       ('state', 'state (xen format)'),
542                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
543                       ]
544     fields = []
545     machine_info = {}
546     machine_info['name'] = machine.name
547     machine_info['description'] = machine.description
548     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
549     machine_info['owner'] = machine.owner
550     machine_info['administrator'] = machine.administrator
551     machine_info['contact'] = machine.contact
552
553     nic_fields = getNicInfo(machine_info, machine)
554     nic_point = display_fields.index('NIC_INFO')
555     display_fields = (display_fields[:nic_point] + nic_fields +
556                       display_fields[nic_point+1:])
557
558     disk_fields = getDiskInfo(machine_info, machine)
559     disk_point = display_fields.index('DISK_INFO')
560     display_fields = (display_fields[:disk_point] + disk_fields +
561                       display_fields[disk_point+1:])
562
563     main_status['memory'] += ' MiB'
564     for field, disp in display_fields:
565         if field in ('uptime', 'cputime') and locals()[field] is not None:
566             fields.append((disp, locals()[field]))
567         elif field in machine_info:
568             fields.append((disp, machine_info[field]))
569         elif field in main_status:
570             fields.append((disp, main_status[field]))
571         else:
572             pass
573             #fields.append((disp, None))
574
575     checkpoint.checkpoint('Got fields')
576
577
578     max_mem = validation.maxMemory(machine.owner, state, machine, False)
579     checkpoint.checkpoint('Got mem')
580     max_disk = validation.maxDisk(machine.owner, machine)
581     defaults = Defaults()
582     for name in 'machine_id name description administrator owner memory contact'.split():
583         setattr(defaults, name, getattr(machine, name))
584     defaults.type = machine.type.type_id
585     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
586     checkpoint.checkpoint('Got defaults')
587     d = dict(user=username,
588              on=status is not None,
589              machine=machine,
590              defaults=defaults,
591              has_vnc=has_vnc,
592              uptime=str(uptime),
593              ram=machine.memory,
594              max_mem=max_mem,
595              max_disk=max_disk,
596              owner_help=helppopup("Owner"),
597              fields = fields)
598     return d
599
600 def info(username, state, path, fields):
601     """Handler for info on a single VM."""
602     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
603     d = infoDict(username, state, machine)
604     checkpoint.checkpoint('Got infodict')
605     return templates.info(searchList=[d])
606
607 def unauthFront(_, _2, _3, fields):
608     """Information for unauth'd users."""
609     return templates.unauth(searchList=[{'simple' : True}])
610
611 def admin(username, state, path, fields):
612     if path == '':
613         return ({'Status': '303 See Other',
614                  'Location': 'admin/'},
615                 "You shouldn't see this message.")
616     if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
617         raise InvalidInput('username', username,
618                            'Not in admin group %s.' % config.web.adminacl)
619     newstate = State(username, isadmin=True)
620     newstate.environ = state.environ
621     return handler(username, newstate, path, fields)
622
623 def throwError(_, __, ___, ____):
624     """Throw an error, to test the error-tracing mechanisms."""
625     raise RuntimeError("test of the emergency broadcast system")
626
627 mapping = dict(list=listVms,
628                vnc=vnc,
629                command=command,
630                modify=modify,
631                info=info,
632                create=create,
633                help=helpHandler,
634                unauth=unauthFront,
635                admin=admin,
636                overlord=admin,
637                errortest=throwError)
638
639 def printHeaders(headers):
640     """Print a dictionary as HTTP headers."""
641     for key, value in headers.iteritems():
642         print '%s: %s' % (key, value)
643     print
644
645 def send_error_mail(subject, body):
646     import subprocess
647
648     to = config.web.errormail
649     mail = """To: %s
650 From: root@%s
651 Subject: %s
652
653 %s
654 """ % (to, config.web.hostname, subject, body)
655     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
656                          stdin=subprocess.PIPE)
657     p.stdin.write(mail)
658     p.stdin.close()
659     p.wait()
660
661 def show_error(op, username, fields, err, emsg, traceback):
662     """Print an error page when an exception occurs"""
663     d = dict(op=op, user=username, fields=fields,
664              errorMessage=str(err), stderr=emsg, traceback=traceback)
665     details = templates.error_raw(searchList=[d])
666     exclude = config.web.errormail_exclude
667     if username not in exclude and '*' not in exclude:
668         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
669                         details)
670     d['details'] = details
671     return templates.error(searchList=[d])
672
673 def getUser(environ):
674     """Return the current user based on the SSL environment variables"""
675     user = environ.get('REMOTE_USER')
676     if user is None:
677         return
678     
679     if environ.get('AUTH_TYPE') == 'Negotiate':
680         # Convert the krb5 principal into a krb4 username
681         if not user.endswith('@%s' % config.kerberos.realm):
682             return
683         else:
684             return user.split('@')[0].replace('/', '.')
685     else:
686         return user
687
688 def handler(username, state, path, fields):
689     operation, path = pathSplit(path)
690     if not operation:
691         operation = 'list'
692     print 'Starting', operation
693     fun = mapping.get(operation, badOperation)
694     return fun(username, state, path, fields)
695
696 class App:
697     def __init__(self, environ, start_response):
698         self.environ = environ
699         self.start = start_response
700
701         self.username = getUser(environ)
702         self.state = State(self.username)
703         self.state.environ = environ
704
705         random.seed() #sigh
706
707     def __iter__(self):
708         start_time = time.time()
709         database.clear_cache()
710         sys.stderr = StringIO()
711         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
712         operation = self.environ.get('PATH_INFO', '')
713         if not operation:
714             self.start("301 Moved Permanently", [('Location', './')])
715             return
716         if self.username is None:
717             operation = 'unauth'
718
719         try:
720             checkpoint.checkpoint('Before')
721             output = handler(self.username, self.state, operation, fields)
722             checkpoint.checkpoint('After')
723
724             headers = dict(DEFAULT_HEADERS)
725             if isinstance(output, tuple):
726                 new_headers, output = output
727                 headers.update(new_headers)
728             e = revertStandardError()
729             if e:
730                 if hasattr(output, 'addError'):
731                     output.addError(e)
732                 else:
733                     # This only happens on redirects, so it'd be a pain to get
734                     # the message to the user.  Maybe in the response is useful.
735                     output = output + '\n\nstderr:\n' + e
736             output_string =  str(output)
737             checkpoint.checkpoint('output as a string')
738         except Exception, err:
739             if not fields.has_key('js'):
740                 if isinstance(err, InvalidInput):
741                     self.start('200 OK', [('Content-Type', 'text/html')])
742                     e = revertStandardError()
743                     yield str(invalidInput(operation, self.username, fields,
744                                            err, e))
745                     return
746             import traceback
747             self.start('500 Internal Server Error',
748                        [('Content-Type', 'text/html')])
749             e = revertStandardError()
750             s = show_error(operation, self.username, fields,
751                            err, e, traceback.format_exc())
752             yield str(s)
753             return
754         status = headers.setdefault('Status', '200 OK')
755         del headers['Status']
756         self.start(status, headers.items())
757         yield output_string
758         if fields.has_key('timedebug'):
759             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
760
761 def constructor():
762     connect()
763     return App
764
765 def main():
766     from flup.server.fcgi_fork import WSGIServer
767     WSGIServer(constructor()).run()
768
769 if __name__ == '__main__':
770     main()