source: package_branches/invirt-web/cherrypy/code/main.py @ 2428

Last change on this file since 2428 was 2428, checked in by quentin, 15 years ago

Update the errortest handler for cherrypy

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