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