source: trunk/packages/sipb-xen-www/code/main.py @ 603

Last change on this file since 603 was 603, checked in by price, 16 years ago

small code cleanups

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