all details in email too, still just for CodeError
[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 error(op, username, fields, err, emsg, traceback):
121     """Print an error page when a CodeError occurs"""
122     d = dict(op=op, user=username, fields=fields,
123              errorMessage=str(err), stderr=emsg, traceback=traceback)
124     details = templates.error_raw(searchList=[d])
125     send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
126                     details)
127     d['details'] = details
128     return templates.error(searchList=[d])
129
130 def invalidInput(op, username, fields, err, emsg):
131     """Print an error page when an InvalidInput exception occurs"""
132     d = dict(op=op, user=username, err_field=err.err_field,
133              err_value=str(err.err_value), stderr=emsg,
134              errorMessage=str(err))
135     return templates.invalid(searchList=[d])
136
137 def hasVnc(status):
138     """Does the machine with a given status list support VNC?"""
139     if status is None:
140         return False
141     for l in status:
142         if l[0] == 'device' and l[1][0] == 'vfb':
143             d = dict(l[1][1:])
144             return 'location' in d
145     return False
146
147 def parseCreate(username, state, fields):
148     kws = dict([(kw, fields.getfirst(kw)) for kw in 'name owner memory disksize vmtype cdrom clone_from'.split()])
149     validate = validation.Validate(username, state, strict=True, **kws)
150     return dict(contact=username, name=validate.name, memory=validate.memory,
151                 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
152                 cdrom=getattr(validate, 'cdrom', None),
153                 clone_from=getattr(validate, 'clone_from', None))
154
155 def create(username, state, fields):
156     """Handler for create requests."""
157     try:
158         parsed_fields = parseCreate(username, state, fields)
159         machine = controls.createVm(username, state, **parsed_fields)
160     except InvalidInput, err:
161         pass
162     else:
163         err = None
164     state.clear() #Changed global state
165     d = getListDict(username, state)
166     d['err'] = err
167     if err:
168         for field in fields.keys():
169             setattr(d['defaults'], field, fields.getfirst(field))
170     else:
171         d['new_machine'] = parsed_fields['name']
172     return templates.list(searchList=[d])
173
174
175 def getListDict(username, state):
176     """Gets the list of local variables used by list.tmpl."""
177     checkpoint.checkpoint('Starting')
178     machines = state.machines
179     checkpoint.checkpoint('Got my machines')
180     on = {}
181     has_vnc = {}
182     xmlist = state.xmlist
183     checkpoint.checkpoint('Got uptimes')
184     can_clone = 'ice3' not in state.xmlist_raw
185     for m in machines:
186         if m not in xmlist:
187             has_vnc[m] = 'Off'
188             m.uptime = None
189         else:
190             m.uptime = xmlist[m]['uptime']
191             if xmlist[m]['console']:
192                 has_vnc[m] = True
193             elif m.type.hvm:
194                 has_vnc[m] = "WTF?"
195             else:
196                 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
197     max_memory = validation.maxMemory(username, state)
198     max_disk = validation.maxDisk(username)
199     checkpoint.checkpoint('Got max mem/disk')
200     defaults = Defaults(max_memory=max_memory,
201                         max_disk=max_disk,
202                         owner=username,
203                         cdrom='gutsy-i386')
204     checkpoint.checkpoint('Got defaults')
205     def sortkey(machine):
206         return (machine.owner != username, machine.owner, machine.name)
207     machines = sorted(machines, key=sortkey)
208     d = dict(user=username,
209              cant_add_vm=validation.cantAddVm(username, state),
210              max_memory=max_memory,
211              max_disk=max_disk,
212              defaults=defaults,
213              machines=machines,
214              has_vnc=has_vnc,
215              can_clone=can_clone)
216     return d
217
218 def listVms(username, state, fields):
219     """Handler for list requests."""
220     checkpoint.checkpoint('Getting list dict')
221     d = getListDict(username, state)
222     checkpoint.checkpoint('Got list dict')
223     return templates.list(searchList=[d])
224
225 def vnc(username, state, fields):
226     """VNC applet page.
227
228     Note that due to same-domain restrictions, the applet connects to
229     the webserver, which needs to forward those requests to the xen
230     server.  The Xen server runs another proxy that (1) authenticates
231     and (2) finds the correct port for the VM.
232
233     You might want iptables like:
234
235     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
236       --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
237     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
238       --dport 10003 -j SNAT --to-source 18.187.7.142
239     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
240       --dport 10003 -j ACCEPT
241
242     Remember to enable iptables!
243     echo 1 > /proc/sys/net/ipv4/ip_forward
244     """
245     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
246
247     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
248
249     data = {}
250     data["user"] = username
251     data["machine"] = machine.name
252     data["expires"] = time.time()+(5*60)
253     pickled_data = cPickle.dumps(data)
254     m = hmac.new(TOKEN_KEY, digestmod=sha)
255     m.update(pickled_data)
256     token = {'data': pickled_data, 'digest': m.digest()}
257     token = cPickle.dumps(token)
258     token = base64.urlsafe_b64encode(token)
259
260     status = controls.statusInfo(machine)
261     has_vnc = hasVnc(status)
262
263     d = dict(user=username,
264              on=status,
265              has_vnc=has_vnc,
266              machine=machine,
267              hostname=state.environ.get('SERVER_NAME', 'localhost'),
268              authtoken=token)
269     return templates.vnc(searchList=[d])
270
271 def getHostname(nic):
272     """Find the hostname associated with a NIC.
273
274     XXX this should be merged with the similar logic in DNS and DHCP.
275     """
276     if nic.hostname and '.' in nic.hostname:
277         return nic.hostname
278     elif nic.machine:
279         return nic.machine.name + '.xvm.mit.edu'
280     else:
281         return None
282
283
284 def getNicInfo(data_dict, machine):
285     """Helper function for info, get data on nics for a machine.
286
287     Modifies data_dict to include the relevant data, and returns a list
288     of (key, name) pairs to display "name: data_dict[key]" to the user.
289     """
290     data_dict['num_nics'] = len(machine.nics)
291     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
292                            ('nic%s_mac', 'NIC %s MAC Addr'),
293                            ('nic%s_ip', 'NIC %s IP'),
294                            ]
295     nic_fields = []
296     for i in range(len(machine.nics)):
297         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
298         if not i:
299             data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
300         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
301         data_dict['nic%s_ip' % i] = machine.nics[i].ip
302     if len(machine.nics) == 1:
303         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
304     return nic_fields
305
306 def getDiskInfo(data_dict, machine):
307     """Helper function for info, get data on disks for a machine.
308
309     Modifies data_dict to include the relevant data, and returns a list
310     of (key, name) pairs to display "name: data_dict[key]" to the user.
311     """
312     data_dict['num_disks'] = len(machine.disks)
313     disk_fields_template = [('%s_size', '%s size')]
314     disk_fields = []
315     for disk in machine.disks:
316         name = disk.guest_device_name
317         disk_fields.extend([(x % name, y % name) for x, y in
318                             disk_fields_template])
319         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
320     return disk_fields
321
322 def command(username, state, fields):
323     """Handler for running commands like boot and delete on a VM."""
324     back = fields.getfirst('back')
325     try:
326         d = controls.commandResult(username, state, fields)
327         if d['command'] == 'Delete VM':
328             back = 'list'
329     except InvalidInput, err:
330         if not back:
331             raise
332         print >> sys.stderr, err
333         result = err
334     else:
335         result = 'Success!'
336         if not back:
337             return templates.command(searchList=[d])
338     if back == 'list':
339         state.clear() #Changed global state
340         d = getListDict(username, state)
341         d['result'] = result
342         return templates.list(searchList=[d])
343     elif back == 'info':
344         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
345         return ({'Status': '303 See Other',
346                  'Location': '/info?machine_id=%d' % machine.machine_id},
347                 "You shouldn't see this message.")
348     else:
349         raise InvalidInput('back', back, 'Not a known back page.')
350
351 def modifyDict(username, state, fields):
352     """Modify a machine as specified by CGI arguments.
353
354     Return a list of local variables for modify.tmpl.
355     """
356     olddisk = {}
357     transaction = ctx.current.create_transaction()
358     try:
359         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name memory vmtype disksize'.split()])
360         validate = validation.Validate(username, state, **kws)
361         machine = validate.machine
362         oldname = machine.name
363
364         if hasattr(validate, 'memory'):
365             machine.memory = validate.memory
366
367         if hasattr(validate, 'vmtype'):
368             machine.type = validate.vmtype
369
370         if hasattr(validate, 'disksize'):
371             disksize = validate.disksize
372             disk = machine.disks[0]
373             if disk.size != disksize:
374                 olddisk[disk.guest_device_name] = disksize
375                 disk.size = disksize
376                 ctx.current.save(disk)
377
378         update_acl = False
379         if hasattr(validate, 'owner') and validate.owner != machine.owner:
380             machine.owner = validate.owner
381             update_acl = True
382         if hasattr(validate, 'name'):
383             machine.name = validate.name
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, 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, 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, e):
478     """Function called when accessing an unknown URI."""
479     raise CodeError("Unknown 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                       ('owner', 'Owner'),
502                       ('administrator', 'Administrator'),
503                       ('contact', 'Contact'),
504                       ('type', 'Type'),
505                       'NIC_INFO',
506                       ('uptime', 'uptime'),
507                       ('cputime', 'CPU usage'),
508                       ('memory', 'RAM'),
509                       'DISK_INFO',
510                       ('state', 'state (xen format)'),
511                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
512                       ('on_reboot', 'Action on VM reboot'),
513                       ('on_poweroff', 'Action on VM poweroff'),
514                       ('on_crash', 'Action on VM crash'),
515                       ('on_xend_start', 'Action on Xen start'),
516                       ('on_xend_stop', 'Action on Xen stop'),
517                       ('bootloader', 'Bootloader options'),
518                       ]
519     fields = []
520     machine_info = {}
521     machine_info['name'] = machine.name
522     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
523     machine_info['owner'] = machine.owner
524     machine_info['administrator'] = machine.administrator
525     machine_info['contact'] = machine.contact
526
527     nic_fields = getNicInfo(machine_info, machine)
528     nic_point = display_fields.index('NIC_INFO')
529     display_fields = (display_fields[:nic_point] + nic_fields +
530                       display_fields[nic_point+1:])
531
532     disk_fields = getDiskInfo(machine_info, machine)
533     disk_point = display_fields.index('DISK_INFO')
534     display_fields = (display_fields[:disk_point] + disk_fields +
535                       display_fields[disk_point+1:])
536
537     main_status['memory'] += ' MiB'
538     for field, disp in display_fields:
539         if field in ('uptime', 'cputime') and locals()[field] is not None:
540             fields.append((disp, locals()[field]))
541         elif field in machine_info:
542             fields.append((disp, machine_info[field]))
543         elif field in main_status:
544             fields.append((disp, main_status[field]))
545         else:
546             pass
547             #fields.append((disp, None))
548
549     checkpoint.checkpoint('Got fields')
550
551
552     max_mem = validation.maxMemory(machine.owner, state, machine, False)
553     checkpoint.checkpoint('Got mem')
554     max_disk = validation.maxDisk(machine.owner, machine)
555     defaults = Defaults()
556     for name in 'machine_id name administrator owner memory contact'.split():
557         setattr(defaults, name, getattr(machine, name))
558     defaults.type = machine.type.type_id
559     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
560     checkpoint.checkpoint('Got defaults')
561     d = dict(user=username,
562              on=status is not None,
563              machine=machine,
564              defaults=defaults,
565              has_vnc=has_vnc,
566              uptime=str(uptime),
567              ram=machine.memory,
568              max_mem=max_mem,
569              max_disk=max_disk,
570              owner_help=helppopup("Owner"),
571              fields = fields)
572     return d
573
574 def info(username, state, fields):
575     """Handler for info on a single VM."""
576     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
577     d = infoDict(username, state, machine)
578     checkpoint.checkpoint('Got infodict')
579     return templates.info(searchList=[d])
580
581 def unauthFront(_, _2, fields):
582     """Information for unauth'd users."""
583     return templates.unauth(searchList=[{'simple' : True}])
584
585 def throwError(_, __, ___):
586     """Throw an error, to test the error-tracing mechanisms."""
587     raise CodeError("test of the emergency broadcast system")
588
589 mapping = dict(list=listVms,
590                vnc=vnc,
591                command=command,
592                modify=modify,
593                info=info,
594                create=create,
595                help=helpHandler,
596                unauth=unauthFront,
597                errortest=throwError)
598
599 def printHeaders(headers):
600     """Print a dictionary as HTTP headers."""
601     for key, value in headers.iteritems():
602         print '%s: %s' % (key, value)
603     print
604
605 def send_error_mail(subject, body):
606     import subprocess
607
608     to = 'xvm@mit.edu'
609     mail = """To: %s
610 From: root@xvm.mit.edu
611 Subject: %s
612
613 %s
614 """ % (to, subject, body)
615     p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
616     p.stdin.write(mail)
617     p.stdin.close()
618     p.wait()
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             print 'MOO2'
666             e = revertStandardError()
667             if e:
668                 if isinstance(output, basestring):
669                     sys.stderr = StringIO()
670                     x = str(output)
671                     print >> sys.stderr, x
672                     print >> sys.stderr, 'XXX'
673                     print >> sys.stderr, e
674                     raise Exception()
675                 output.addError(e)
676             output_string =  str(output)
677             checkpoint.checkpoint('output as a string')
678         except Exception, err:
679             import traceback
680             if not fields.has_key('js'):
681                 if isinstance(err, CodeError):
682                     self.start('500 Internal Server Error', [('Content-Type', 'text/html')])
683                     e = revertStandardError()
684                     s = error(operation, self.username, fields,
685                               err, e, traceback.format_exc())
686                     yield str(s)
687                     return
688                 if isinstance(err, InvalidInput):
689                     self.start('200 OK', [('Content-Type', 'text/html')])
690                     e = revertStandardError()
691                     yield str(invalidInput(operation, self.username, fields, err, e))
692                     return
693             self.start('500 Internal Server Error', [('Content-Type', 'text/plain')])
694             send_error_mail('xvm error: %s' % (err,),
695                             '%s\n' % (traceback.format_exc(),))
696             yield '''Uh-oh!  We experienced an error.
697 Sorry about that.  We've gotten mail about it.
698
699 Feel free to poke us at xvm@mit.edu if this bug is
700 consistently biting you and we don't seem to be fixing it.
701
702 In case you're curious, the gory details are here.
703 ----
704 %s
705 ----
706 %s
707 ----''' % (str(err), traceback.format_exc())
708         status = headers.setdefault('Status', '200 OK')
709         del headers['Status']
710         self.start(status, headers.items())
711         yield output_string
712         if fields.has_key('timedebug'):
713             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
714
715 def constructor():
716     connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
717     return App
718
719 def main():
720     from flup.server.fcgi_fork import WSGIServer
721     WSGIServer(constructor()).run()
722
723 if __name__ == '__main__':
724     main()