Clarify that staff can receive Windows license keys from MIT
[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()])
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     xmlist = state.xmlist
378     checkpoint.checkpoint('Got uptimes')
379     can_clone = 'ice3' not in state.xmlist_raw
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     max_memory = validation.maxMemory(username, state)
393     max_disk = validation.maxDisk(username)
394     checkpoint.checkpoint('Got max mem/disk')
395     defaults = Defaults(max_memory=max_memory,
396                         max_disk=max_disk,
397                         owner=username)
398     checkpoint.checkpoint('Got defaults')
399     def sortkey(machine):
400         return (machine.owner != username, machine.owner, machine.name)
401     machines = sorted(machines, key=sortkey)
402     d = dict(user=username,
403              cant_add_vm=validation.cantAddVm(username, state),
404              max_memory=max_memory,
405              max_disk=max_disk,
406              defaults=defaults,
407              machines=machines,
408              has_vnc=has_vnc,
409              can_clone=can_clone)
410     return d
411
412 def getHostname(nic):
413     """Find the hostname associated with a NIC.
414
415     XXX this should be merged with the similar logic in DNS and DHCP.
416     """
417     if nic.hostname:
418         hostname = nic.hostname
419     elif nic.machine:
420         hostname = nic.machine.name
421     else:
422         return None
423     if '.' in hostname:
424         return hostname
425     else:
426         return hostname + '.' + config.dns.domains[0]
427
428 def getNicInfo(data_dict, machine):
429     """Helper function for info, get data on nics for a machine.
430
431     Modifies data_dict to include the relevant data, and returns a list
432     of (key, name) pairs to display "name: data_dict[key]" to the user.
433     """
434     data_dict['num_nics'] = len(machine.nics)
435     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
436                            ('nic%s_mac', 'NIC %s MAC Addr'),
437                            ('nic%s_ip', 'NIC %s IP'),
438                            ]
439     nic_fields = []
440     for i in range(len(machine.nics)):
441         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
442         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
443         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
444         data_dict['nic%s_ip' % i] = machine.nics[i].ip
445     if len(machine.nics) == 1:
446         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
447     return nic_fields
448
449 def getDiskInfo(data_dict, machine):
450     """Helper function for info, get data on disks for a machine.
451
452     Modifies data_dict to include the relevant data, and returns a list
453     of (key, name) pairs to display "name: data_dict[key]" to the user.
454     """
455     data_dict['num_disks'] = len(machine.disks)
456     disk_fields_template = [('%s_size', '%s size')]
457     disk_fields = []
458     for disk in machine.disks:
459         name = disk.guest_device_name
460         disk_fields.extend([(x % name, y % name) for x, y in
461                             disk_fields_template])
462         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
463     return disk_fields
464
465 def modifyDict(username, state, fields):
466     """Modify a machine as specified by CGI arguments.
467
468     Return a list of local variables for modify.tmpl.
469     """
470     olddisk = {}
471     session.begin()
472     try:
473         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
474         validate = validation.Validate(username, state, **kws)
475         machine = validate.machine
476         oldname = machine.name
477
478         if hasattr(validate, 'memory'):
479             machine.memory = validate.memory
480
481         if hasattr(validate, 'vmtype'):
482             machine.type = validate.vmtype
483
484         if hasattr(validate, 'disksize'):
485             disksize = validate.disksize
486             disk = machine.disks[0]
487             if disk.size != disksize:
488                 olddisk[disk.guest_device_name] = disksize
489                 disk.size = disksize
490                 session.save_or_update(disk)
491
492         update_acl = False
493         if hasattr(validate, 'owner') and validate.owner != machine.owner:
494             machine.owner = validate.owner
495             update_acl = True
496         if hasattr(validate, 'name'):
497             machine.name = validate.name
498             for n in machine.nics:
499                 if n.hostname == oldname:
500                     n.hostname = validate.name
501         if hasattr(validate, 'description'):
502             machine.description = validate.description
503         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
504             machine.administrator = validate.admin
505             update_acl = True
506         if hasattr(validate, 'contact'):
507             machine.contact = validate.contact
508
509         session.save_or_update(machine)
510         if update_acl:
511             cache_acls.refreshMachine(machine)
512         session.commit()
513     except:
514         session.rollback()
515         raise
516     for diskname in olddisk:
517         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
518     if hasattr(validate, 'name'):
519         controls.renameMachine(machine, oldname, validate.name)
520     return dict(user=username,
521                 command="modify",
522                 machine=machine)
523
524 def modify(username, state, path, fields):
525     """Handler for modifying attributes of a machine."""
526     try:
527         modify_dict = modifyDict(username, state, fields)
528     except InvalidInput, err:
529         result = None
530         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
531     else:
532         machine = modify_dict['machine']
533         result = 'Success!'
534         err = None
535     info_dict = infoDict(username, state, machine)
536     info_dict['err'] = err
537     if err:
538         for field in fields.keys():
539             setattr(info_dict['defaults'], field, fields.getfirst(field))
540     info_dict['result'] = result
541     return templates.info(searchList=[info_dict])
542
543 def badOperation(u, s, p, e):
544     """Function called when accessing an unknown URI."""
545     return ({'Status': '404 Not Found'}, 'Invalid operation.')
546
547 def infoDict(username, state, machine):
548     """Get the variables used by info.tmpl."""
549     status = controls.statusInfo(machine)
550     checkpoint.checkpoint('Getting status info')
551     has_vnc = hasVnc(status)
552     if status is None:
553         main_status = dict(name=machine.name,
554                            memory=str(machine.memory))
555         uptime = None
556         cputime = None
557     else:
558         main_status = dict(status[1:])
559         main_status['host'] = controls.listHost(machine)
560         start_time = float(main_status.get('start_time', 0))
561         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
562         cpu_time_float = float(main_status.get('cpu_time', 0))
563         cputime = datetime.timedelta(seconds=int(cpu_time_float))
564     checkpoint.checkpoint('Status')
565     display_fields = [('name', 'Name'),
566                       ('description', 'Description'),
567                       ('owner', 'Owner'),
568                       ('administrator', 'Administrator'),
569                       ('contact', 'Contact'),
570                       ('type', 'Type'),
571                       'NIC_INFO',
572                       ('uptime', 'uptime'),
573                       ('cputime', 'CPU usage'),
574                       ('host', 'Hosted on'),
575                       ('memory', 'RAM'),
576                       'DISK_INFO',
577                       ('state', 'state (xen format)'),
578                       ]
579     fields = []
580     machine_info = {}
581     machine_info['name'] = machine.name
582     machine_info['description'] = machine.description
583     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
584     machine_info['owner'] = machine.owner
585     machine_info['administrator'] = machine.administrator
586     machine_info['contact'] = machine.contact
587
588     nic_fields = getNicInfo(machine_info, machine)
589     nic_point = display_fields.index('NIC_INFO')
590     display_fields = (display_fields[:nic_point] + nic_fields +
591                       display_fields[nic_point+1:])
592
593     disk_fields = getDiskInfo(machine_info, machine)
594     disk_point = display_fields.index('DISK_INFO')
595     display_fields = (display_fields[:disk_point] + disk_fields +
596                       display_fields[disk_point+1:])
597
598     main_status['memory'] += ' MiB'
599     for field, disp in display_fields:
600         if field in ('uptime', 'cputime') and locals()[field] is not None:
601             fields.append((disp, locals()[field]))
602         elif field in machine_info:
603             fields.append((disp, machine_info[field]))
604         elif field in main_status:
605             fields.append((disp, main_status[field]))
606         else:
607             pass
608             #fields.append((disp, None))
609
610     checkpoint.checkpoint('Got fields')
611
612
613     max_mem = validation.maxMemory(machine.owner, state, machine, False)
614     checkpoint.checkpoint('Got mem')
615     max_disk = validation.maxDisk(machine.owner, machine)
616     defaults = Defaults()
617     for name in 'machine_id name description administrator owner memory contact'.split():
618         setattr(defaults, name, getattr(machine, name))
619     defaults.type = machine.type.type_id
620     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
621     checkpoint.checkpoint('Got defaults')
622     d = dict(user=username,
623              on=status is not None,
624              machine=machine,
625              defaults=defaults,
626              has_vnc=has_vnc,
627              uptime=str(uptime),
628              ram=machine.memory,
629              max_mem=max_mem,
630              max_disk=max_disk,
631              fields = fields)
632     return d
633
634 def unauthFront(_, _2, _3, fields):
635     """Information for unauth'd users."""
636     return templates.unauth(searchList=[{'simple' : True, 
637             'hostname' : socket.getfqdn()}])
638
639 def admin(username, state, path, fields):
640     if path == '':
641         return ({'Status': '303 See Other',
642                  'Location': 'admin/'},
643                 "You shouldn't see this message.")
644     if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
645         raise InvalidInput('username', username,
646                            'Not in admin group %s.' % config.adminacl)
647     newstate = State(username, isadmin=True)
648     newstate.environ = state.environ
649     return handler(username, newstate, path, fields)
650
651 def throwError(_, __, ___, ____):
652     """Throw an error, to test the error-tracing mechanisms."""
653     raise RuntimeError("test of the emergency broadcast system")
654
655 mapping = dict(
656                modify=modify,
657                unauth=unauthFront,
658                admin=admin,
659                overlord=admin,
660                errortest=throwError)
661
662 def printHeaders(headers):
663     """Print a dictionary as HTTP headers."""
664     for key, value in headers.iteritems():
665         print '%s: %s' % (key, value)
666     print
667
668 def send_error_mail(subject, body):
669     import subprocess
670
671     to = config.web.errormail
672     mail = """To: %s
673 From: root@%s
674 Subject: %s
675
676 %s
677 """ % (to, config.web.hostname, subject, body)
678     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
679                          stdin=subprocess.PIPE)
680     p.stdin.write(mail)
681     p.stdin.close()
682     p.wait()
683
684 def show_error(op, username, fields, err, emsg, traceback):
685     """Print an error page when an exception occurs"""
686     d = dict(op=op, user=username, fields=fields,
687              errorMessage=str(err), stderr=emsg, traceback=traceback)
688     details = templates.error_raw(searchList=[d])
689     exclude = config.web.errormail_exclude
690     if username not in exclude and '*' not in exclude:
691         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
692                         details)
693     d['details'] = details
694     return templates.error(searchList=[d])
695
696 def handler(username, state, path, fields):
697     operation, path = pathSplit(path)
698     if not operation:
699         operation = 'list'
700     print 'Starting', operation
701     fun = mapping.get(operation, badOperation)
702     return fun(username, state, path, fields)
703
704 class App:
705     def __init__(self, environ, start_response):
706         self.environ = environ
707         self.start = start_response
708
709         self.username = getUser(environ)
710         self.state = State(self.username)
711         self.state.environ = environ
712
713         random.seed() #sigh
714
715     def __iter__(self):
716         start_time = time.time()
717         database.clear_cache()
718         sys.stderr = StringIO()
719         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
720         operation = self.environ.get('PATH_INFO', '')
721         if not operation:
722             self.start("301 Moved Permanently", [('Location', './')])
723             return
724         if self.username is None:
725             operation = 'unauth'
726
727         try:
728             checkpoint.checkpoint('Before')
729             output = handler(self.username, self.state, operation, fields)
730             checkpoint.checkpoint('After')
731
732             headers = dict(DEFAULT_HEADERS)
733             if isinstance(output, tuple):
734                 new_headers, output = output
735                 headers.update(new_headers)
736             e = revertStandardError()
737             if e:
738                 if hasattr(output, 'addError'):
739                     output.addError(e)
740                 else:
741                     # This only happens on redirects, so it'd be a pain to get
742                     # the message to the user.  Maybe in the response is useful.
743                     output = output + '\n\nstderr:\n' + e
744             output_string =  str(output)
745             checkpoint.checkpoint('output as a string')
746         except Exception, err:
747             if not fields.has_key('js'):
748                 if isinstance(err, InvalidInput):
749                     self.start('200 OK', [('Content-Type', 'text/html')])
750                     e = revertStandardError()
751                     yield str(invalidInput(operation, self.username, fields,
752                                            err, e))
753                     return
754             import traceback
755             self.start('500 Internal Server Error',
756                        [('Content-Type', 'text/html')])
757             e = revertStandardError()
758             s = show_error(operation, self.username, fields,
759                            err, e, traceback.format_exc())
760             yield str(s)
761             return
762         status = headers.setdefault('Status', '200 OK')
763         del headers['Status']
764         self.start(status, headers.items())
765         yield output_string
766         if fields.has_key('timedebug'):
767             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
768
769 def constructor():
770     connect()
771     return App
772
773 def main():
774     from flup.server.fcgi_fork import WSGIServer
775     WSGIServer(constructor()).run()
776
777 if __name__ == '__main__':
778     main()