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

Last change on this file since 2420 was 2418, checked in by quentin, 15 years ago

Make commands work from list and info pages

  • Property svn:executable set to *
File size: 28.1 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, result=None):
62        """Handler for list requests."""
63        checkpoint.checkpoint('Getting list dict')
64        d = getListDict(cherrypy.request.login, cherrypy.request.state)
65        if result is not None:
66            d['result'] = result
67        checkpoint.checkpoint('Got list dict')
68        return d
69    index=list
70
71    @cherrypy.expose
72    @cherrypy.tools.mako(filename="/help.mako")
73    def help(self, subject=None, simple=False):
74        """Handler for help messages."""
75
76        help_mapping = {
77            'Autoinstalls': """
78The autoinstaller builds a minimal Debian or Ubuntu system to run as a
79ParaVM.  You can access the resulting system by logging into the <a
80href="help?simple=true&subject=ParaVM+Console">serial console server</a>
81with your Kerberos tickets; there is no root password so sshd will
82refuse login.</p>
83
84<p>Under the covers, the autoinstaller uses our own patched version of
85xen-create-image, which is a tool based on debootstrap.  If you log
86into the serial console while the install is running, you can watch
87it.
88""",
89            'ParaVM Console': """
90ParaVM machines do not support local console access over VNC.  To
91access the serial console of these machines, you can SSH with Kerberos
92to %s, using the name of the machine as your
93username.""" % config.console.hostname,
94            'HVM/ParaVM': """
95HVM machines use the virtualization features of the processor, while
96ParaVM machines rely on a modified kernel to communicate directly with
97the hypervisor.  HVMs support boot CDs of any operating system, and
98the VNC console applet.  The three-minute autoinstaller produces
99ParaVMs.  ParaVMs typically are more efficient, and always support the
100<a href="help?subject=ParaVM+Console">console server</a>.</p>
101
102<p>More details are <a
103href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
104wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
105(which you can skip by using the autoinstaller to begin with.)</p>
106
107<p>We recommend using a ParaVM when possible and an HVM when necessary.
108""",
109            'CPU Weight': """
110Don't ask us!  We're as mystified as you are.""",
111            'Owner': """
112The owner field is used to determine <a
113href="help?subject=Quotas">quotas</a>.  It must be the name of a
114locker that you are an AFS administrator of.  In particular, you or an
115AFS group you are a member of must have AFS rlidwka bits on the
116locker.  You can check who administers the LOCKER locker using the
117commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
118href="help?subject=Administrator">administrator</a>.""",
119            'Administrator': """
120The administrator field determines who can access the console and
121power on and off the machine.  This can be either a user or a moira
122group.""",
123            'Quotas': """
124Quotas are determined on a per-locker basis.  Each locker may have a
125maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
126active machines.""",
127            'Console': """
128<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
129setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
130your machine will run just fine, but the applet's display of the
131console will suffer artifacts.
132""",
133            'Windows': """
134<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>
135<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.
136"""
137            }
138
139        if not subject:
140            subject = sorted(help_mapping.keys())
141        if not isinstance(subject, list):
142            subject = [subject]
143
144        return dict(simple=simple,
145                    subjects=subject,
146                    mapping=help_mapping)
147    help._cp_config['tools.require_login.on'] = False
148
149    @cherrypy.expose
150    @cherrypy.tools.mako(filename="/helloworld.mako")
151    def helloworld(self, **kwargs):
152        return {'request': cherrypy.request, 'kwargs': kwargs}
153    helloworld._cp_config['tools.require_login.on'] = False
154
155    class MachineView(View):
156        # This is hairy. Fix when CherryPy 3.2 is out. (rename to
157        # _cp_dispatch, and parse the argument as a list instead of
158        # string
159
160        def __getattr__(self, name):
161            try:
162                machine_id = int(name)
163                cherrypy.request.params['machine_id'] = machine_id
164                return self
165            except ValueError:
166                return None
167
168        @cherrypy.expose
169        @cherrypy.tools.mako(filename="/info.mako")
170        def info(self, machine_id):
171            """Handler for info on a single VM."""
172            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
173            d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
174            checkpoint.checkpoint('Got infodict')
175            return d
176        index = info
177
178        @cherrypy.expose
179        @cherrypy.tools.mako(filename="/vnc.mako")
180        def vnc(self, machine_id):
181            """VNC applet page.
182
183            Note that due to same-domain restrictions, the applet connects to
184            the webserver, which needs to forward those requests to the xen
185            server.  The Xen server runs another proxy that (1) authenticates
186            and (2) finds the correct port for the VM.
187
188            You might want iptables like:
189
190            -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
191            --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
192            -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
193            --dport 10003 -j SNAT --to-source 18.187.7.142
194            -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
195            --dport 10003 -j ACCEPT
196
197            Remember to enable iptables!
198            echo 1 > /proc/sys/net/ipv4/ip_forward
199            """
200            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
201
202            token = controls.vnctoken(machine)
203            host = controls.listHost(machine)
204            if host:
205                port = 10003 + [h.hostname for h in config.hosts].index(host)
206            else:
207                port = 5900 # dummy
208
209            status = controls.statusInfo(machine)
210            has_vnc = hasVnc(status)
211
212            d = dict(on=status,
213                     has_vnc=has_vnc,
214                     machine=machine,
215                     hostname=cherrypy.request.local.name,
216                     port=port,
217                     authtoken=token)
218            return d
219        @cherrypy.expose
220        @cherrypy.tools.mako(filename="/command.mako")
221        def command(self, command_name, machine_id, **kwargs):
222            """Handler for running commands like boot and delete on a VM."""
223            if cherrypy.request.method != "POST":
224                raise InvalidInput("command_name", command_name, "That's not a valid command")
225            back = kwargs.get('back', None)
226            try:
227                d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
228                if d['command'] == 'Delete VM':
229                    back = 'list'
230            except InvalidInput, err:
231                if not back:
232                    raise
233                print >> sys.stderr, err
234                result = err
235            else:
236                result = 'Success!'
237                if not back:
238                    return d
239            if back == 'list':
240                cherrypy.request.state.clear() #Changed global state
241                raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
242            elif back == 'info':
243                raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
244            else:
245                raise InvalidInput('back', back, 'Not a known back page.')
246
247    machine = MachineView()
248
249def pathSplit(path):
250    if path.startswith('/'):
251        path = path[1:]
252    i = path.find('/')
253    if i == -1:
254        i = len(path)
255    return path[:i], path[i:]
256
257class Checkpoint:
258    def __init__(self):
259        self.start_time = time.time()
260        self.checkpoints = []
261
262    def checkpoint(self, s):
263        self.checkpoints.append((s, time.time()))
264
265    def __str__(self):
266        return ('Timing info:\n%s\n' %
267                '\n'.join(['%s: %s' % (d, t - self.start_time) for
268                           (d, t) in self.checkpoints]))
269
270checkpoint = Checkpoint()
271
272def makeErrorPre(old, addition):
273    if addition is None:
274        return
275    if old:
276        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
277    else:
278        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
279
280Template.database = database
281Template.config = config
282Template.err = None
283
284class JsonDict:
285    """Class to store a dictionary that will be converted to JSON"""
286    def __init__(self, **kws):
287        self.data = kws
288        if 'err' in kws:
289            err = kws['err']
290            del kws['err']
291            self.addError(err)
292
293    def __str__(self):
294        return simplejson.dumps(self.data)
295
296    def addError(self, text):
297        """Add stderr text to be displayed on the website."""
298        self.data['err'] = \
299            makeErrorPre(self.data.get('err'), text)
300
301class Defaults:
302    """Class to store default values for fields."""
303    memory = 256
304    disk = 4.0
305    cdrom = ''
306    autoinstall = ''
307    name = ''
308    description = ''
309    type = 'linux-hvm'
310
311    def __init__(self, max_memory=None, max_disk=None, **kws):
312        if max_memory is not None:
313            self.memory = min(self.memory, max_memory)
314        if max_disk is not None:
315            self.disk = min(self.disk, max_disk)
316        for key in kws:
317            setattr(self, key, kws[key])
318
319
320
321DEFAULT_HEADERS = {'Content-Type': 'text/html'}
322
323def invalidInput(op, username, fields, err, emsg):
324    """Print an error page when an InvalidInput exception occurs"""
325    d = dict(op=op, user=username, err_field=err.err_field,
326             err_value=str(err.err_value), stderr=emsg,
327             errorMessage=str(err))
328    return templates.invalid(searchList=[d])
329
330def hasVnc(status):
331    """Does the machine with a given status list support VNC?"""
332    if status is None:
333        return False
334    for l in status:
335        if l[0] == 'device' and l[1][0] == 'vfb':
336            d = dict(l[1][1:])
337            return 'location' in d
338    return False
339
340def parseCreate(username, state, fields):
341    kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
342    validate = validation.Validate(username, state, strict=True, **kws)
343    return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
344                disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
345                cdrom=getattr(validate, 'cdrom', None),
346                autoinstall=getattr(validate, 'autoinstall', None))
347
348def create(username, state, path, fields):
349    """Handler for create requests."""
350    try:
351        parsed_fields = parseCreate(username, state, fields)
352        machine = controls.createVm(username, state, **parsed_fields)
353    except InvalidInput, err:
354        pass
355    else:
356        err = None
357    state.clear() #Changed global state
358    d = getListDict(username, state)
359    d['err'] = err
360    if err:
361        for field in fields.keys():
362            setattr(d['defaults'], field, fields.getfirst(field))
363    else:
364        d['new_machine'] = parsed_fields['name']
365    return templates.list(searchList=[d])
366
367
368def getListDict(username, state):
369    """Gets the list of local variables used by list.tmpl."""
370    checkpoint.checkpoint('Starting')
371    machines = state.machines
372    checkpoint.checkpoint('Got my machines')
373    on = {}
374    has_vnc = {}
375    xmlist = state.xmlist
376    checkpoint.checkpoint('Got uptimes')
377    can_clone = 'ice3' not in state.xmlist_raw
378    for m in machines:
379        if m not in xmlist:
380            has_vnc[m] = 'Off'
381            m.uptime = None
382        else:
383            m.uptime = xmlist[m]['uptime']
384            if xmlist[m]['console']:
385                has_vnc[m] = True
386            elif m.type.hvm:
387                has_vnc[m] = "WTF?"
388            else:
389                has_vnc[m] = "ParaVM"
390    max_memory = validation.maxMemory(username, state)
391    max_disk = validation.maxDisk(username)
392    checkpoint.checkpoint('Got max mem/disk')
393    defaults = Defaults(max_memory=max_memory,
394                        max_disk=max_disk,
395                        owner=username)
396    checkpoint.checkpoint('Got defaults')
397    def sortkey(machine):
398        return (machine.owner != username, machine.owner, machine.name)
399    machines = sorted(machines, key=sortkey)
400    d = dict(user=username,
401             cant_add_vm=validation.cantAddVm(username, state),
402             max_memory=max_memory,
403             max_disk=max_disk,
404             defaults=defaults,
405             machines=machines,
406             has_vnc=has_vnc,
407             can_clone=can_clone)
408    return d
409
410def getHostname(nic):
411    """Find the hostname associated with a NIC.
412
413    XXX this should be merged with the similar logic in DNS and DHCP.
414    """
415    if nic.hostname:
416        hostname = nic.hostname
417    elif nic.machine:
418        hostname = nic.machine.name
419    else:
420        return None
421    if '.' in hostname:
422        return hostname
423    else:
424        return hostname + '.' + config.dns.domains[0]
425
426def getNicInfo(data_dict, machine):
427    """Helper function for info, get data on nics for a machine.
428
429    Modifies data_dict to include the relevant data, and returns a list
430    of (key, name) pairs to display "name: data_dict[key]" to the user.
431    """
432    data_dict['num_nics'] = len(machine.nics)
433    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
434                           ('nic%s_mac', 'NIC %s MAC Addr'),
435                           ('nic%s_ip', 'NIC %s IP'),
436                           ]
437    nic_fields = []
438    for i in range(len(machine.nics)):
439        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
440        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
441        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
442        data_dict['nic%s_ip' % i] = machine.nics[i].ip
443    if len(machine.nics) == 1:
444        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
445    return nic_fields
446
447def getDiskInfo(data_dict, machine):
448    """Helper function for info, get data on disks for a machine.
449
450    Modifies data_dict to include the relevant data, and returns a list
451    of (key, name) pairs to display "name: data_dict[key]" to the user.
452    """
453    data_dict['num_disks'] = len(machine.disks)
454    disk_fields_template = [('%s_size', '%s size')]
455    disk_fields = []
456    for disk in machine.disks:
457        name = disk.guest_device_name
458        disk_fields.extend([(x % name, y % name) for x, y in
459                            disk_fields_template])
460        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
461    return disk_fields
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               modify=modify,
655               create=create,
656               unauth=unauthFront,
657               admin=admin,
658               overlord=admin,
659               errortest=throwError)
660
661def printHeaders(headers):
662    """Print a dictionary as HTTP headers."""
663    for key, value in headers.iteritems():
664        print '%s: %s' % (key, value)
665    print
666
667def send_error_mail(subject, body):
668    import subprocess
669
670    to = config.web.errormail
671    mail = """To: %s
672From: root@%s
673Subject: %s
674
675%s
676""" % (to, config.web.hostname, subject, body)
677    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
678                         stdin=subprocess.PIPE)
679    p.stdin.write(mail)
680    p.stdin.close()
681    p.wait()
682
683def show_error(op, username, fields, err, emsg, traceback):
684    """Print an error page when an exception occurs"""
685    d = dict(op=op, user=username, fields=fields,
686             errorMessage=str(err), stderr=emsg, traceback=traceback)
687    details = templates.error_raw(searchList=[d])
688    exclude = config.web.errormail_exclude
689    if username not in exclude and '*' not in exclude:
690        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
691                        details)
692    d['details'] = details
693    return templates.error(searchList=[d])
694
695def handler(username, state, path, fields):
696    operation, path = pathSplit(path)
697    if not operation:
698        operation = 'list'
699    print 'Starting', operation
700    fun = mapping.get(operation, badOperation)
701    return fun(username, state, path, fields)
702
703class App:
704    def __init__(self, environ, start_response):
705        self.environ = environ
706        self.start = start_response
707
708        self.username = getUser(environ)
709        self.state = State(self.username)
710        self.state.environ = environ
711
712        random.seed() #sigh
713
714    def __iter__(self):
715        start_time = time.time()
716        database.clear_cache()
717        sys.stderr = StringIO()
718        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
719        operation = self.environ.get('PATH_INFO', '')
720        if not operation:
721            self.start("301 Moved Permanently", [('Location', './')])
722            return
723        if self.username is None:
724            operation = 'unauth'
725
726        try:
727            checkpoint.checkpoint('Before')
728            output = handler(self.username, self.state, operation, fields)
729            checkpoint.checkpoint('After')
730
731            headers = dict(DEFAULT_HEADERS)
732            if isinstance(output, tuple):
733                new_headers, output = output
734                headers.update(new_headers)
735            e = revertStandardError()
736            if e:
737                if hasattr(output, 'addError'):
738                    output.addError(e)
739                else:
740                    # This only happens on redirects, so it'd be a pain to get
741                    # the message to the user.  Maybe in the response is useful.
742                    output = output + '\n\nstderr:\n' + e
743            output_string =  str(output)
744            checkpoint.checkpoint('output as a string')
745        except Exception, err:
746            if not fields.has_key('js'):
747                if isinstance(err, InvalidInput):
748                    self.start('200 OK', [('Content-Type', 'text/html')])
749                    e = revertStandardError()
750                    yield str(invalidInput(operation, self.username, fields,
751                                           err, e))
752                    return
753            import traceback
754            self.start('500 Internal Server Error',
755                       [('Content-Type', 'text/html')])
756            e = revertStandardError()
757            s = show_error(operation, self.username, fields,
758                           err, e, traceback.format_exc())
759            yield str(s)
760            return
761        status = headers.setdefault('Status', '200 OK')
762        del headers['Status']
763        self.start(status, headers.items())
764        yield output_string
765        if fields.has_key('timedebug'):
766            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
767
768def constructor():
769    connect()
770    return App
771
772def main():
773    from flup.server.fcgi_fork import WSGIServer
774    WSGIServer(constructor()).run()
775
776if __name__ == '__main__':
777    main()
Note: See TracBrowser for help on using the repository browser.