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

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

Move the remctl code into invirt.remctl

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