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