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