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

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

Show installer status on the front page, and unbreak the autoinstall fields in validation logic

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