Allow global imports to be specified for Mako templates
[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 import socket
16 import cherrypy
17 from StringIO import StringIO
18 def revertStandardError():
19     """Move stderr to stdout, and return the contents of the old stderr."""
20     errio = sys.stderr
21     if not isinstance(errio, StringIO):
22         return ''
23     sys.stderr = sys.stdout
24     errio.seek(0)
25     return errio.read()
26
27 def printError():
28     """Revert stderr to stdout, and print the contents of stderr"""
29     if isinstance(sys.stderr, StringIO):
30         print revertStandardError()
31
32 if __name__ == '__main__':
33     import atexit
34     atexit.register(printError)
35
36 import templates
37 from Cheetah.Template import Template
38 import validation
39 import cache_acls
40 from webcommon import State
41 import controls
42 from getafsgroups import getAfsGroupMembers
43 from invirt import database
44 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
45 from invirt.config import structs as config
46 from invirt.common import InvalidInput, CodeError
47
48 from view import View
49
50 class InvirtWeb(View):
51     def __init__(self):
52         super(self.__class__,self).__init__()
53         connect()
54         self._cp_config['tools.require_login.on'] = True
55
56     @cherrypy.expose
57     @cherrypy.tools.mako(filename="/list.mako")
58     def list(self):
59         """Handler for list requests."""
60         checkpoint.checkpoint('Getting list dict')
61         d = getListDict(cherrypy.request.login, cherrypy.request.state)
62         checkpoint.checkpoint('Got list dict')
63         return d
64     index=list
65
66     @cherrypy.expose
67     @cherrypy.tools.mako(filename="/helloworld.mako")
68     def helloworld(self):
69         return {}
70         return "Hello world!\nYour request: "+repr(dir(cherrypy.request))
71     helloworld._cp_config['tools.require_login.on'] = False
72
73 def pathSplit(path):
74     if path.startswith('/'):
75         path = path[1:]
76     i = path.find('/')
77     if i == -1:
78         i = len(path)
79     return path[:i], path[i:]
80
81 class Checkpoint:
82     def __init__(self):
83         self.start_time = time.time()
84         self.checkpoints = []
85
86     def checkpoint(self, s):
87         self.checkpoints.append((s, time.time()))
88
89     def __str__(self):
90         return ('Timing info:\n%s\n' %
91                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
92                            (d, t) in self.checkpoints]))
93
94 checkpoint = Checkpoint()
95
96 def makeErrorPre(old, addition):
97     if addition is None:
98         return
99     if old:
100         return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
101     else:
102         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
103
104 Template.database = database
105 Template.config = config
106 Template.err = None
107
108 class JsonDict:
109     """Class to store a dictionary that will be converted to JSON"""
110     def __init__(self, **kws):
111         self.data = kws
112         if 'err' in kws:
113             err = kws['err']
114             del kws['err']
115             self.addError(err)
116
117     def __str__(self):
118         return simplejson.dumps(self.data)
119
120     def addError(self, text):
121         """Add stderr text to be displayed on the website."""
122         self.data['err'] = \
123             makeErrorPre(self.data.get('err'), text)
124
125 class Defaults:
126     """Class to store default values for fields."""
127     memory = 256
128     disk = 4.0
129     cdrom = ''
130     autoinstall = ''
131     name = ''
132     description = ''
133     type = 'linux-hvm'
134
135     def __init__(self, max_memory=None, max_disk=None, **kws):
136         if max_memory is not None:
137             self.memory = min(self.memory, max_memory)
138         if max_disk is not None:
139             self.disk = min(self.disk, max_disk)
140         for key in kws:
141             setattr(self, key, kws[key])
142
143
144
145 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
146
147 def invalidInput(op, username, fields, err, emsg):
148     """Print an error page when an InvalidInput exception occurs"""
149     d = dict(op=op, user=username, err_field=err.err_field,
150              err_value=str(err.err_value), stderr=emsg,
151              errorMessage=str(err))
152     return templates.invalid(searchList=[d])
153
154 def hasVnc(status):
155     """Does the machine with a given status list support VNC?"""
156     if status is None:
157         return False
158     for l in status:
159         if l[0] == 'device' and l[1][0] == 'vfb':
160             d = dict(l[1][1:])
161             return 'location' in d
162     return False
163
164 def parseCreate(username, state, fields):
165     kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
166     validate = validation.Validate(username, state, strict=True, **kws)
167     return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
168                 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
169                 cdrom=getattr(validate, 'cdrom', None),
170                 autoinstall=getattr(validate, 'autoinstall', None))
171
172 def create(username, state, path, fields):
173     """Handler for create requests."""
174     try:
175         parsed_fields = parseCreate(username, state, fields)
176         machine = controls.createVm(username, state, **parsed_fields)
177     except InvalidInput, err:
178         pass
179     else:
180         err = None
181     state.clear() #Changed global state
182     d = getListDict(username, state)
183     d['err'] = err
184     if err:
185         for field in fields.keys():
186             setattr(d['defaults'], field, fields.getfirst(field))
187     else:
188         d['new_machine'] = parsed_fields['name']
189     return templates.list(searchList=[d])
190
191
192 def getListDict(username, state):
193     """Gets the list of local variables used by list.tmpl."""
194     checkpoint.checkpoint('Starting')
195     machines = state.machines
196     checkpoint.checkpoint('Got my machines')
197     on = {}
198     has_vnc = {}
199     xmlist = state.xmlist
200     checkpoint.checkpoint('Got uptimes')
201     can_clone = 'ice3' not in state.xmlist_raw
202     for m in machines:
203         if m not in xmlist:
204             has_vnc[m] = 'Off'
205             m.uptime = None
206         else:
207             m.uptime = xmlist[m]['uptime']
208             if xmlist[m]['console']:
209                 has_vnc[m] = True
210             elif m.type.hvm:
211                 has_vnc[m] = "WTF?"
212             else:
213                 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
214     max_memory = validation.maxMemory(username, state)
215     max_disk = validation.maxDisk(username)
216     checkpoint.checkpoint('Got max mem/disk')
217     defaults = Defaults(max_memory=max_memory,
218                         max_disk=max_disk,
219                         owner=username)
220     checkpoint.checkpoint('Got defaults')
221     def sortkey(machine):
222         return (machine.owner != username, machine.owner, machine.name)
223     machines = sorted(machines, key=sortkey)
224     d = dict(user=username,
225              cant_add_vm=validation.cantAddVm(username, state),
226              max_memory=max_memory,
227              max_disk=max_disk,
228              defaults=defaults,
229              machines=machines,
230              has_vnc=has_vnc,
231              can_clone=can_clone)
232     return d
233
234 def vnc(username, state, path, fields):
235     """VNC applet page.
236
237     Note that due to same-domain restrictions, the applet connects to
238     the webserver, which needs to forward those requests to the xen
239     server.  The Xen server runs another proxy that (1) authenticates
240     and (2) finds the correct port for the VM.
241
242     You might want iptables like:
243
244     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
245       --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
246     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
247       --dport 10003 -j SNAT --to-source 18.187.7.142
248     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
249       --dport 10003 -j ACCEPT
250
251     Remember to enable iptables!
252     echo 1 > /proc/sys/net/ipv4/ip_forward
253     """
254     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
255
256     token = controls.vnctoken(machine)
257     host = controls.listHost(machine)
258     if host:
259         port = 10003 + [h.hostname for h in config.hosts].index(host)
260     else:
261         port = 5900 # dummy
262
263     status = controls.statusInfo(machine)
264     has_vnc = hasVnc(status)
265
266     d = dict(user=username,
267              on=status,
268              has_vnc=has_vnc,
269              machine=machine,
270              hostname=state.environ.get('SERVER_NAME', 'localhost'),
271              port=port,
272              authtoken=token)
273     return templates.vnc(searchList=[d])
274
275 def getHostname(nic):
276     """Find the hostname associated with a NIC.
277
278     XXX this should be merged with the similar logic in DNS and DHCP.
279     """
280     if nic.hostname:
281         hostname = nic.hostname
282     elif nic.machine:
283         hostname = nic.machine.name
284     else:
285         return None
286     if '.' in hostname:
287         return hostname
288     else:
289         return hostname + '.' + config.dns.domains[0]
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         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     session.begin()
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                 session.save_or_update(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             for n in machine.nics:
391                 if n.hostname == oldname:
392                     n.hostname = validate.name
393         if hasattr(validate, 'description'):
394             machine.description = validate.description
395         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
396             machine.administrator = validate.admin
397             update_acl = True
398         if hasattr(validate, 'contact'):
399             machine.contact = validate.contact
400
401         session.save_or_update(machine)
402         if update_acl:
403             cache_acls.refreshMachine(machine)
404         session.commit()
405     except:
406         session.rollback()
407         raise
408     for diskname in olddisk:
409         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
410     if hasattr(validate, 'name'):
411         controls.renameMachine(machine, oldname, validate.name)
412     return dict(user=username,
413                 command="modify",
414                 machine=machine)
415
416 def modify(username, state, path, fields):
417     """Handler for modifying attributes of a machine."""
418     try:
419         modify_dict = modifyDict(username, state, fields)
420     except InvalidInput, err:
421         result = None
422         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
423     else:
424         machine = modify_dict['machine']
425         result = 'Success!'
426         err = None
427     info_dict = infoDict(username, state, machine)
428     info_dict['err'] = err
429     if err:
430         for field in fields.keys():
431             setattr(info_dict['defaults'], field, fields.getfirst(field))
432     info_dict['result'] = result
433     return templates.info(searchList=[info_dict])
434
435
436 def helpHandler(username, state, path, fields):
437     """Handler for help messages."""
438     simple = fields.getfirst('simple')
439     subjects = fields.getlist('subject')
440
441     help_mapping = {
442                     'Autoinstalls': """
443 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
444 ParaVM.  You can access the resulting system by logging into the <a
445 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
446 with your Kerberos tickets; there is no root password so sshd will
447 refuse login.</p>
448
449 <p>Under the covers, the autoinstaller uses our own patched version of
450 xen-create-image, which is a tool based on debootstrap.  If you log
451 into the serial console while the install is running, you can watch
452 it.
453 """,
454                     'ParaVM Console': """
455 ParaVM machines do not support local console access over VNC.  To
456 access the serial console of these machines, you can SSH with Kerberos
457 to %s, using the name of the machine as your
458 username.""" % config.console.hostname,
459                     'HVM/ParaVM': """
460 HVM machines use the virtualization features of the processor, while
461 ParaVM machines rely on a modified kernel to communicate directly with
462 the hypervisor.  HVMs support boot CDs of any operating system, and
463 the VNC console applet.  The three-minute autoinstaller produces
464 ParaVMs.  ParaVMs typically are more efficient, and always support the
465 <a href="help?subject=ParaVM+Console">console server</a>.</p>
466
467 <p>More details are <a
468 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
469 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
470 (which you can skip by using the autoinstaller to begin with.)</p>
471
472 <p>We recommend using a ParaVM when possible and an HVM when necessary.
473 """,
474                     'CPU Weight': """
475 Don't ask us!  We're as mystified as you are.""",
476                     'Owner': """
477 The owner field is used to determine <a
478 href="help?subject=Quotas">quotas</a>.  It must be the name of a
479 locker that you are an AFS administrator of.  In particular, you or an
480 AFS group you are a member of must have AFS rlidwka bits on the
481 locker.  You can check who administers the LOCKER locker using the
482 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
483 href="help?subject=Administrator">administrator</a>.""",
484                     'Administrator': """
485 The administrator field determines who can access the console and
486 power on and off the machine.  This can be either a user or a moira
487 group.""",
488                     'Quotas': """
489 Quotas are determined on a per-locker basis.  Each locker may have a
490 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
491 active machines.""",
492                     'Console': """
493 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
494 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
495 your machine will run just fine, but the applet's display of the
496 console will suffer artifacts.
497 """,
498                     'Windows': """
499 <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 requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).<br>
500 <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.
501 """
502                     }
503
504     if not subjects:
505         subjects = sorted(help_mapping.keys())
506
507     d = dict(user=username,
508              simple=simple,
509              subjects=subjects,
510              mapping=help_mapping)
511
512     return templates.help(searchList=[d])
513
514
515 def badOperation(u, s, p, e):
516     """Function called when accessing an unknown URI."""
517     return ({'Status': '404 Not Found'}, 'Invalid operation.')
518
519 def infoDict(username, state, machine):
520     """Get the variables used by info.tmpl."""
521     status = controls.statusInfo(machine)
522     checkpoint.checkpoint('Getting status info')
523     has_vnc = hasVnc(status)
524     if status is None:
525         main_status = dict(name=machine.name,
526                            memory=str(machine.memory))
527         uptime = None
528         cputime = None
529     else:
530         main_status = dict(status[1:])
531         main_status['host'] = controls.listHost(machine)
532         start_time = float(main_status.get('start_time', 0))
533         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
534         cpu_time_float = float(main_status.get('cpu_time', 0))
535         cputime = datetime.timedelta(seconds=int(cpu_time_float))
536     checkpoint.checkpoint('Status')
537     display_fields = [('name', 'Name'),
538                       ('description', 'Description'),
539                       ('owner', 'Owner'),
540                       ('administrator', 'Administrator'),
541                       ('contact', 'Contact'),
542                       ('type', 'Type'),
543                       'NIC_INFO',
544                       ('uptime', 'uptime'),
545                       ('cputime', 'CPU usage'),
546                       ('host', 'Hosted on'),
547                       ('memory', 'RAM'),
548                       'DISK_INFO',
549                       ('state', 'state (xen format)'),
550                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
551                       ]
552     fields = []
553     machine_info = {}
554     machine_info['name'] = machine.name
555     machine_info['description'] = machine.description
556     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
557     machine_info['owner'] = machine.owner
558     machine_info['administrator'] = machine.administrator
559     machine_info['contact'] = machine.contact
560
561     nic_fields = getNicInfo(machine_info, machine)
562     nic_point = display_fields.index('NIC_INFO')
563     display_fields = (display_fields[:nic_point] + nic_fields +
564                       display_fields[nic_point+1:])
565
566     disk_fields = getDiskInfo(machine_info, machine)
567     disk_point = display_fields.index('DISK_INFO')
568     display_fields = (display_fields[:disk_point] + disk_fields +
569                       display_fields[disk_point+1:])
570
571     main_status['memory'] += ' MiB'
572     for field, disp in display_fields:
573         if field in ('uptime', 'cputime') and locals()[field] is not None:
574             fields.append((disp, locals()[field]))
575         elif field in machine_info:
576             fields.append((disp, machine_info[field]))
577         elif field in main_status:
578             fields.append((disp, main_status[field]))
579         else:
580             pass
581             #fields.append((disp, None))
582
583     checkpoint.checkpoint('Got fields')
584
585
586     max_mem = validation.maxMemory(machine.owner, state, machine, False)
587     checkpoint.checkpoint('Got mem')
588     max_disk = validation.maxDisk(machine.owner, machine)
589     defaults = Defaults()
590     for name in 'machine_id name description administrator owner memory contact'.split():
591         setattr(defaults, name, getattr(machine, name))
592     defaults.type = machine.type.type_id
593     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
594     checkpoint.checkpoint('Got defaults')
595     d = dict(user=username,
596              on=status is not None,
597              machine=machine,
598              defaults=defaults,
599              has_vnc=has_vnc,
600              uptime=str(uptime),
601              ram=machine.memory,
602              max_mem=max_mem,
603              max_disk=max_disk,
604              owner_help=helppopup("Owner"),
605              fields = fields)
606     return d
607
608 def info(username, state, path, fields):
609     """Handler for info on a single VM."""
610     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
611     d = infoDict(username, state, machine)
612     checkpoint.checkpoint('Got infodict')
613     return templates.info(searchList=[d])
614
615 def unauthFront(_, _2, _3, fields):
616     """Information for unauth'd users."""
617     return templates.unauth(searchList=[{'simple' : True, 
618             'hostname' : socket.getfqdn()}])
619
620 def admin(username, state, path, fields):
621     if path == '':
622         return ({'Status': '303 See Other',
623                  'Location': 'admin/'},
624                 "You shouldn't see this message.")
625     if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
626         raise InvalidInput('username', username,
627                            'Not in admin group %s.' % config.adminacl)
628     newstate = State(username, isadmin=True)
629     newstate.environ = state.environ
630     return handler(username, newstate, path, fields)
631
632 def throwError(_, __, ___, ____):
633     """Throw an error, to test the error-tracing mechanisms."""
634     raise RuntimeError("test of the emergency broadcast system")
635
636 mapping = dict(#list=listVms,
637                vnc=vnc,
638                command=command,
639                modify=modify,
640                info=info,
641                create=create,
642                help=helpHandler,
643                unauth=unauthFront,
644                admin=admin,
645                overlord=admin,
646                errortest=throwError)
647
648 def printHeaders(headers):
649     """Print a dictionary as HTTP headers."""
650     for key, value in headers.iteritems():
651         print '%s: %s' % (key, value)
652     print
653
654 def send_error_mail(subject, body):
655     import subprocess
656
657     to = config.web.errormail
658     mail = """To: %s
659 From: root@%s
660 Subject: %s
661
662 %s
663 """ % (to, config.web.hostname, subject, body)
664     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
665                          stdin=subprocess.PIPE)
666     p.stdin.write(mail)
667     p.stdin.close()
668     p.wait()
669
670 def show_error(op, username, fields, err, emsg, traceback):
671     """Print an error page when an exception occurs"""
672     d = dict(op=op, user=username, fields=fields,
673              errorMessage=str(err), stderr=emsg, traceback=traceback)
674     details = templates.error_raw(searchList=[d])
675     exclude = config.web.errormail_exclude
676     if username not in exclude and '*' not in exclude:
677         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
678                         details)
679     d['details'] = details
680     return templates.error(searchList=[d])
681
682 def handler(username, state, path, fields):
683     operation, path = pathSplit(path)
684     if not operation:
685         operation = 'list'
686     print 'Starting', operation
687     fun = mapping.get(operation, badOperation)
688     return fun(username, state, path, fields)
689
690 class App:
691     def __init__(self, environ, start_response):
692         self.environ = environ
693         self.start = start_response
694
695         self.username = getUser(environ)
696         self.state = State(self.username)
697         self.state.environ = environ
698
699         random.seed() #sigh
700
701     def __iter__(self):
702         start_time = time.time()
703         database.clear_cache()
704         sys.stderr = StringIO()
705         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
706         operation = self.environ.get('PATH_INFO', '')
707         if not operation:
708             self.start("301 Moved Permanently", [('Location', './')])
709             return
710         if self.username is None:
711             operation = 'unauth'
712
713         try:
714             checkpoint.checkpoint('Before')
715             output = handler(self.username, self.state, operation, fields)
716             checkpoint.checkpoint('After')
717
718             headers = dict(DEFAULT_HEADERS)
719             if isinstance(output, tuple):
720                 new_headers, output = output
721                 headers.update(new_headers)
722             e = revertStandardError()
723             if e:
724                 if hasattr(output, 'addError'):
725                     output.addError(e)
726                 else:
727                     # This only happens on redirects, so it'd be a pain to get
728                     # the message to the user.  Maybe in the response is useful.
729                     output = output + '\n\nstderr:\n' + e
730             output_string =  str(output)
731             checkpoint.checkpoint('output as a string')
732         except Exception, err:
733             if not fields.has_key('js'):
734                 if isinstance(err, InvalidInput):
735                     self.start('200 OK', [('Content-Type', 'text/html')])
736                     e = revertStandardError()
737                     yield str(invalidInput(operation, self.username, fields,
738                                            err, e))
739                     return
740             import traceback
741             self.start('500 Internal Server Error',
742                        [('Content-Type', 'text/html')])
743             e = revertStandardError()
744             s = show_error(operation, self.username, fields,
745                            err, e, traceback.format_exc())
746             yield str(s)
747             return
748         status = headers.setdefault('Status', '200 OK')
749         del headers['Status']
750         self.start(status, headers.items())
751         yield output_string
752         if fields.has_key('timedebug'):
753             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
754
755 def constructor():
756     connect()
757     return App
758
759 def main():
760     from flup.server.fcgi_fork import WSGIServer
761     WSGIServer(constructor()).run()
762
763 if __name__ == '__main__':
764     main()