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