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