473647f76cd8287efc822539e48626fe74402576
[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     @cherrypy.expose
186     def errortest(self):
187         """Throw an error, to test the error-tracing mechanisms."""
188         raise RuntimeError("test of the emergency broadcast system")
189
190     class MachineView(View):
191         # This is hairy. Fix when CherryPy 3.2 is out. (rename to
192         # _cp_dispatch, and parse the argument as a list instead of
193         # string
194
195         def __getattr__(self, name):
196             try:
197                 machine_id = int(name)
198                 cherrypy.request.params['machine_id'] = machine_id
199                 return self
200             except ValueError:
201                 return None
202
203         @cherrypy.expose
204         @cherrypy.tools.mako(filename="/info.mako")
205         def info(self, machine_id):
206             """Handler for info on a single VM."""
207             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
208             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
209             checkpoint.checkpoint('Got infodict')
210             return d
211         index = info
212
213         @cherrypy.expose
214         @cherrypy.tools.mako(filename="/vnc.mako")
215         def vnc(self, machine_id):
216             """VNC applet page.
217
218             Note that due to same-domain restrictions, the applet connects to
219             the webserver, which needs to forward those requests to the xen
220             server.  The Xen server runs another proxy that (1) authenticates
221             and (2) finds the correct port for the VM.
222
223             You might want iptables like:
224
225             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
226             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
227             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
228             --dport 10003 -j SNAT --to-source 18.187.7.142
229             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
230             --dport 10003 -j ACCEPT
231
232             Remember to enable iptables!
233             echo 1 > /proc/sys/net/ipv4/ip_forward
234             """
235             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
236
237             token = controls.vnctoken(machine)
238             host = controls.listHost(machine)
239             if host:
240                 port = 10003 + [h.hostname for h in config.hosts].index(host)
241             else:
242                 port = 5900 # dummy
243
244             status = controls.statusInfo(machine)
245             has_vnc = hasVnc(status)
246
247             d = dict(on=status,
248                      has_vnc=has_vnc,
249                      machine=machine,
250                      hostname=cherrypy.request.local.name,
251                      port=port,
252                      authtoken=token)
253             return d
254         @cherrypy.expose
255         @cherrypy.tools.mako(filename="/command.mako")
256         @cherrypy.tools.require_POST()
257         def command(self, command_name, machine_id, **kwargs):
258             """Handler for running commands like boot and delete on a VM."""
259             back = kwargs.get('back', None)
260             try:
261                 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
262                 if d['command'] == 'Delete VM':
263                     back = 'list'
264             except InvalidInput, err:
265                 if not back:
266                     raise
267                 print >> sys.stderr, err
268                 result = err
269             else:
270                 result = 'Success!'
271                 if not back:
272                     return d
273             if back == 'list':
274                 cherrypy.request.state.clear() #Changed global state
275                 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
276             elif back == 'info':
277                 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
278             else:
279                 raise InvalidInput('back', back, 'Not a known back page.')
280
281     machine = MachineView()
282
283 def pathSplit(path):
284     if path.startswith('/'):
285         path = path[1:]
286     i = path.find('/')
287     if i == -1:
288         i = len(path)
289     return path[:i], path[i:]
290
291 class Checkpoint:
292     def __init__(self):
293         self.start_time = time.time()
294         self.checkpoints = []
295
296     def checkpoint(self, s):
297         self.checkpoints.append((s, time.time()))
298
299     def __str__(self):
300         return ('Timing info:\n%s\n' %
301                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
302                            (d, t) in self.checkpoints]))
303
304 checkpoint = Checkpoint()
305
306 def makeErrorPre(old, addition):
307     if addition is None:
308         return
309     if old:
310         return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
311     else:
312         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
313
314 Template.database = database
315 Template.config = config
316 Template.err = None
317
318 class JsonDict:
319     """Class to store a dictionary that will be converted to JSON"""
320     def __init__(self, **kws):
321         self.data = kws
322         if 'err' in kws:
323             err = kws['err']
324             del kws['err']
325             self.addError(err)
326
327     def __str__(self):
328         return simplejson.dumps(self.data)
329
330     def addError(self, text):
331         """Add stderr text to be displayed on the website."""
332         self.data['err'] = \
333             makeErrorPre(self.data.get('err'), text)
334
335 class Defaults:
336     """Class to store default values for fields."""
337     memory = 256
338     disk = 4.0
339     cdrom = ''
340     autoinstall = ''
341     name = ''
342     description = ''
343     type = 'linux-hvm'
344
345     def __init__(self, max_memory=None, max_disk=None, **kws):
346         if max_memory is not None:
347             self.memory = min(self.memory, max_memory)
348         if max_disk is not None:
349             self.disk = min(self.disk, max_disk)
350         for key in kws:
351             setattr(self, key, kws[key])
352
353
354
355 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
356
357 def invalidInput(op, username, fields, err, emsg):
358     """Print an error page when an InvalidInput exception occurs"""
359     d = dict(op=op, user=username, err_field=err.err_field,
360              err_value=str(err.err_value), stderr=emsg,
361              errorMessage=str(err))
362     return templates.invalid(searchList=[d])
363
364 def hasVnc(status):
365     """Does the machine with a given status list support VNC?"""
366     if status is None:
367         return False
368     for l in status:
369         if l[0] == 'device' and l[1][0] == 'vfb':
370             d = dict(l[1][1:])
371             return 'location' in d
372     return False
373
374
375 def getListDict(username, state):
376     """Gets the list of local variables used by list.tmpl."""
377     checkpoint.checkpoint('Starting')
378     machines = state.machines
379     checkpoint.checkpoint('Got my machines')
380     on = {}
381     has_vnc = {}
382     installing = {}
383     xmlist = state.xmlist
384     checkpoint.checkpoint('Got uptimes')
385     for m in machines:
386         if m not in xmlist:
387             has_vnc[m] = 'Off'
388             m.uptime = None
389         else:
390             m.uptime = xmlist[m]['uptime']
391             if xmlist[m]['console']:
392                 has_vnc[m] = True
393             elif m.type.hvm:
394                 has_vnc[m] = "WTF?"
395             else:
396                 has_vnc[m] = "ParaVM"
397             if xmlist[m].get('autoinstall'):
398                 installing[m] = True
399             else:
400                 installing[m] = False
401     max_memory = validation.maxMemory(username, state)
402     max_disk = validation.maxDisk(username)
403     checkpoint.checkpoint('Got max mem/disk')
404     defaults = Defaults(max_memory=max_memory,
405                         max_disk=max_disk,
406                         owner=username)
407     checkpoint.checkpoint('Got defaults')
408     def sortkey(machine):
409         return (machine.owner != username, machine.owner, machine.name)
410     machines = sorted(machines, key=sortkey)
411     d = dict(user=username,
412              cant_add_vm=validation.cantAddVm(username, state),
413              max_memory=max_memory,
414              max_disk=max_disk,
415              defaults=defaults,
416              machines=machines,
417              has_vnc=has_vnc,
418              installing=installing)
419     return d
420
421 def getHostname(nic):
422     """Find the hostname associated with a NIC.
423
424     XXX this should be merged with the similar logic in DNS and DHCP.
425     """
426     if nic.hostname:
427         hostname = nic.hostname
428     elif nic.machine:
429         hostname = nic.machine.name
430     else:
431         return None
432     if '.' in hostname:
433         return hostname
434     else:
435         return hostname + '.' + config.dns.domains[0]
436
437 def getNicInfo(data_dict, machine):
438     """Helper function for info, get data on nics for a machine.
439
440     Modifies data_dict to include the relevant data, and returns a list
441     of (key, name) pairs to display "name: data_dict[key]" to the user.
442     """
443     data_dict['num_nics'] = len(machine.nics)
444     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
445                            ('nic%s_mac', 'NIC %s MAC Addr'),
446                            ('nic%s_ip', 'NIC %s IP'),
447                            ]
448     nic_fields = []
449     for i in range(len(machine.nics)):
450         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
451         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
452         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
453         data_dict['nic%s_ip' % i] = machine.nics[i].ip
454     if len(machine.nics) == 1:
455         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
456     return nic_fields
457
458 def getDiskInfo(data_dict, machine):
459     """Helper function for info, get data on disks for a machine.
460
461     Modifies data_dict to include the relevant data, and returns a list
462     of (key, name) pairs to display "name: data_dict[key]" to the user.
463     """
464     data_dict['num_disks'] = len(machine.disks)
465     disk_fields_template = [('%s_size', '%s size')]
466     disk_fields = []
467     for disk in machine.disks:
468         name = disk.guest_device_name
469         disk_fields.extend([(x % name, y % name) for x, y in
470                             disk_fields_template])
471         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
472     return disk_fields
473
474 def modifyDict(username, state, fields):
475     """Modify a machine as specified by CGI arguments.
476
477     Return a list of local variables for modify.tmpl.
478     """
479     olddisk = {}
480     session.begin()
481     try:
482         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
483         validate = validation.Validate(username, state, **kws)
484         machine = validate.machine
485         oldname = machine.name
486
487         if hasattr(validate, 'memory'):
488             machine.memory = validate.memory
489
490         if hasattr(validate, 'vmtype'):
491             machine.type = validate.vmtype
492
493         if hasattr(validate, 'disksize'):
494             disksize = validate.disksize
495             disk = machine.disks[0]
496             if disk.size != disksize:
497                 olddisk[disk.guest_device_name] = disksize
498                 disk.size = disksize
499                 session.save_or_update(disk)
500
501         update_acl = False
502         if hasattr(validate, 'owner') and validate.owner != machine.owner:
503             machine.owner = validate.owner
504             update_acl = True
505         if hasattr(validate, 'name'):
506             machine.name = validate.name
507             for n in machine.nics:
508                 if n.hostname == oldname:
509                     n.hostname = validate.name
510         if hasattr(validate, 'description'):
511             machine.description = validate.description
512         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
513             machine.administrator = validate.admin
514             update_acl = True
515         if hasattr(validate, 'contact'):
516             machine.contact = validate.contact
517
518         session.save_or_update(machine)
519         if update_acl:
520             cache_acls.refreshMachine(machine)
521         session.commit()
522     except:
523         session.rollback()
524         raise
525     for diskname in olddisk:
526         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
527     if hasattr(validate, 'name'):
528         controls.renameMachine(machine, oldname, validate.name)
529     return dict(user=username,
530                 command="modify",
531                 machine=machine)
532
533 def modify(username, state, path, fields):
534     """Handler for modifying attributes of a machine."""
535     try:
536         modify_dict = modifyDict(username, state, fields)
537     except InvalidInput, err:
538         result = None
539         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
540     else:
541         machine = modify_dict['machine']
542         result = 'Success!'
543         err = None
544     info_dict = infoDict(username, state, machine)
545     info_dict['err'] = err
546     if err:
547         for field in fields.keys():
548             setattr(info_dict['defaults'], field, fields.getfirst(field))
549     info_dict['result'] = result
550     return templates.info(searchList=[info_dict])
551
552 def badOperation(u, s, p, e):
553     """Function called when accessing an unknown URI."""
554     return ({'Status': '404 Not Found'}, 'Invalid operation.')
555
556 def infoDict(username, state, machine):
557     """Get the variables used by info.tmpl."""
558     status = controls.statusInfo(machine)
559     checkpoint.checkpoint('Getting status info')
560     has_vnc = hasVnc(status)
561     if status is None:
562         main_status = dict(name=machine.name,
563                            memory=str(machine.memory))
564         uptime = None
565         cputime = None
566     else:
567         main_status = dict(status[1:])
568         main_status['host'] = controls.listHost(machine)
569         start_time = float(main_status.get('start_time', 0))
570         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
571         cpu_time_float = float(main_status.get('cpu_time', 0))
572         cputime = datetime.timedelta(seconds=int(cpu_time_float))
573     checkpoint.checkpoint('Status')
574     display_fields = [('name', 'Name'),
575                       ('description', 'Description'),
576                       ('owner', 'Owner'),
577                       ('administrator', 'Administrator'),
578                       ('contact', 'Contact'),
579                       ('type', 'Type'),
580                       'NIC_INFO',
581                       ('uptime', 'uptime'),
582                       ('cputime', 'CPU usage'),
583                       ('host', 'Hosted on'),
584                       ('memory', 'RAM'),
585                       'DISK_INFO',
586                       ('state', 'state (xen format)'),
587                       ]
588     fields = []
589     machine_info = {}
590     machine_info['name'] = machine.name
591     machine_info['description'] = machine.description
592     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
593     machine_info['owner'] = machine.owner
594     machine_info['administrator'] = machine.administrator
595     machine_info['contact'] = machine.contact
596
597     nic_fields = getNicInfo(machine_info, machine)
598     nic_point = display_fields.index('NIC_INFO')
599     display_fields = (display_fields[:nic_point] + nic_fields +
600                       display_fields[nic_point+1:])
601
602     disk_fields = getDiskInfo(machine_info, machine)
603     disk_point = display_fields.index('DISK_INFO')
604     display_fields = (display_fields[:disk_point] + disk_fields +
605                       display_fields[disk_point+1:])
606
607     main_status['memory'] += ' MiB'
608     for field, disp in display_fields:
609         if field in ('uptime', 'cputime') and locals()[field] is not None:
610             fields.append((disp, locals()[field]))
611         elif field in machine_info:
612             fields.append((disp, machine_info[field]))
613         elif field in main_status:
614             fields.append((disp, main_status[field]))
615         else:
616             pass
617             #fields.append((disp, None))
618
619     checkpoint.checkpoint('Got fields')
620
621
622     max_mem = validation.maxMemory(machine.owner, state, machine, False)
623     checkpoint.checkpoint('Got mem')
624     max_disk = validation.maxDisk(machine.owner, machine)
625     defaults = Defaults()
626     for name in 'machine_id name description administrator owner memory contact'.split():
627         setattr(defaults, name, getattr(machine, name))
628     defaults.type = machine.type.type_id
629     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
630     checkpoint.checkpoint('Got defaults')
631     d = dict(user=username,
632              on=status is not None,
633              machine=machine,
634              defaults=defaults,
635              has_vnc=has_vnc,
636              uptime=str(uptime),
637              ram=machine.memory,
638              max_mem=max_mem,
639              max_disk=max_disk,
640              fields = fields)
641     return d
642
643 def unauthFront(_, _2, _3, fields):
644     """Information for unauth'd users."""
645     return templates.unauth(searchList=[{'simple' : True, 
646             'hostname' : socket.getfqdn()}])
647
648 def admin(username, state, path, fields):
649     if path == '':
650         return ({'Status': '303 See Other',
651                  'Location': 'admin/'},
652                 "You shouldn't see this message.")
653     if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
654         raise InvalidInput('username', username,
655                            'Not in admin group %s.' % config.adminacl)
656     newstate = State(username, isadmin=True)
657     newstate.environ = state.environ
658     return handler(username, newstate, path, fields)
659
660 mapping = dict(
661                modify=modify,
662                unauth=unauthFront,
663                admin=admin,
664                overlord=admin)
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()