source: trunk/packages/invirt-web/code/main.py @ 2000

Last change on this file since 2000 was 1977, checked in by broder, 16 years ago

Update the nics.hostname field to keep it in sync with machine.name
when VMs are renamed.

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