7af4ecc559a335e31b30b682523fe602acac82c0
[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 sipb_xen_database
38 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
39 import validation
40 import cache_acls
41 from webcommon import InvalidInput, CodeError, State
42 import controls
43 from getafsgroups import getAfsGroupMembers
44 import invirt.config
45 invirt_config = invirt.config.load()
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.sipb_xen_database = sipb_xen_database
89 Template.helppopup = staticmethod(helppopup)
90 Template.err = None
91
92 class JsonDict:
93     """Class to store a dictionary that will be converted to JSON"""
94     def __init__(self, **kws):
95         self.data = kws
96         if 'err' in kws:
97             err = kws['err']
98             del kws['err']
99             self.addError(err)
100
101     def __str__(self):
102         return simplejson.dumps(self.data)
103
104     def addError(self, text):
105         """Add stderr text to be displayed on the website."""
106         self.data['err'] = \
107             makeErrorPre(self.data.get('err'), text)
108
109 class Defaults:
110     """Class to store default values for fields."""
111     memory = 256
112     disk = 4.0
113     cdrom = ''
114     autoinstall = ''
115     name = ''
116     description = ''
117     type = 'linux-hvm'
118
119     def __init__(self, max_memory=None, max_disk=None, **kws):
120         if max_memory is not None:
121             self.memory = min(self.memory, max_memory)
122         if max_disk is not None:
123             self.max_disk = min(self.disk, max_disk)
124         for key in kws:
125             setattr(self, key, kws[key])
126
127
128
129 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
130
131 def invalidInput(op, username, fields, err, emsg):
132     """Print an error page when an InvalidInput exception occurs"""
133     d = dict(op=op, user=username, err_field=err.err_field,
134              err_value=str(err.err_value), stderr=emsg,
135              errorMessage=str(err))
136     return templates.invalid(searchList=[d])
137
138 def hasVnc(status):
139     """Does the machine with a given status list support VNC?"""
140     if status is None:
141         return False
142     for l in status:
143         if l[0] == 'device' and l[1][0] == 'vfb':
144             d = dict(l[1][1:])
145             return 'location' in d
146     return False
147
148 def parseCreate(username, state, fields):
149     kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
150     validate = validation.Validate(username, state, strict=True, **kws)
151     return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
152                 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
153                 cdrom=getattr(validate, 'cdrom', None),
154                 autoinstall=getattr(validate, 'autoinstall', None))
155
156 def create(username, state, path, fields):
157     """Handler for create requests."""
158     try:
159         parsed_fields = parseCreate(username, state, fields)
160         machine = controls.createVm(username, state, **parsed_fields)
161     except InvalidInput, err:
162         pass
163     else:
164         err = None
165     state.clear() #Changed global state
166     d = getListDict(username, state)
167     d['err'] = err
168     if err:
169         for field in fields.keys():
170             setattr(d['defaults'], field, fields.getfirst(field))
171     else:
172         d['new_machine'] = parsed_fields['name']
173     return templates.list(searchList=[d])
174
175
176 def getListDict(username, state):
177     """Gets the list of local variables used by list.tmpl."""
178     checkpoint.checkpoint('Starting')
179     machines = state.machines
180     checkpoint.checkpoint('Got my machines')
181     on = {}
182     has_vnc = {}
183     xmlist = state.xmlist
184     checkpoint.checkpoint('Got uptimes')
185     can_clone = 'ice3' not in state.xmlist_raw
186     for m in machines:
187         if m not in xmlist:
188             has_vnc[m] = 'Off'
189             m.uptime = None
190         else:
191             m.uptime = xmlist[m]['uptime']
192             if xmlist[m]['console']:
193                 has_vnc[m] = True
194             elif m.type.hvm:
195                 has_vnc[m] = "WTF?"
196             else:
197                 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
198     max_memory = validation.maxMemory(username, state)
199     max_disk = validation.maxDisk(username)
200     checkpoint.checkpoint('Got max mem/disk')
201     defaults = Defaults(max_memory=max_memory,
202                         max_disk=max_disk,
203                         owner=username,
204                         cdrom='gutsy-i386')
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_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
249
250     data = {}
251     data["user"] = username
252     data["machine"] = machine.name
253     data["expires"] = time.time()+(5*60)
254     pickled_data = cPickle.dumps(data)
255     m = hmac.new(TOKEN_KEY, digestmod=sha)
256     m.update(pickled_data)
257     token = {'data': pickled_data, 'digest': m.digest()}
258     token = cPickle.dumps(token)
259     token = base64.urlsafe_b64encode(token)
260     host = controls.listHost(machine)
261     if host:
262         port = 10003 + [config_host["hostname"] for config_host in invirt_config["hosts"]
263             ].index(controls.listHost(machine))
264     else:
265         port = 5900 # dummy
266
267     status = controls.statusInfo(machine)
268     has_vnc = hasVnc(status)
269
270     d = dict(user=username,
271              on=status,
272              has_vnc=has_vnc,
273              machine=machine,
274              hostname=state.environ.get('SERVER_NAME', 'localhost'),
275              port=port,
276              authtoken=token)
277     return templates.vnc(searchList=[d])
278
279 def getHostname(nic):
280     """Find the hostname associated with a NIC.
281
282     XXX this should be merged with the similar logic in DNS and DHCP.
283     """
284     if nic.hostname and '.' in nic.hostname:
285         return nic.hostname
286     elif nic.machine:
287         return nic.machine.name + '.xvm.mit.edu'
288     else:
289         return None
290
291
292 def getNicInfo(data_dict, machine):
293     """Helper function for info, get data on nics for a machine.
294
295     Modifies data_dict to include the relevant data, and returns a list
296     of (key, name) pairs to display "name: data_dict[key]" to the user.
297     """
298     data_dict['num_nics'] = len(machine.nics)
299     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
300                            ('nic%s_mac', 'NIC %s MAC Addr'),
301                            ('nic%s_ip', 'NIC %s IP'),
302                            ]
303     nic_fields = []
304     for i in range(len(machine.nics)):
305         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
306         if not i:
307             data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
308         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
309         data_dict['nic%s_ip' % i] = machine.nics[i].ip
310     if len(machine.nics) == 1:
311         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
312     return nic_fields
313
314 def getDiskInfo(data_dict, machine):
315     """Helper function for info, get data on disks for a machine.
316
317     Modifies data_dict to include the relevant data, and returns a list
318     of (key, name) pairs to display "name: data_dict[key]" to the user.
319     """
320     data_dict['num_disks'] = len(machine.disks)
321     disk_fields_template = [('%s_size', '%s size')]
322     disk_fields = []
323     for disk in machine.disks:
324         name = disk.guest_device_name
325         disk_fields.extend([(x % name, y % name) for x, y in
326                             disk_fields_template])
327         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
328     return disk_fields
329
330 def command(username, state, path, fields):
331     """Handler for running commands like boot and delete on a VM."""
332     back = fields.getfirst('back')
333     try:
334         d = controls.commandResult(username, state, fields)
335         if d['command'] == 'Delete VM':
336             back = 'list'
337     except InvalidInput, err:
338         if not back:
339             raise
340         print >> sys.stderr, err
341         result = err
342     else:
343         result = 'Success!'
344         if not back:
345             return templates.command(searchList=[d])
346     if back == 'list':
347         state.clear() #Changed global state
348         d = getListDict(username, state)
349         d['result'] = result
350         return templates.list(searchList=[d])
351     elif back == 'info':
352         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
353         return ({'Status': '303 See Other',
354                  'Location': 'info?machine_id=%d' % machine.machine_id},
355                 "You shouldn't see this message.")
356     else:
357         raise InvalidInput('back', back, 'Not a known back page.')
358
359 def modifyDict(username, state, fields):
360     """Modify a machine as specified by CGI arguments.
361
362     Return a list of local variables for modify.tmpl.
363     """
364     olddisk = {}
365     transaction = ctx.current.create_transaction()
366     try:
367         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
368         validate = validation.Validate(username, state, **kws)
369         machine = validate.machine
370         oldname = machine.name
371
372         if hasattr(validate, 'memory'):
373             machine.memory = validate.memory
374
375         if hasattr(validate, 'vmtype'):
376             machine.type = validate.vmtype
377
378         if hasattr(validate, 'disksize'):
379             disksize = validate.disksize
380             disk = machine.disks[0]
381             if disk.size != disksize:
382                 olddisk[disk.guest_device_name] = disksize
383                 disk.size = disksize
384                 ctx.current.save(disk)
385
386         update_acl = False
387         if hasattr(validate, 'owner') and validate.owner != machine.owner:
388             machine.owner = validate.owner
389             update_acl = True
390         if hasattr(validate, 'name'):
391             machine.name = validate.name
392         if hasattr(validate, 'description'):
393             machine.description = validate.description
394         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
395             machine.administrator = validate.admin
396             update_acl = True
397         if hasattr(validate, 'contact'):
398             machine.contact = validate.contact
399
400         ctx.current.save(machine)
401         if update_acl:
402             print >> sys.stderr, machine, machine.administrator
403             cache_acls.refreshMachine(machine)
404         transaction.commit()
405     except:
406         transaction.rollback()
407         raise
408     for diskname in olddisk:
409         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
410     if hasattr(validate, 'name'):
411         controls.renameMachine(machine, oldname, validate.name)
412     return dict(user=username,
413                 command="modify",
414                 machine=machine)
415
416 def modify(username, state, path, fields):
417     """Handler for modifying attributes of a machine."""
418     try:
419         modify_dict = modifyDict(username, state, fields)
420     except InvalidInput, err:
421         result = None
422         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
423     else:
424         machine = modify_dict['machine']
425         result = 'Success!'
426         err = None
427     info_dict = infoDict(username, state, machine)
428     info_dict['err'] = err
429     if err:
430         for field in fields.keys():
431             setattr(info_dict['defaults'], field, fields.getfirst(field))
432     info_dict['result'] = result
433     return templates.info(searchList=[info_dict])
434
435
436 def helpHandler(username, state, path, fields):
437     """Handler for help messages."""
438     simple = fields.getfirst('simple')
439     subjects = fields.getlist('subject')
440
441     help_mapping = {'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 console.xvm.mit.edu, using the name of the machine as your
445 username.""",
446                     'HVM/ParaVM': """
447 HVM machines use the virtualization features of the processor, while
448 ParaVM machines use Xen's emulation of virtualization features.  You
449 want an HVM virtualized machine.""",
450                     'CPU Weight': """
451 Don't ask us!  We're as mystified as you are.""",
452                     'Owner': """
453 The owner field is used to determine <a
454 href="help?subject=Quotas">quotas</a>.  It must be the name of a
455 locker that you are an AFS administrator of.  In particular, you or an
456 AFS group you are a member of must have AFS rlidwka bits on the
457 locker.  You can check who administers the LOCKER locker using the
458 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
459 href="help?subject=Administrator">administrator</a>.""",
460                     'Administrator': """
461 The administrator field determines who can access the console and
462 power on and off the machine.  This can be either a user or a moira
463 group.""",
464                     'Quotas': """
465 Quotas are determined on a per-locker basis.  Each locker may have a
466 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
467 active machines.""",
468                     'Console': """
469 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
470 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
471 your machine will run just fine, but the applet's display of the
472 console will suffer artifacts.
473 """
474                     }
475
476     if not subjects:
477         subjects = sorted(help_mapping.keys())
478
479     d = dict(user=username,
480              simple=simple,
481              subjects=subjects,
482              mapping=help_mapping)
483
484     return templates.help(searchList=[d])
485
486
487 def badOperation(u, s, p, e):
488     """Function called when accessing an unknown URI."""
489     return ({'Status': '404 Not Found'}, 'Invalid operation.')
490
491 def infoDict(username, state, machine):
492     """Get the variables used by info.tmpl."""
493     status = controls.statusInfo(machine)
494     checkpoint.checkpoint('Getting status info')
495     has_vnc = hasVnc(status)
496     if status is None:
497         main_status = dict(name=machine.name,
498                            memory=str(machine.memory))
499         uptime = None
500         cputime = None
501     else:
502         main_status = dict(status[1:])
503         main_status['host'] = controls.listHost(machine)
504         start_time = float(main_status.get('start_time', 0))
505         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
506         cpu_time_float = float(main_status.get('cpu_time', 0))
507         cputime = datetime.timedelta(seconds=int(cpu_time_float))
508     checkpoint.checkpoint('Status')
509     display_fields = """name uptime memory state cpu_weight on_reboot 
510      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
511     display_fields = [('name', 'Name'),
512                       ('description', 'Description'),
513                       ('owner', 'Owner'),
514                       ('administrator', 'Administrator'),
515                       ('contact', 'Contact'),
516                       ('type', 'Type'),
517                       'NIC_INFO',
518                       ('uptime', 'uptime'),
519                       ('cputime', 'CPU usage'),
520                       ('host', 'Hosted on'),
521                       ('memory', 'RAM'),
522                       'DISK_INFO',
523                       ('state', 'state (xen format)'),
524                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
525                       ('on_reboot', 'Action on VM reboot'),
526                       ('on_poweroff', 'Action on VM poweroff'),
527                       ('on_crash', 'Action on VM crash'),
528                       ('on_xend_start', 'Action on Xen start'),
529                       ('on_xend_stop', 'Action on Xen stop'),
530                       ('bootloader', 'Bootloader options'),
531                       ]
532     fields = []
533     machine_info = {}
534     machine_info['name'] = machine.name
535     machine_info['description'] = machine.description
536     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
537     machine_info['owner'] = machine.owner
538     machine_info['administrator'] = machine.administrator
539     machine_info['contact'] = machine.contact
540
541     nic_fields = getNicInfo(machine_info, machine)
542     nic_point = display_fields.index('NIC_INFO')
543     display_fields = (display_fields[:nic_point] + nic_fields +
544                       display_fields[nic_point+1:])
545
546     disk_fields = getDiskInfo(machine_info, machine)
547     disk_point = display_fields.index('DISK_INFO')
548     display_fields = (display_fields[:disk_point] + disk_fields +
549                       display_fields[disk_point+1:])
550
551     main_status['memory'] += ' MiB'
552     for field, disp in display_fields:
553         if field in ('uptime', 'cputime') and locals()[field] is not None:
554             fields.append((disp, locals()[field]))
555         elif field in machine_info:
556             fields.append((disp, machine_info[field]))
557         elif field in main_status:
558             fields.append((disp, main_status[field]))
559         else:
560             pass
561             #fields.append((disp, None))
562
563     checkpoint.checkpoint('Got fields')
564
565
566     max_mem = validation.maxMemory(machine.owner, state, machine, False)
567     checkpoint.checkpoint('Got mem')
568     max_disk = validation.maxDisk(machine.owner, machine)
569     defaults = Defaults()
570     for name in 'machine_id name description administrator owner memory contact'.split():
571         setattr(defaults, name, getattr(machine, name))
572     defaults.type = machine.type.type_id
573     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
574     checkpoint.checkpoint('Got defaults')
575     d = dict(user=username,
576              on=status is not None,
577              machine=machine,
578              defaults=defaults,
579              has_vnc=has_vnc,
580              uptime=str(uptime),
581              ram=machine.memory,
582              max_mem=max_mem,
583              max_disk=max_disk,
584              owner_help=helppopup("Owner"),
585              fields = fields)
586     return d
587
588 def info(username, state, path, fields):
589     """Handler for info on a single VM."""
590     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
591     d = infoDict(username, state, machine)
592     checkpoint.checkpoint('Got infodict')
593     return templates.info(searchList=[d])
594
595 def unauthFront(_, _2, _3, fields):
596     """Information for unauth'd users."""
597     return templates.unauth(searchList=[{'simple' : True}])
598
599 def overlord(username, state, path, fields):
600     if path == '':
601         return ({'Status': '303 See Other',
602                  'Location': 'overlord/'},
603                 "You shouldn't see this message.")
604     if not username in getAfsGroupMembers('system:xvm', 'athena.mit.edu'):
605         raise InvalidInput('username', username, 'Not an overlord.')
606     newstate = State(username, overlord=True)
607     newstate.environ = state.environ
608     return handler(username, newstate, path, fields)
609
610 def throwError(_, __, ___, ____):
611     """Throw an error, to test the error-tracing mechanisms."""
612     raise RuntimeError("test of the emergency broadcast system")
613
614 mapping = dict(list=listVms,
615                vnc=vnc,
616                command=command,
617                modify=modify,
618                info=info,
619                create=create,
620                help=helpHandler,
621                unauth=unauthFront,
622                overlord=overlord,
623                errortest=throwError)
624
625 def printHeaders(headers):
626     """Print a dictionary as HTTP headers."""
627     for key, value in headers.iteritems():
628         print '%s: %s' % (key, value)
629     print
630
631 def send_error_mail(subject, body):
632     import subprocess
633
634     to = 'xvm@mit.edu'
635     mail = """To: %s
636 From: root@xvm.mit.edu
637 Subject: %s
638
639 %s
640 """ % (to, subject, body)
641     p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
642     p.stdin.write(mail)
643     p.stdin.close()
644     p.wait()
645
646 def show_error(op, username, fields, err, emsg, traceback):
647     """Print an error page when an exception occurs"""
648     d = dict(op=op, user=username, fields=fields,
649              errorMessage=str(err), stderr=emsg, traceback=traceback)
650     details = templates.error_raw(searchList=[d])
651     if username not in ('price', 'ecprice', 'andersk'): #add yourself at will
652         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
653                         details)
654     d['details'] = details
655     return templates.error(searchList=[d])
656
657 def getUser(environ):
658     """Return the current user based on the SSL environment variables"""
659     return environ.get('REMOTE_USER', None)
660
661 def handler(username, state, path, fields):
662     operation, path = pathSplit(path)
663     if not operation:
664         operation = 'list'
665     print 'Starting', operation
666     fun = mapping.get(operation, badOperation)
667     return fun(username, state, path, fields)
668
669 class App:
670     def __init__(self, environ, start_response):
671         self.environ = environ
672         self.start = start_response
673
674         self.username = getUser(environ)
675         self.state = State(self.username)
676         self.state.environ = environ
677
678         random.seed() #sigh
679
680     def __iter__(self):
681         start_time = time.time()
682         sipb_xen_database.clear_cache()
683         sys.stderr = StringIO()
684         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
685         operation = self.environ.get('PATH_INFO', '')
686         if not operation:
687             self.start("301 Moved Permanently", [('Location', './')])
688             return
689         if self.username is None:
690             operation = 'unauth'
691
692         try:
693             checkpoint.checkpoint('Before')
694             output = handler(self.username, self.state, operation, fields)
695             checkpoint.checkpoint('After')
696
697             headers = dict(DEFAULT_HEADERS)
698             if isinstance(output, tuple):
699                 new_headers, output = output
700                 headers.update(new_headers)
701             e = revertStandardError()
702             if e:
703                 if hasattr(output, 'addError'):
704                     output.addError(e)
705                 else:
706                     # This only happens on redirects, so it'd be a pain to get
707                     # the message to the user.  Maybe in the response is useful.
708                     output = output + '\n\nstderr:\n' + e
709             output_string =  str(output)
710             checkpoint.checkpoint('output as a string')
711         except Exception, err:
712             if not fields.has_key('js'):
713                 if isinstance(err, InvalidInput):
714                     self.start('200 OK', [('Content-Type', 'text/html')])
715                     e = revertStandardError()
716                     yield str(invalidInput(operation, self.username, fields,
717                                            err, e))
718                     return
719             import traceback
720             self.start('500 Internal Server Error',
721                        [('Content-Type', 'text/html')])
722             e = revertStandardError()
723             s = show_error(operation, self.username, fields,
724                            err, e, traceback.format_exc())
725             yield str(s)
726             return
727         status = headers.setdefault('Status', '200 OK')
728         del headers['Status']
729         self.start(status, headers.items())
730         yield output_string
731         if fields.has_key('timedebug'):
732             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
733
734 def constructor():
735     connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
736     return App
737
738 def main():
739     from flup.server.fcgi_fork import WSGIServer
740     WSGIServer(constructor()).run()
741
742 if __name__ == '__main__':
743     main()