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

Last change on this file since 2685 was 2685, checked in by broder, 15 years ago

Write handler for create form, and consolidate code to require POST into a CherryPy? tool

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