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