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