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

Last change on this file since 1705 was 1705, checked in by price, 15 years ago

rename autoinstaller help topic

  • Property svn:executable set to *
File size: 27.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
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.max_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                        cdrom='gutsy-i386')
206    checkpoint.checkpoint('Got defaults')
207    def sortkey(machine):
208        return (machine.owner != username, machine.owner, machine.name)
209    machines = sorted(machines, key=sortkey)
210    d = dict(user=username,
211             cant_add_vm=validation.cantAddVm(username, state),
212             max_memory=max_memory,
213             max_disk=max_disk,
214             defaults=defaults,
215             machines=machines,
216             has_vnc=has_vnc,
217             can_clone=can_clone)
218    return d
219
220def listVms(username, state, path, fields):
221    """Handler for list requests."""
222    checkpoint.checkpoint('Getting list dict')
223    d = getListDict(username, state)
224    checkpoint.checkpoint('Got list dict')
225    return templates.list(searchList=[d])
226
227def vnc(username, state, path, fields):
228    """VNC applet page.
229
230    Note that due to same-domain restrictions, the applet connects to
231    the webserver, which needs to forward those requests to the xen
232    server.  The Xen server runs another proxy that (1) authenticates
233    and (2) finds the correct port for the VM.
234
235    You might want iptables like:
236
237    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
238      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
239    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
240      --dport 10003 -j SNAT --to-source 18.187.7.142
241    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
242      --dport 10003 -j ACCEPT
243
244    Remember to enable iptables!
245    echo 1 > /proc/sys/net/ipv4/ip_forward
246    """
247    machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
248
249    token = controls.vnctoken(machine)
250    host = controls.listHost(machine)
251    if host:
252        port = 10003 + [h.hostname for h in config.hosts].index(host)
253    else:
254        port = 5900 # dummy
255
256    status = controls.statusInfo(machine)
257    has_vnc = hasVnc(status)
258
259    d = dict(user=username,
260             on=status,
261             has_vnc=has_vnc,
262             machine=machine,
263             hostname=state.environ.get('SERVER_NAME', 'localhost'),
264             port=port,
265             authtoken=token)
266    return templates.vnc(searchList=[d])
267
268def getHostname(nic):
269    """Find the hostname associated with a NIC.
270
271    XXX this should be merged with the similar logic in DNS and DHCP.
272    """
273    if nic.hostname and '.' in nic.hostname:
274        return nic.hostname
275    elif nic.machine:
276        return nic.machine.name + '.' + config.dns.domains[0]
277    else:
278        return None
279
280
281def getNicInfo(data_dict, machine):
282    """Helper function for info, get data on nics for a machine.
283
284    Modifies data_dict to include the relevant data, and returns a list
285    of (key, name) pairs to display "name: data_dict[key]" to the user.
286    """
287    data_dict['num_nics'] = len(machine.nics)
288    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
289                           ('nic%s_mac', 'NIC %s MAC Addr'),
290                           ('nic%s_ip', 'NIC %s IP'),
291                           ]
292    nic_fields = []
293    for i in range(len(machine.nics)):
294        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
295        if not i:
296            data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
297        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
298        data_dict['nic%s_ip' % i] = machine.nics[i].ip
299    if len(machine.nics) == 1:
300        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
301    return nic_fields
302
303def getDiskInfo(data_dict, machine):
304    """Helper function for info, get data on disks for a machine.
305
306    Modifies data_dict to include the relevant data, and returns a list
307    of (key, name) pairs to display "name: data_dict[key]" to the user.
308    """
309    data_dict['num_disks'] = len(machine.disks)
310    disk_fields_template = [('%s_size', '%s size')]
311    disk_fields = []
312    for disk in machine.disks:
313        name = disk.guest_device_name
314        disk_fields.extend([(x % name, y % name) for x, y in
315                            disk_fields_template])
316        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
317    return disk_fields
318
319def command(username, state, path, fields):
320    """Handler for running commands like boot and delete on a VM."""
321    back = fields.getfirst('back')
322    try:
323        d = controls.commandResult(username, state, fields)
324        if d['command'] == 'Delete VM':
325            back = 'list'
326    except InvalidInput, err:
327        if not back:
328            raise
329        print >> sys.stderr, err
330        result = err
331    else:
332        result = 'Success!'
333        if not back:
334            return templates.command(searchList=[d])
335    if back == 'list':
336        state.clear() #Changed global state
337        d = getListDict(username, state)
338        d['result'] = result
339        return templates.list(searchList=[d])
340    elif back == 'info':
341        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
342        return ({'Status': '303 See Other',
343                 'Location': 'info?machine_id=%d' % machine.machine_id},
344                "You shouldn't see this message.")
345    else:
346        raise InvalidInput('back', back, 'Not a known back page.')
347
348def modifyDict(username, state, fields):
349    """Modify a machine as specified by CGI arguments.
350
351    Return a list of local variables for modify.tmpl.
352    """
353    olddisk = {}
354    session.begin()
355    try:
356        kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
357        validate = validation.Validate(username, state, **kws)
358        machine = validate.machine
359        oldname = machine.name
360
361        if hasattr(validate, 'memory'):
362            machine.memory = validate.memory
363
364        if hasattr(validate, 'vmtype'):
365            machine.type = validate.vmtype
366
367        if hasattr(validate, 'disksize'):
368            disksize = validate.disksize
369            disk = machine.disks[0]
370            if disk.size != disksize:
371                olddisk[disk.guest_device_name] = disksize
372                disk.size = disksize
373                session.save_or_update(disk)
374
375        update_acl = False
376        if hasattr(validate, 'owner') and validate.owner != machine.owner:
377            machine.owner = validate.owner
378            update_acl = True
379        if hasattr(validate, 'name'):
380            machine.name = validate.name
381        if hasattr(validate, 'description'):
382            machine.description = validate.description
383        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
384            machine.administrator = validate.admin
385            update_acl = True
386        if hasattr(validate, 'contact'):
387            machine.contact = validate.contact
388
389        session.save_or_update(machine)
390        if update_acl:
391            cache_acls.refreshMachine(machine)
392        session.commit()
393    except:
394        session.rollback()
395        raise
396    for diskname in olddisk:
397        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
398    if hasattr(validate, 'name'):
399        controls.renameMachine(machine, oldname, validate.name)
400    return dict(user=username,
401                command="modify",
402                machine=machine)
403
404def modify(username, state, path, fields):
405    """Handler for modifying attributes of a machine."""
406    try:
407        modify_dict = modifyDict(username, state, fields)
408    except InvalidInput, err:
409        result = None
410        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
411    else:
412        machine = modify_dict['machine']
413        result = 'Success!'
414        err = None
415    info_dict = infoDict(username, state, machine)
416    info_dict['err'] = err
417    if err:
418        for field in fields.keys():
419            setattr(info_dict['defaults'], field, fields.getfirst(field))
420    info_dict['result'] = result
421    return templates.info(searchList=[info_dict])
422
423
424def helpHandler(username, state, path, fields):
425    """Handler for help messages."""
426    simple = fields.getfirst('simple')
427    subjects = fields.getlist('subject')
428
429    help_mapping = {
430                    'Autoinstalls': """
431The autoinstaller builds a minimal Debian or Ubuntu system to run as a
432ParaVM.  You can access the resulting system by logging into the <a
433href="help?simple=true&subject=ParaVM+Console">serial console server</a>
434with your Kerberos tickets; there is no root password and no sshd.</p>
435
436<p>Under the covers the autoinstaller uses our own patched version of
437xen-create-image, which is a tool based on debootstrap.  If you're
438quick in logging into the serial console, you can see it running.
439""",
440                    'ParaVM Console': """
441ParaVM machines do not support local console access over VNC.  To
442access the serial console of these machines, you can SSH with Kerberos
443to %s, using the name of the machine as your
444username.""" % config.console.hostname,
445                    'HVM/ParaVM': """
446HVM machines use the virtualization features of the processor, while
447ParaVM machines use Xen's emulation of virtualization features.  You
448want an HVM virtualized machine.""",
449                    'CPU Weight': """
450Don't ask us!  We're as mystified as you are.""",
451                    'Owner': """
452The owner field is used to determine <a
453href="help?subject=Quotas">quotas</a>.  It must be the name of a
454locker that you are an AFS administrator of.  In particular, you or an
455AFS group you are a member of must have AFS rlidwka bits on the
456locker.  You can check who administers the LOCKER locker using the
457commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
458href="help?subject=Administrator">administrator</a>.""",
459                    'Administrator': """
460The administrator field determines who can access the console and
461power on and off the machine.  This can be either a user or a moira
462group.""",
463                    'Quotas': """
464Quotas are determined on a per-locker basis.  Each locker may have a
465maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
466active machines.""",
467                    'Console': """
468<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
469setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
470your machine will run just fine, but the applet's display of the
471console will suffer artifacts.
472""",
473                    'Windows': """
474<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>
475<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.
476"""
477                    }
478
479    if not subjects:
480        subjects = sorted(help_mapping.keys())
481
482    d = dict(user=username,
483             simple=simple,
484             subjects=subjects,
485             mapping=help_mapping)
486
487    return templates.help(searchList=[d])
488
489
490def badOperation(u, s, p, e):
491    """Function called when accessing an unknown URI."""
492    return ({'Status': '404 Not Found'}, 'Invalid operation.')
493
494def infoDict(username, state, machine):
495    """Get the variables used by info.tmpl."""
496    status = controls.statusInfo(machine)
497    checkpoint.checkpoint('Getting status info')
498    has_vnc = hasVnc(status)
499    if status is None:
500        main_status = dict(name=machine.name,
501                           memory=str(machine.memory))
502        uptime = None
503        cputime = None
504    else:
505        main_status = dict(status[1:])
506        main_status['host'] = controls.listHost(machine)
507        start_time = float(main_status.get('start_time', 0))
508        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
509        cpu_time_float = float(main_status.get('cpu_time', 0))
510        cputime = datetime.timedelta(seconds=int(cpu_time_float))
511    checkpoint.checkpoint('Status')
512    display_fields = """name uptime memory state cpu_weight on_reboot
513     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
514    display_fields = [('name', 'Name'),
515                      ('description', 'Description'),
516                      ('owner', 'Owner'),
517                      ('administrator', 'Administrator'),
518                      ('contact', 'Contact'),
519                      ('type', 'Type'),
520                      'NIC_INFO',
521                      ('uptime', 'uptime'),
522                      ('cputime', 'CPU usage'),
523                      ('host', 'Hosted on'),
524                      ('memory', 'RAM'),
525                      'DISK_INFO',
526                      ('state', 'state (xen format)'),
527                      ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
528                      ('on_reboot', 'Action on VM reboot'),
529                      ('on_poweroff', 'Action on VM poweroff'),
530                      ('on_crash', 'Action on VM crash'),
531                      ('on_xend_start', 'Action on Xen start'),
532                      ('on_xend_stop', 'Action on Xen stop'),
533                      ('bootloader', 'Bootloader options'),
534                      ]
535    fields = []
536    machine_info = {}
537    machine_info['name'] = machine.name
538    machine_info['description'] = machine.description
539    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
540    machine_info['owner'] = machine.owner
541    machine_info['administrator'] = machine.administrator
542    machine_info['contact'] = machine.contact
543
544    nic_fields = getNicInfo(machine_info, machine)
545    nic_point = display_fields.index('NIC_INFO')
546    display_fields = (display_fields[:nic_point] + nic_fields +
547                      display_fields[nic_point+1:])
548
549    disk_fields = getDiskInfo(machine_info, machine)
550    disk_point = display_fields.index('DISK_INFO')
551    display_fields = (display_fields[:disk_point] + disk_fields +
552                      display_fields[disk_point+1:])
553
554    main_status['memory'] += ' MiB'
555    for field, disp in display_fields:
556        if field in ('uptime', 'cputime') and locals()[field] is not None:
557            fields.append((disp, locals()[field]))
558        elif field in machine_info:
559            fields.append((disp, machine_info[field]))
560        elif field in main_status:
561            fields.append((disp, main_status[field]))
562        else:
563            pass
564            #fields.append((disp, None))
565
566    checkpoint.checkpoint('Got fields')
567
568
569    max_mem = validation.maxMemory(machine.owner, state, machine, False)
570    checkpoint.checkpoint('Got mem')
571    max_disk = validation.maxDisk(machine.owner, machine)
572    defaults = Defaults()
573    for name in 'machine_id name description administrator owner memory contact'.split():
574        setattr(defaults, name, getattr(machine, name))
575    defaults.type = machine.type.type_id
576    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
577    checkpoint.checkpoint('Got defaults')
578    d = dict(user=username,
579             on=status is not None,
580             machine=machine,
581             defaults=defaults,
582             has_vnc=has_vnc,
583             uptime=str(uptime),
584             ram=machine.memory,
585             max_mem=max_mem,
586             max_disk=max_disk,
587             owner_help=helppopup("Owner"),
588             fields = fields)
589    return d
590
591def info(username, state, path, fields):
592    """Handler for info on a single VM."""
593    machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
594    d = infoDict(username, state, machine)
595    checkpoint.checkpoint('Got infodict')
596    return templates.info(searchList=[d])
597
598def unauthFront(_, _2, _3, fields):
599    """Information for unauth'd users."""
600    return templates.unauth(searchList=[{'simple' : True}])
601
602def admin(username, state, path, fields):
603    if path == '':
604        return ({'Status': '303 See Other',
605                 'Location': 'admin/'},
606                "You shouldn't see this message.")
607    if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
608        raise InvalidInput('username', username,
609                           'Not in admin group %s.' % config.web.adminacl)
610    newstate = State(username, isadmin=True)
611    newstate.environ = state.environ
612    return handler(username, newstate, path, fields)
613
614def throwError(_, __, ___, ____):
615    """Throw an error, to test the error-tracing mechanisms."""
616    raise RuntimeError("test of the emergency broadcast system")
617
618mapping = dict(list=listVms,
619               vnc=vnc,
620               command=command,
621               modify=modify,
622               info=info,
623               create=create,
624               help=helpHandler,
625               unauth=unauthFront,
626               admin=admin,
627               overlord=admin,
628               errortest=throwError)
629
630def printHeaders(headers):
631    """Print a dictionary as HTTP headers."""
632    for key, value in headers.iteritems():
633        print '%s: %s' % (key, value)
634    print
635
636def send_error_mail(subject, body):
637    import subprocess
638
639    to = config.web.errormail
640    mail = """To: %s
641From: root@%s
642Subject: %s
643
644%s
645""" % (to, config.web.hostname, subject, body)
646    p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
647    p.stdin.write(mail)
648    p.stdin.close()
649    p.wait()
650
651def show_error(op, username, fields, err, emsg, traceback):
652    """Print an error page when an exception occurs"""
653    d = dict(op=op, user=username, fields=fields,
654             errorMessage=str(err), stderr=emsg, traceback=traceback)
655    details = templates.error_raw(searchList=[d])
656    exclude = config.web.errormail_exclude
657    if username not in exclude and '*' not in exclude:
658        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
659                        details)
660    d['details'] = details
661    return templates.error(searchList=[d])
662
663def getUser(environ):
664    """Return the current user based on the SSL environment variables"""
665    user = environ.get('REMOTE_USER')
666    if user is None:
667        return
668   
669    if environ.get('AUTH_TYPE') == 'Negotiate':
670        # Convert the krb5 principal into a krb4 username
671        if not user.endswith('@%s' % config.authn[0].realm):
672            return
673        else:
674            return user.split('@')[0].replace('/', '.')
675    else:
676        return user
677
678def handler(username, state, path, fields):
679    operation, path = pathSplit(path)
680    if not operation:
681        operation = 'list'
682    print 'Starting', operation
683    fun = mapping.get(operation, badOperation)
684    return fun(username, state, path, fields)
685
686class App:
687    def __init__(self, environ, start_response):
688        self.environ = environ
689        self.start = start_response
690
691        self.username = getUser(environ)
692        self.state = State(self.username)
693        self.state.environ = environ
694
695        random.seed() #sigh
696
697    def __iter__(self):
698        start_time = time.time()
699        database.clear_cache()
700        sys.stderr = StringIO()
701        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
702        operation = self.environ.get('PATH_INFO', '')
703        if not operation:
704            self.start("301 Moved Permanently", [('Location', './')])
705            return
706        if self.username is None:
707            operation = 'unauth'
708
709        try:
710            checkpoint.checkpoint('Before')
711            output = handler(self.username, self.state, operation, fields)
712            checkpoint.checkpoint('After')
713
714            headers = dict(DEFAULT_HEADERS)
715            if isinstance(output, tuple):
716                new_headers, output = output
717                headers.update(new_headers)
718            e = revertStandardError()
719            if e:
720                if hasattr(output, 'addError'):
721                    output.addError(e)
722                else:
723                    # This only happens on redirects, so it'd be a pain to get
724                    # the message to the user.  Maybe in the response is useful.
725                    output = output + '\n\nstderr:\n' + e
726            output_string =  str(output)
727            checkpoint.checkpoint('output as a string')
728        except Exception, err:
729            if not fields.has_key('js'):
730                if isinstance(err, InvalidInput):
731                    self.start('200 OK', [('Content-Type', 'text/html')])
732                    e = revertStandardError()
733                    yield str(invalidInput(operation, self.username, fields,
734                                           err, e))
735                    return
736            import traceback
737            self.start('500 Internal Server Error',
738                       [('Content-Type', 'text/html')])
739            e = revertStandardError()
740            s = show_error(operation, self.username, fields,
741                           err, e, traceback.format_exc())
742            yield str(s)
743            return
744        status = headers.setdefault('Status', '200 OK')
745        del headers['Status']
746        self.start(status, headers.items())
747        yield output_string
748        if fields.has_key('timedebug'):
749            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
750
751def constructor():
752    connect()
753    return App
754
755def main():
756    from flup.server.fcgi_fork import WSGIServer
757    WSGIServer(constructor()).run()
758
759if __name__ == '__main__':
760    main()
Note: See TracBrowser for help on using the repository browser.