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