web: say where a VM is running
[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 sha
10 import simplejson
11 import sys
12 import time
13 import urllib
14 import random
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
45 def pathSplit(path):
46     if path.startswith('/'):
47         path = path[1:]
48     i = path.find('/')
49     if i == -1:
50         i = len(path)
51     return path[:i], path[i:]
52
53 class Checkpoint:
54     def __init__(self):
55         self.start_time = time.time()
56         self.checkpoints = []
57
58     def checkpoint(self, s):
59         self.checkpoints.append((s, time.time()))
60
61     def __str__(self):
62         return ('Timing info:\n%s\n' %
63                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
64                            (d, t) in self.checkpoints]))
65
66 checkpoint = Checkpoint()
67
68 def jquote(string):
69     return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
70
71 def helppopup(subj):
72     """Return HTML code for a (?) link to a specified help topic"""
73     return ('<span class="helplink"><a href="help?' +
74             cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
75             +'" target="_blank" ' +
76             'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
77
78 def makeErrorPre(old, addition):
79     if addition is None:
80         return
81     if old:
82         return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
83     else:
84         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
85
86 Template.sipb_xen_database = sipb_xen_database
87 Template.helppopup = staticmethod(helppopup)
88 Template.err = None
89
90 class JsonDict:
91     """Class to store a dictionary that will be converted to JSON"""
92     def __init__(self, **kws):
93         self.data = kws
94         if 'err' in kws:
95             err = kws['err']
96             del kws['err']
97             self.addError(err)
98
99     def __str__(self):
100         return simplejson.dumps(self.data)
101
102     def addError(self, text):
103         """Add stderr text to be displayed on the website."""
104         self.data['err'] = \
105             makeErrorPre(self.data.get('err'), text)
106
107 class Defaults:
108     """Class to store default values for fields."""
109     memory = 256
110     disk = 4.0
111     cdrom = ''
112     autoinstall = ''
113     name = ''
114     description = ''
115     type = 'linux-hvm'
116
117     def __init__(self, max_memory=None, max_disk=None, **kws):
118         if max_memory is not None:
119             self.memory = min(self.memory, max_memory)
120         if max_disk is not None:
121             self.max_disk = min(self.disk, max_disk)
122         for key in kws:
123             setattr(self, key, kws[key])
124
125
126
127 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
128
129 def invalidInput(op, username, fields, err, emsg):
130     """Print an error page when an InvalidInput exception occurs"""
131     d = dict(op=op, user=username, err_field=err.err_field,
132              err_value=str(err.err_value), stderr=emsg,
133              errorMessage=str(err))
134     return templates.invalid(searchList=[d])
135
136 def hasVnc(status):
137     """Does the machine with a given status list support VNC?"""
138     if status is None:
139         return False
140     for l in status:
141         if l[0] == 'device' and l[1][0] == 'vfb':
142             d = dict(l[1][1:])
143             return 'location' in d
144     return False
145
146 def parseCreate(username, state, fields):
147     kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
148     validate = validation.Validate(username, state, strict=True, **kws)
149     return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
150                 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
151                 cdrom=getattr(validate, 'cdrom', None),
152                 autoinstall=getattr(validate, 'autoinstall', None))
153
154 def create(username, state, path, fields):
155     """Handler for create requests."""
156     try:
157         parsed_fields = parseCreate(username, state, fields)
158         machine = controls.createVm(username, state, **parsed_fields)
159     except InvalidInput, err:
160         pass
161     else:
162         err = None
163     state.clear() #Changed global state
164     d = getListDict(username, state)
165     d['err'] = err
166     if err:
167         for field in fields.keys():
168             setattr(d['defaults'], field, fields.getfirst(field))
169     else:
170         d['new_machine'] = parsed_fields['name']
171     return templates.list(searchList=[d])
172
173
174 def getListDict(username, state):
175     """Gets the list of local variables used by list.tmpl."""
176     checkpoint.checkpoint('Starting')
177     machines = state.machines
178     checkpoint.checkpoint('Got my machines')
179     on = {}
180     has_vnc = {}
181     xmlist = state.xmlist
182     checkpoint.checkpoint('Got uptimes')
183     can_clone = 'ice3' not in state.xmlist_raw
184     for m in machines:
185         if m not in xmlist:
186             has_vnc[m] = 'Off'
187             m.uptime = None
188         else:
189             m.uptime = xmlist[m]['uptime']
190             if xmlist[m]['console']:
191                 has_vnc[m] = True
192             elif m.type.hvm:
193                 has_vnc[m] = "WTF?"
194             else:
195                 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
196     max_memory = validation.maxMemory(username, state)
197     max_disk = validation.maxDisk(username)
198     checkpoint.checkpoint('Got max mem/disk')
199     defaults = Defaults(max_memory=max_memory,
200                         max_disk=max_disk,
201                         owner=username,
202                         cdrom='gutsy-i386')
203     checkpoint.checkpoint('Got defaults')
204     def sortkey(machine):
205         return (machine.owner != username, machine.owner, machine.name)
206     machines = sorted(machines, key=sortkey)
207     d = dict(user=username,
208              cant_add_vm=validation.cantAddVm(username, state),
209              max_memory=max_memory,
210              max_disk=max_disk,
211              defaults=defaults,
212              machines=machines,
213              has_vnc=has_vnc,
214              can_clone=can_clone)
215     return d
216
217 def listVms(username, state, path, fields):
218     """Handler for list requests."""
219     checkpoint.checkpoint('Getting list dict')
220     d = getListDict(username, state)
221     checkpoint.checkpoint('Got list dict')
222     return templates.list(searchList=[d])
223
224 def vnc(username, state, path, fields):
225     """VNC applet page.
226
227     Note that due to same-domain restrictions, the applet connects to
228     the webserver, which needs to forward those requests to the xen
229     server.  The Xen server runs another proxy that (1) authenticates
230     and (2) finds the correct port for the VM.
231
232     You might want iptables like:
233
234     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
235       --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
236     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
237       --dport 10003 -j SNAT --to-source 18.187.7.142
238     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
239       --dport 10003 -j ACCEPT
240
241     Remember to enable iptables!
242     echo 1 > /proc/sys/net/ipv4/ip_forward
243     """
244     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
245
246     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
247
248     data = {}
249     data["user"] = username
250     data["machine"] = machine.name
251     data["expires"] = time.time()+(5*60)
252     pickled_data = cPickle.dumps(data)
253     m = hmac.new(TOKEN_KEY, digestmod=sha)
254     m.update(pickled_data)
255     token = {'data': pickled_data, 'digest': m.digest()}
256     token = cPickle.dumps(token)
257     token = base64.urlsafe_b64encode(token)
258
259     status = controls.statusInfo(machine)
260     has_vnc = hasVnc(status)
261
262     d = dict(user=username,
263              on=status,
264              has_vnc=has_vnc,
265              machine=machine,
266              hostname=state.environ.get('SERVER_NAME', 'localhost'),
267              authtoken=token)
268     return templates.vnc(searchList=[d])
269
270 def getHostname(nic):
271     """Find the hostname associated with a NIC.
272
273     XXX this should be merged with the similar logic in DNS and DHCP.
274     """
275     if nic.hostname and '.' in nic.hostname:
276         return nic.hostname
277     elif nic.machine:
278         return nic.machine.name + '.xvm.mit.edu'
279     else:
280         return None
281
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         if not i:
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     transaction = ctx.current.create_transaction()
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                 ctx.current.save(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         if hasattr(validate, 'description'):
384             machine.description = validate.description
385         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
386             machine.administrator = validate.admin
387             update_acl = True
388         if hasattr(validate, 'contact'):
389             machine.contact = validate.contact
390
391         ctx.current.save(machine)
392         if update_acl:
393             print >> sys.stderr, machine, machine.administrator
394             cache_acls.refreshMachine(machine)
395         transaction.commit()
396     except:
397         transaction.rollback()
398         raise
399     for diskname in olddisk:
400         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
401     if hasattr(validate, 'name'):
402         controls.renameMachine(machine, oldname, validate.name)
403     return dict(user=username,
404                 command="modify",
405                 machine=machine)
406
407 def modify(username, state, path, fields):
408     """Handler for modifying attributes of a machine."""
409     try:
410         modify_dict = modifyDict(username, state, fields)
411     except InvalidInput, err:
412         result = None
413         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
414     else:
415         machine = modify_dict['machine']
416         result = 'Success!'
417         err = None
418     info_dict = infoDict(username, state, machine)
419     info_dict['err'] = err
420     if err:
421         for field in fields.keys():
422             setattr(info_dict['defaults'], field, fields.getfirst(field))
423     info_dict['result'] = result
424     return templates.info(searchList=[info_dict])
425
426
427 def helpHandler(username, state, path, fields):
428     """Handler for help messages."""
429     simple = fields.getfirst('simple')
430     subjects = fields.getlist('subject')
431
432     help_mapping = {'ParaVM Console': """
433 ParaVM machines do not support local console access over VNC.  To
434 access the serial console of these machines, you can SSH with Kerberos
435 to console.xvm.mit.edu, using the name of the machine as your
436 username.""",
437                     'HVM/ParaVM': """
438 HVM machines use the virtualization features of the processor, while
439 ParaVM machines use Xen's emulation of virtualization features.  You
440 want an HVM virtualized machine.""",
441                     'CPU Weight': """
442 Don't ask us!  We're as mystified as you are.""",
443                     'Owner': """
444 The owner field is used to determine <a
445 href="help?subject=Quotas">quotas</a>.  It must be the name of a
446 locker that you are an AFS administrator of.  In particular, you or an
447 AFS group you are a member of must have AFS rlidwka bits on the
448 locker.  You can check who administers the LOCKER locker using the
449 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
450 href="help?subject=Administrator">administrator</a>.""",
451                     'Administrator': """
452 The administrator field determines who can access the console and
453 power on and off the machine.  This can be either a user or a moira
454 group.""",
455                     'Quotas': """
456 Quotas are determined on a per-locker basis.  Each locker may have a
457 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
458 active machines.""",
459                     'Console': """
460 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
461 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
462 your machine will run just fine, but the applet's display of the
463 console will suffer artifacts.
464 """
465                     }
466
467     if not subjects:
468         subjects = sorted(help_mapping.keys())
469
470     d = dict(user=username,
471              simple=simple,
472              subjects=subjects,
473              mapping=help_mapping)
474
475     return templates.help(searchList=[d])
476
477
478 def badOperation(u, s, p, e):
479     """Function called when accessing an unknown URI."""
480     return ({'Status': '404 Not Found'}, 'Invalid operation.')
481
482 def infoDict(username, state, machine):
483     """Get the variables used by info.tmpl."""
484     status = controls.statusInfo(machine)
485     checkpoint.checkpoint('Getting status info')
486     has_vnc = hasVnc(status)
487     if status is None:
488         main_status = dict(name=machine.name,
489                            memory=str(machine.memory))
490         uptime = None
491         cputime = None
492     else:
493         main_status = dict(status[1:])
494         main_status['host'] = controls.listHost(machine)
495         start_time = float(main_status.get('start_time', 0))
496         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
497         cpu_time_float = float(main_status.get('cpu_time', 0))
498         cputime = datetime.timedelta(seconds=int(cpu_time_float))
499     checkpoint.checkpoint('Status')
500     display_fields = """name uptime memory state cpu_weight on_reboot 
501      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
502     display_fields = [('name', 'Name'),
503                       ('description', 'Description'),
504                       ('owner', 'Owner'),
505                       ('administrator', 'Administrator'),
506                       ('contact', 'Contact'),
507                       ('type', 'Type'),
508                       'NIC_INFO',
509                       ('uptime', 'uptime'),
510                       ('cputime', 'CPU usage'),
511                       ('host', 'Hosted on'),
512                       ('memory', 'RAM'),
513                       'DISK_INFO',
514                       ('state', 'state (xen format)'),
515                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
516                       ('on_reboot', 'Action on VM reboot'),
517                       ('on_poweroff', 'Action on VM poweroff'),
518                       ('on_crash', 'Action on VM crash'),
519                       ('on_xend_start', 'Action on Xen start'),
520                       ('on_xend_stop', 'Action on Xen stop'),
521                       ('bootloader', 'Bootloader options'),
522                       ]
523     fields = []
524     machine_info = {}
525     machine_info['name'] = machine.name
526     machine_info['description'] = machine.description
527     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
528     machine_info['owner'] = machine.owner
529     machine_info['administrator'] = machine.administrator
530     machine_info['contact'] = machine.contact
531
532     nic_fields = getNicInfo(machine_info, machine)
533     nic_point = display_fields.index('NIC_INFO')
534     display_fields = (display_fields[:nic_point] + nic_fields +
535                       display_fields[nic_point+1:])
536
537     disk_fields = getDiskInfo(machine_info, machine)
538     disk_point = display_fields.index('DISK_INFO')
539     display_fields = (display_fields[:disk_point] + disk_fields +
540                       display_fields[disk_point+1:])
541
542     main_status['memory'] += ' MiB'
543     for field, disp in display_fields:
544         if field in ('uptime', 'cputime') and locals()[field] is not None:
545             fields.append((disp, locals()[field]))
546         elif field in machine_info:
547             fields.append((disp, machine_info[field]))
548         elif field in main_status:
549             fields.append((disp, main_status[field]))
550         else:
551             pass
552             #fields.append((disp, None))
553
554     checkpoint.checkpoint('Got fields')
555
556
557     max_mem = validation.maxMemory(machine.owner, state, machine, False)
558     checkpoint.checkpoint('Got mem')
559     max_disk = validation.maxDisk(machine.owner, machine)
560     defaults = Defaults()
561     for name in 'machine_id name description administrator owner memory contact'.split():
562         setattr(defaults, name, getattr(machine, name))
563     defaults.type = machine.type.type_id
564     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
565     checkpoint.checkpoint('Got defaults')
566     d = dict(user=username,
567              on=status is not None,
568              machine=machine,
569              defaults=defaults,
570              has_vnc=has_vnc,
571              uptime=str(uptime),
572              ram=machine.memory,
573              max_mem=max_mem,
574              max_disk=max_disk,
575              owner_help=helppopup("Owner"),
576              fields = fields)
577     return d
578
579 def info(username, state, path, fields):
580     """Handler for info on a single VM."""
581     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
582     d = infoDict(username, state, machine)
583     checkpoint.checkpoint('Got infodict')
584     return templates.info(searchList=[d])
585
586 def unauthFront(_, _2, _3, fields):
587     """Information for unauth'd users."""
588     return templates.unauth(searchList=[{'simple' : True}])
589
590 def overlord(username, state, path, fields):
591     if path == '':
592         return ({'Status': '303 See Other',
593                  'Location': 'overlord/'},
594                 "You shouldn't see this message.")
595     if not username in getAfsGroupMembers('system:xvm', 'athena.mit.edu'):
596         raise InvalidInput('username', username, 'Not an overlord.')
597     newstate = State(username, overlord=True)
598     newstate.environ = state.environ
599     return handler(username, newstate, path, fields)
600
601 def throwError(_, __, ___, ____):
602     """Throw an error, to test the error-tracing mechanisms."""
603     raise RuntimeError("test of the emergency broadcast system")
604
605 mapping = dict(list=listVms,
606                vnc=vnc,
607                command=command,
608                modify=modify,
609                info=info,
610                create=create,
611                help=helpHandler,
612                unauth=unauthFront,
613                overlord=overlord,
614                errortest=throwError)
615
616 def printHeaders(headers):
617     """Print a dictionary as HTTP headers."""
618     for key, value in headers.iteritems():
619         print '%s: %s' % (key, value)
620     print
621
622 def send_error_mail(subject, body):
623     import subprocess
624
625     to = 'xvm@mit.edu'
626     mail = """To: %s
627 From: root@xvm.mit.edu
628 Subject: %s
629
630 %s
631 """ % (to, subject, body)
632     p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
633     p.stdin.write(mail)
634     p.stdin.close()
635     p.wait()
636
637 def show_error(op, username, fields, err, emsg, traceback):
638     """Print an error page when an exception occurs"""
639     d = dict(op=op, user=username, fields=fields,
640              errorMessage=str(err), stderr=emsg, traceback=traceback)
641     details = templates.error_raw(searchList=[d])
642     if username not in ('price', 'ecprice', 'andersk'): #add yourself at will
643         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
644                         details)
645     d['details'] = details
646     return templates.error(searchList=[d])
647
648 def getUser(environ):
649     """Return the current user based on the SSL environment variables"""
650     return environ.get('REMOTE_USER', None)
651
652 def handler(username, state, path, fields):
653     operation, path = pathSplit(path)
654     if not operation:
655         operation = 'list'
656     print 'Starting', operation
657     fun = mapping.get(operation, badOperation)
658     return fun(username, state, path, fields)
659
660 class App:
661     def __init__(self, environ, start_response):
662         self.environ = environ
663         self.start = start_response
664
665         self.username = getUser(environ)
666         self.state = State(self.username)
667         self.state.environ = environ
668
669         random.seed() #sigh
670
671     def __iter__(self):
672         start_time = time.time()
673         sipb_xen_database.clear_cache()
674         sys.stderr = StringIO()
675         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
676         operation = self.environ.get('PATH_INFO', '')
677         if not operation:
678             self.start("301 Moved Permanently", [('Location', './')])
679             return
680         if self.username is None:
681             operation = 'unauth'
682
683         try:
684             checkpoint.checkpoint('Before')
685             output = handler(self.username, self.state, operation, fields)
686             checkpoint.checkpoint('After')
687
688             headers = dict(DEFAULT_HEADERS)
689             if isinstance(output, tuple):
690                 new_headers, output = output
691                 headers.update(new_headers)
692             e = revertStandardError()
693             if e:
694                 if isinstance(output, basestring):
695                     sys.stderr = StringIO()
696                     x = str(output)
697                     print >> sys.stderr, x
698                     print >> sys.stderr, 'XXX'
699                     print >> sys.stderr, e
700                     raise Exception()
701                 output.addError(e)
702             output_string =  str(output)
703             checkpoint.checkpoint('output as a string')
704         except Exception, err:
705             if not fields.has_key('js'):
706                 if isinstance(err, InvalidInput):
707                     self.start('200 OK', [('Content-Type', 'text/html')])
708                     e = revertStandardError()
709                     yield str(invalidInput(operation, self.username, fields,
710                                            err, e))
711                     return
712             import traceback
713             self.start('500 Internal Server Error',
714                        [('Content-Type', 'text/html')])
715             e = revertStandardError()
716             s = show_error(operation, self.username, fields,
717                            err, e, traceback.format_exc())
718             yield str(s)
719             return
720         status = headers.setdefault('Status', '200 OK')
721         del headers['Status']
722         self.start(status, headers.items())
723         yield output_string
724         if fields.has_key('timedebug'):
725             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
726
727 def constructor():
728     connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
729     return App
730
731 def main():
732     from flup.server.fcgi_fork import WSGIServer
733     WSGIServer(constructor()).run()
734
735 if __name__ == '__main__':
736     main()