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

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

Add terminal page

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