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