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