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

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

Port vnc page to Mako and CherryPy?

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