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