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

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

Make command XSS error correct

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