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

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

Get username and state from CherryPy? request

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