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