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