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