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