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

Last change on this file since 2690 was 2690, checked in by broder, 14 years ago

Implement administrator mode

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