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