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