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