Make commands work from list and info pages
[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.
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     @cherrypy.expose
150     @cherrypy.tools.mako(filename="/helloworld.mako")
151     def helloworld(self, **kwargs):
152         return {'request': cherrypy.request, 'kwargs': kwargs}
153     helloworld._cp_config['tools.require_login.on'] = False
154
155     class MachineView(View):
156         # This is hairy. Fix when CherryPy 3.2 is out. (rename to
157         # _cp_dispatch, and parse the argument as a list instead of
158         # string
159
160         def __getattr__(self, name):
161             try:
162                 machine_id = int(name)
163                 cherrypy.request.params['machine_id'] = machine_id
164                 return self
165             except ValueError:
166                 return None
167
168         @cherrypy.expose
169         @cherrypy.tools.mako(filename="/info.mako")
170         def info(self, machine_id):
171             """Handler for info on a single VM."""
172             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
173             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
174             checkpoint.checkpoint('Got infodict')
175             return d
176         index = info
177
178         @cherrypy.expose
179         @cherrypy.tools.mako(filename="/vnc.mako")
180         def vnc(self, machine_id):
181             """VNC applet page.
182
183             Note that due to same-domain restrictions, the applet connects to
184             the webserver, which needs to forward those requests to the xen
185             server.  The Xen server runs another proxy that (1) authenticates
186             and (2) finds the correct port for the VM.
187
188             You might want iptables like:
189
190             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
191             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
192             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
193             --dport 10003 -j SNAT --to-source 18.187.7.142
194             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
195             --dport 10003 -j ACCEPT
196
197             Remember to enable iptables!
198             echo 1 > /proc/sys/net/ipv4/ip_forward
199             """
200             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
201
202             token = controls.vnctoken(machine)
203             host = controls.listHost(machine)
204             if host:
205                 port = 10003 + [h.hostname for h in config.hosts].index(host)
206             else:
207                 port = 5900 # dummy
208
209             status = controls.statusInfo(machine)
210             has_vnc = hasVnc(status)
211
212             d = dict(on=status,
213                      has_vnc=has_vnc,
214                      machine=machine,
215                      hostname=cherrypy.request.local.name,
216                      port=port,
217                      authtoken=token)
218             return d
219         @cherrypy.expose
220         @cherrypy.tools.mako(filename="/command.mako")
221         def command(self, command_name, machine_id, **kwargs):
222             """Handler for running commands like boot and delete on a VM."""
223             if cherrypy.request.method != "POST":
224                 raise InvalidInput("request.method", command_name, "You must execute commands via POST")
225             back = kwargs.get('back', None)
226             try:
227                 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
228                 if d['command'] == 'Delete VM':
229                     back = 'list'
230             except InvalidInput, err:
231                 if not back:
232                     raise
233                 print >> sys.stderr, err
234                 result = err
235             else:
236                 result = 'Success!'
237                 if not back:
238                     return d
239             if back == 'list':
240                 cherrypy.request.state.clear() #Changed global state
241                 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
242             elif back == 'info':
243                 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
244             else:
245                 raise InvalidInput('back', back, 'Not a known back page.')
246
247     machine = MachineView()
248
249 def pathSplit(path):
250     if path.startswith('/'):
251         path = path[1:]
252     i = path.find('/')
253     if i == -1:
254         i = len(path)
255     return path[:i], path[i:]
256
257 class Checkpoint:
258     def __init__(self):
259         self.start_time = time.time()
260         self.checkpoints = []
261
262     def checkpoint(self, s):
263         self.checkpoints.append((s, time.time()))
264
265     def __str__(self):
266         return ('Timing info:\n%s\n' %
267                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
268                            (d, t) in self.checkpoints]))
269
270 checkpoint = Checkpoint()
271
272 def makeErrorPre(old, addition):
273     if addition is None:
274         return
275     if old:
276         return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
277     else:
278         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
279
280 Template.database = database
281 Template.config = config
282 Template.err = None
283
284 class JsonDict:
285     """Class to store a dictionary that will be converted to JSON"""
286     def __init__(self, **kws):
287         self.data = kws
288         if 'err' in kws:
289             err = kws['err']
290             del kws['err']
291             self.addError(err)
292
293     def __str__(self):
294         return simplejson.dumps(self.data)
295
296     def addError(self, text):
297         """Add stderr text to be displayed on the website."""
298         self.data['err'] = \
299             makeErrorPre(self.data.get('err'), text)
300
301 class Defaults:
302     """Class to store default values for fields."""
303     memory = 256
304     disk = 4.0
305     cdrom = ''
306     autoinstall = ''
307     name = ''
308     description = ''
309     type = 'linux-hvm'
310
311     def __init__(self, max_memory=None, max_disk=None, **kws):
312         if max_memory is not None:
313             self.memory = min(self.memory, max_memory)
314         if max_disk is not None:
315             self.disk = min(self.disk, max_disk)
316         for key in kws:
317             setattr(self, key, kws[key])
318
319
320
321 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
322
323 def invalidInput(op, username, fields, err, emsg):
324     """Print an error page when an InvalidInput exception occurs"""
325     d = dict(op=op, user=username, err_field=err.err_field,
326              err_value=str(err.err_value), stderr=emsg,
327              errorMessage=str(err))
328     return templates.invalid(searchList=[d])
329
330 def hasVnc(status):
331     """Does the machine with a given status list support VNC?"""
332     if status is None:
333         return False
334     for l in status:
335         if l[0] == 'device' and l[1][0] == 'vfb':
336             d = dict(l[1][1:])
337             return 'location' in d
338     return False
339
340 def parseCreate(username, state, fields):
341     kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
342     validate = validation.Validate(username, state, strict=True, **kws)
343     return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
344                 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
345                 cdrom=getattr(validate, 'cdrom', None),
346                 autoinstall=getattr(validate, 'autoinstall', None))
347
348 def create(username, state, path, fields):
349     """Handler for create requests."""
350     try:
351         parsed_fields = parseCreate(username, state, fields)
352         machine = controls.createVm(username, state, **parsed_fields)
353     except InvalidInput, err:
354         pass
355     else:
356         err = None
357     state.clear() #Changed global state
358     d = getListDict(username, state)
359     d['err'] = err
360     if err:
361         for field in fields.keys():
362             setattr(d['defaults'], field, fields.getfirst(field))
363     else:
364         d['new_machine'] = parsed_fields['name']
365     return templates.list(searchList=[d])
366
367
368 def getListDict(username, state):
369     """Gets the list of local variables used by list.tmpl."""
370     checkpoint.checkpoint('Starting')
371     machines = state.machines
372     checkpoint.checkpoint('Got my machines')
373     on = {}
374     has_vnc = {}
375     xmlist = state.xmlist
376     checkpoint.checkpoint('Got uptimes')
377     can_clone = 'ice3' not in state.xmlist_raw
378     for m in machines:
379         if m not in xmlist:
380             has_vnc[m] = 'Off'
381             m.uptime = None
382         else:
383             m.uptime = xmlist[m]['uptime']
384             if xmlist[m]['console']:
385                 has_vnc[m] = True
386             elif m.type.hvm:
387                 has_vnc[m] = "WTF?"
388             else:
389                 has_vnc[m] = "ParaVM"
390     max_memory = validation.maxMemory(username, state)
391     max_disk = validation.maxDisk(username)
392     checkpoint.checkpoint('Got max mem/disk')
393     defaults = Defaults(max_memory=max_memory,
394                         max_disk=max_disk,
395                         owner=username)
396     checkpoint.checkpoint('Got defaults')
397     def sortkey(machine):
398         return (machine.owner != username, machine.owner, machine.name)
399     machines = sorted(machines, key=sortkey)
400     d = dict(user=username,
401              cant_add_vm=validation.cantAddVm(username, state),
402              max_memory=max_memory,
403              max_disk=max_disk,
404              defaults=defaults,
405              machines=machines,
406              has_vnc=has_vnc,
407              can_clone=can_clone)
408     return d
409
410 def getHostname(nic):
411     """Find the hostname associated with a NIC.
412
413     XXX this should be merged with the similar logic in DNS and DHCP.
414     """
415     if nic.hostname:
416         hostname = nic.hostname
417     elif nic.machine:
418         hostname = nic.machine.name
419     else:
420         return None
421     if '.' in hostname:
422         return hostname
423     else:
424         return hostname + '.' + config.dns.domains[0]
425
426 def getNicInfo(data_dict, machine):
427     """Helper function for info, get data on nics for a machine.
428
429     Modifies data_dict to include the relevant data, and returns a list
430     of (key, name) pairs to display "name: data_dict[key]" to the user.
431     """
432     data_dict['num_nics'] = len(machine.nics)
433     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
434                            ('nic%s_mac', 'NIC %s MAC Addr'),
435                            ('nic%s_ip', 'NIC %s IP'),
436                            ]
437     nic_fields = []
438     for i in range(len(machine.nics)):
439         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
440         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
441         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
442         data_dict['nic%s_ip' % i] = machine.nics[i].ip
443     if len(machine.nics) == 1:
444         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
445     return nic_fields
446
447 def getDiskInfo(data_dict, machine):
448     """Helper function for info, get data on disks for a machine.
449
450     Modifies data_dict to include the relevant data, and returns a list
451     of (key, name) pairs to display "name: data_dict[key]" to the user.
452     """
453     data_dict['num_disks'] = len(machine.disks)
454     disk_fields_template = [('%s_size', '%s size')]
455     disk_fields = []
456     for disk in machine.disks:
457         name = disk.guest_device_name
458         disk_fields.extend([(x % name, y % name) for x, y in
459                             disk_fields_template])
460         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
461     return disk_fields
462
463 def modifyDict(username, state, fields):
464     """Modify a machine as specified by CGI arguments.
465
466     Return a list of local variables for modify.tmpl.
467     """
468     olddisk = {}
469     session.begin()
470     try:
471         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
472         validate = validation.Validate(username, state, **kws)
473         machine = validate.machine
474         oldname = machine.name
475
476         if hasattr(validate, 'memory'):
477             machine.memory = validate.memory
478
479         if hasattr(validate, 'vmtype'):
480             machine.type = validate.vmtype
481
482         if hasattr(validate, 'disksize'):
483             disksize = validate.disksize
484             disk = machine.disks[0]
485             if disk.size != disksize:
486                 olddisk[disk.guest_device_name] = disksize
487                 disk.size = disksize
488                 session.save_or_update(disk)
489
490         update_acl = False
491         if hasattr(validate, 'owner') and validate.owner != machine.owner:
492             machine.owner = validate.owner
493             update_acl = True
494         if hasattr(validate, 'name'):
495             machine.name = validate.name
496             for n in machine.nics:
497                 if n.hostname == oldname:
498                     n.hostname = validate.name
499         if hasattr(validate, 'description'):
500             machine.description = validate.description
501         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
502             machine.administrator = validate.admin
503             update_acl = True
504         if hasattr(validate, 'contact'):
505             machine.contact = validate.contact
506
507         session.save_or_update(machine)
508         if update_acl:
509             cache_acls.refreshMachine(machine)
510         session.commit()
511     except:
512         session.rollback()
513         raise
514     for diskname in olddisk:
515         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
516     if hasattr(validate, 'name'):
517         controls.renameMachine(machine, oldname, validate.name)
518     return dict(user=username,
519                 command="modify",
520                 machine=machine)
521
522 def modify(username, state, path, fields):
523     """Handler for modifying attributes of a machine."""
524     try:
525         modify_dict = modifyDict(username, state, fields)
526     except InvalidInput, err:
527         result = None
528         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
529     else:
530         machine = modify_dict['machine']
531         result = 'Success!'
532         err = None
533     info_dict = infoDict(username, state, machine)
534     info_dict['err'] = err
535     if err:
536         for field in fields.keys():
537             setattr(info_dict['defaults'], field, fields.getfirst(field))
538     info_dict['result'] = result
539     return templates.info(searchList=[info_dict])
540
541 def badOperation(u, s, p, e):
542     """Function called when accessing an unknown URI."""
543     return ({'Status': '404 Not Found'}, 'Invalid operation.')
544
545 def infoDict(username, state, machine):
546     """Get the variables used by info.tmpl."""
547     status = controls.statusInfo(machine)
548     checkpoint.checkpoint('Getting status info')
549     has_vnc = hasVnc(status)
550     if status is None:
551         main_status = dict(name=machine.name,
552                            memory=str(machine.memory))
553         uptime = None
554         cputime = None
555     else:
556         main_status = dict(status[1:])
557         main_status['host'] = controls.listHost(machine)
558         start_time = float(main_status.get('start_time', 0))
559         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
560         cpu_time_float = float(main_status.get('cpu_time', 0))
561         cputime = datetime.timedelta(seconds=int(cpu_time_float))
562     checkpoint.checkpoint('Status')
563     display_fields = [('name', 'Name'),
564                       ('description', 'Description'),
565                       ('owner', 'Owner'),
566                       ('administrator', 'Administrator'),
567                       ('contact', 'Contact'),
568                       ('type', 'Type'),
569                       'NIC_INFO',
570                       ('uptime', 'uptime'),
571                       ('cputime', 'CPU usage'),
572                       ('host', 'Hosted on'),
573                       ('memory', 'RAM'),
574                       'DISK_INFO',
575                       ('state', 'state (xen format)'),
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              fields = fields)
630     return d
631
632 def unauthFront(_, _2, _3, fields):
633     """Information for unauth'd users."""
634     return templates.unauth(searchList=[{'simple' : True, 
635             'hostname' : socket.getfqdn()}])
636
637 def admin(username, state, path, fields):
638     if path == '':
639         return ({'Status': '303 See Other',
640                  'Location': 'admin/'},
641                 "You shouldn't see this message.")
642     if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
643         raise InvalidInput('username', username,
644                            'Not in admin group %s.' % config.adminacl)
645     newstate = State(username, isadmin=True)
646     newstate.environ = state.environ
647     return handler(username, newstate, path, fields)
648
649 def throwError(_, __, ___, ____):
650     """Throw an error, to test the error-tracing mechanisms."""
651     raise RuntimeError("test of the emergency broadcast system")
652
653 mapping = dict(
654                modify=modify,
655                create=create,
656                unauth=unauthFront,
657                admin=admin,
658                overlord=admin,
659                errortest=throwError)
660
661 def printHeaders(headers):
662     """Print a dictionary as HTTP headers."""
663     for key, value in headers.iteritems():
664         print '%s: %s' % (key, value)
665     print
666
667 def send_error_mail(subject, body):
668     import subprocess
669
670     to = config.web.errormail
671     mail = """To: %s
672 From: root@%s
673 Subject: %s
674
675 %s
676 """ % (to, config.web.hostname, subject, body)
677     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
678                          stdin=subprocess.PIPE)
679     p.stdin.write(mail)
680     p.stdin.close()
681     p.wait()
682
683 def show_error(op, username, fields, err, emsg, traceback):
684     """Print an error page when an exception occurs"""
685     d = dict(op=op, user=username, fields=fields,
686              errorMessage=str(err), stderr=emsg, traceback=traceback)
687     details = templates.error_raw(searchList=[d])
688     exclude = config.web.errormail_exclude
689     if username not in exclude and '*' not in exclude:
690         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
691                         details)
692     d['details'] = details
693     return templates.error(searchList=[d])
694
695 def handler(username, state, path, fields):
696     operation, path = pathSplit(path)
697     if not operation:
698         operation = 'list'
699     print 'Starting', operation
700     fun = mapping.get(operation, badOperation)
701     return fun(username, state, path, fields)
702
703 class App:
704     def __init__(self, environ, start_response):
705         self.environ = environ
706         self.start = start_response
707
708         self.username = getUser(environ)
709         self.state = State(self.username)
710         self.state.environ = environ
711
712         random.seed() #sigh
713
714     def __iter__(self):
715         start_time = time.time()
716         database.clear_cache()
717         sys.stderr = StringIO()
718         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
719         operation = self.environ.get('PATH_INFO', '')
720         if not operation:
721             self.start("301 Moved Permanently", [('Location', './')])
722             return
723         if self.username is None:
724             operation = 'unauth'
725
726         try:
727             checkpoint.checkpoint('Before')
728             output = handler(self.username, self.state, operation, fields)
729             checkpoint.checkpoint('After')
730
731             headers = dict(DEFAULT_HEADERS)
732             if isinstance(output, tuple):
733                 new_headers, output = output
734                 headers.update(new_headers)
735             e = revertStandardError()
736             if e:
737                 if hasattr(output, 'addError'):
738                     output.addError(e)
739                 else:
740                     # This only happens on redirects, so it'd be a pain to get
741                     # the message to the user.  Maybe in the response is useful.
742                     output = output + '\n\nstderr:\n' + e
743             output_string =  str(output)
744             checkpoint.checkpoint('output as a string')
745         except Exception, err:
746             if not fields.has_key('js'):
747                 if isinstance(err, InvalidInput):
748                     self.start('200 OK', [('Content-Type', 'text/html')])
749                     e = revertStandardError()
750                     yield str(invalidInput(operation, self.username, fields,
751                                            err, e))
752                     return
753             import traceback
754             self.start('500 Internal Server Error',
755                        [('Content-Type', 'text/html')])
756             e = revertStandardError()
757             s = show_error(operation, self.username, fields,
758                            err, e, traceback.format_exc())
759             yield str(s)
760             return
761         status = headers.setdefault('Status', '200 OK')
762         del headers['Status']
763         self.start(status, headers.items())
764         yield output_string
765         if fields.has_key('timedebug'):
766             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
767
768 def constructor():
769     connect()
770     return App
771
772 def main():
773     from flup.server.fcgi_fork import WSGIServer
774     WSGIServer(constructor()).run()
775
776 if __name__ == '__main__':
777     main()