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

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

Unauthenticated front page

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