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