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