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