source: trunk/packages/invirt-web/code/main.py @ 1724

Last change on this file since 1724 was 1718, checked in by price, 16 years ago

include a return address in error mail

outgoing.mit.edu requires this.

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