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

Last change on this file since 1942 was 1836, checked in by broder, 16 years ago

config.{authn[0] => kerberos}.realm

  • Property svn:executable set to *
File size: 27.8 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,
[1739]204                        owner=username)
[235]205    checkpoint.checkpoint('Got defaults')
[424]206    def sortkey(machine):
[572]207        return (machine.owner != username, machine.owner, machine.name)
[424]208    machines = sorted(machines, key=sortkey)
[572]209    d = dict(user=username,
210             cant_add_vm=validation.cantAddVm(username, state),
[205]211             max_memory=max_memory,
[144]212             max_disk=max_disk,
[205]213             defaults=defaults,
[113]214             machines=machines,
[540]215             has_vnc=has_vnc,
216             can_clone=can_clone)
[205]217    return d
[113]218
[632]219def listVms(username, state, path, fields):
[205]220    """Handler for list requests."""
[235]221    checkpoint.checkpoint('Getting list dict')
[572]222    d = getListDict(username, state)
[235]223    checkpoint.checkpoint('Got list dict')
224    return templates.list(searchList=[d])
[438]225
[632]226def vnc(username, state, path, fields):
[119]227    """VNC applet page.
228
229    Note that due to same-domain restrictions, the applet connects to
230    the webserver, which needs to forward those requests to the xen
231    server.  The Xen server runs another proxy that (1) authenticates
232    and (2) finds the correct port for the VM.
233
234    You might want iptables like:
235
[205]236    -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
[438]237      --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
[205]238    -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
[438]239      --dport 10003 -j SNAT --to-source 18.187.7.142
[205]240    -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
241      --dport 10003 -j ACCEPT
[145]242
243    Remember to enable iptables!
244    echo 1 > /proc/sys/net/ipv4/ip_forward
[119]245    """
[572]246    machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
[438]247
[1618]248    token = controls.vnctoken(machine)
[797]249    host = controls.listHost(machine)
250    if host:
[863]251        port = 10003 + [h.hostname for h in config.hosts].index(host)
[797]252    else:
253        port = 5900 # dummy
[438]254
[209]255    status = controls.statusInfo(machine)
[152]256    has_vnc = hasVnc(status)
[438]257
[572]258    d = dict(user=username,
[152]259             on=status,
260             has_vnc=has_vnc,
[113]261             machine=machine,
[581]262             hostname=state.environ.get('SERVER_NAME', 'localhost'),
[667]263             port=port,
[113]264             authtoken=token)
[235]265    return templates.vnc(searchList=[d])
[113]266
[252]267def getHostname(nic):
[438]268    """Find the hostname associated with a NIC.
269
270    XXX this should be merged with the similar logic in DNS and DHCP.
271    """
[252]272    if nic.hostname and '.' in nic.hostname:
273        return nic.hostname
274    elif nic.machine:
[863]275        return nic.machine.name + '.' + config.dns.domains[0]
[252]276    else:
277        return None
278
279
[133]280def getNicInfo(data_dict, machine):
[145]281    """Helper function for info, get data on nics for a machine.
282
283    Modifies data_dict to include the relevant data, and returns a list
284    of (key, name) pairs to display "name: data_dict[key]" to the user.
285    """
[133]286    data_dict['num_nics'] = len(machine.nics)
[227]287    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
[133]288                           ('nic%s_mac', 'NIC %s MAC Addr'),
289                           ('nic%s_ip', 'NIC %s IP'),
290                           ]
291    nic_fields = []
292    for i in range(len(machine.nics)):
293        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
[227]294        if not i:
[252]295            data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
[133]296        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
297        data_dict['nic%s_ip' % i] = machine.nics[i].ip
298    if len(machine.nics) == 1:
299        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
300    return nic_fields
301
302def getDiskInfo(data_dict, machine):
[145]303    """Helper function for info, get data on disks for a machine.
304
305    Modifies data_dict to include the relevant data, and returns a list
306    of (key, name) pairs to display "name: data_dict[key]" to the user.
307    """
[133]308    data_dict['num_disks'] = len(machine.disks)
309    disk_fields_template = [('%s_size', '%s size')]
310    disk_fields = []
311    for disk in machine.disks:
312        name = disk.guest_device_name
[438]313        disk_fields.extend([(x % name, y % name) for x, y in
[205]314                            disk_fields_template])
[211]315        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
[133]316    return disk_fields
317
[632]318def command(username, state, path, fields):
[205]319    """Handler for running commands like boot and delete on a VM."""
[207]320    back = fields.getfirst('back')
[205]321    try:
[572]322        d = controls.commandResult(username, state, fields)
[207]323        if d['command'] == 'Delete VM':
324            back = 'list'
[205]325    except InvalidInput, err:
[207]326        if not back:
[205]327            raise
[572]328        print >> sys.stderr, err
[261]329        result = err
[205]330    else:
331        result = 'Success!'
[207]332        if not back:
[235]333            return templates.command(searchList=[d])
[207]334    if back == 'list':
[572]335        state.clear() #Changed global state
[576]336        d = getListDict(username, state)
[207]337        d['result'] = result
[235]338        return templates.list(searchList=[d])
[207]339    elif back == 'info':
[572]340        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
[588]341        return ({'Status': '303 See Other',
[633]342                 'Location': 'info?machine_id=%d' % machine.machine_id},
[407]343                "You shouldn't see this message.")
[205]344    else:
[261]345        raise InvalidInput('back', back, 'Not a known back page.')
[205]346
[572]347def modifyDict(username, state, fields):
[438]348    """Modify a machine as specified by CGI arguments.
349
350    Return a list of local variables for modify.tmpl.
351    """
[177]352    olddisk = {}
[1013]353    session.begin()
[161]354    try:
[609]355        kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
[572]356        validate = validation.Validate(username, state, **kws)
357        machine = validate.machine
[161]358        oldname = machine.name
[153]359
[572]360        if hasattr(validate, 'memory'):
361            machine.memory = validate.memory
[438]362
[572]363        if hasattr(validate, 'vmtype'):
364            machine.type = validate.vmtype
[440]365
[572]366        if hasattr(validate, 'disksize'):
367            disksize = validate.disksize
[177]368            disk = machine.disks[0]
369            if disk.size != disksize:
370                olddisk[disk.guest_device_name] = disksize
371                disk.size = disksize
[1013]372                session.save_or_update(disk)
[438]373
[446]374        update_acl = False
[572]375        if hasattr(validate, 'owner') and validate.owner != machine.owner:
376            machine.owner = validate.owner
[446]377            update_acl = True
[572]378        if hasattr(validate, 'name'):
[586]379            machine.name = validate.name
[609]380        if hasattr(validate, 'description'):
381            machine.description = validate.description
[572]382        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
383            machine.administrator = validate.admin
[446]384            update_acl = True
[572]385        if hasattr(validate, 'contact'):
386            machine.contact = validate.contact
[438]387
[1013]388        session.save_or_update(machine)
[446]389        if update_acl:
390            cache_acls.refreshMachine(machine)
[1013]391        session.commit()
[161]392    except:
[1013]393        session.rollback()
[163]394        raise
[177]395    for diskname in olddisk:
[209]396        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
[572]397    if hasattr(validate, 'name'):
398        controls.renameMachine(machine, oldname, validate.name)
399    return dict(user=username,
400                command="modify",
[205]401                machine=machine)
[438]402
[632]403def modify(username, state, path, fields):
[205]404    """Handler for modifying attributes of a machine."""
405    try:
[572]406        modify_dict = modifyDict(username, state, fields)
[205]407    except InvalidInput, err:
[207]408        result = None
[572]409        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
[205]410    else:
411        machine = modify_dict['machine']
[209]412        result = 'Success!'
[205]413        err = None
[585]414    info_dict = infoDict(username, state, machine)
[205]415    info_dict['err'] = err
416    if err:
417        for field in fields.keys():
418            setattr(info_dict['defaults'], field, fields.getfirst(field))
[207]419    info_dict['result'] = result
[235]420    return templates.info(searchList=[info_dict])
[161]421
[438]422
[632]423def helpHandler(username, state, path, fields):
[145]424    """Handler for help messages."""
[139]425    simple = fields.getfirst('simple')
426    subjects = fields.getlist('subject')
[438]427
[1704]428    help_mapping = {
[1705]429                    'Autoinstalls': """
[1704]430The autoinstaller builds a minimal Debian or Ubuntu system to run as a
431ParaVM.  You can access the resulting system by logging into the <a
432href="help?simple=true&subject=ParaVM+Console">serial console server</a>
[1707]433with your Kerberos tickets; there is no root password so sshd will
[1706]434refuse login.</p>
[1704]435
[1707]436<p>Under the covers, the autoinstaller uses our own patched version of
437xen-create-image, which is a tool based on debootstrap.  If you log
438into the serial console while the install is running, you can watch
439it.
[1704]440""",
441                    'ParaVM Console': """
[432]442ParaVM machines do not support local console access over VNC.  To
443access the serial console of these machines, you can SSH with Kerberos
[1634]444to %s, using the name of the machine as your
445username.""" % config.console.hostname,
[536]446                    'HVM/ParaVM': """
[139]447HVM machines use the virtualization features of the processor, while
[1736]448ParaVM machines rely on a modified kernel to communicate directly with
449the hypervisor.  HVMs support boot CDs of any operating system, and
450the VNC console applet.  The three-minute autoinstaller produces
451ParaVMs.  ParaVMs typically are more efficient, and always support the
[1737]452<a href="help?subject=ParaVM+Console">console server</a>.</p>
[1736]453
[1737]454<p>More details are <a
455href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
456wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
457(which you can skip by using the autoinstaller to begin with.)</p>
458
[1736]459<p>We recommend using a ParaVM when possible and an HVM when necessary.
460""",
[536]461                    'CPU Weight': """
[205]462Don't ask us!  We're as mystified as you are.""",
[536]463                    'Owner': """
[205]464The owner field is used to determine <a
[536]465href="help?subject=Quotas">quotas</a>.  It must be the name of a
[205]466locker that you are an AFS administrator of.  In particular, you or an
467AFS group you are a member of must have AFS rlidwka bits on the
[432]468locker.  You can check who administers the LOCKER locker using the
469commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
[536]470href="help?subject=Administrator">administrator</a>.""",
471                    'Administrator': """
[205]472The administrator field determines who can access the console and
473power on and off the machine.  This can be either a user or a moira
474group.""",
[536]475                    'Quotas': """
[408]476Quotas are determined on a per-locker basis.  Each locker may have a
[205]477maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
[309]478active machines.""",
[536]479                    'Console': """
[309]480<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
481setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
482your machine will run just fine, but the applet's display of the
483console will suffer artifacts.
[912]484""",
485                    'Windows': """
486<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>
487<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]488"""
[536]489                    }
[438]490
[187]491    if not subjects:
[205]492        subjects = sorted(help_mapping.keys())
[438]493
[572]494    d = dict(user=username,
[139]495             simple=simple,
496             subjects=subjects,
[205]497             mapping=help_mapping)
[438]498
[235]499    return templates.help(searchList=[d])
[133]500
[438]501
[632]502def badOperation(u, s, p, e):
[438]503    """Function called when accessing an unknown URI."""
[607]504    return ({'Status': '404 Not Found'}, 'Invalid operation.')
[205]505
[579]506def infoDict(username, state, machine):
[438]507    """Get the variables used by info.tmpl."""
[209]508    status = controls.statusInfo(machine)
[235]509    checkpoint.checkpoint('Getting status info')
[133]510    has_vnc = hasVnc(status)
511    if status is None:
512        main_status = dict(name=machine.name,
513                           memory=str(machine.memory))
[205]514        uptime = None
515        cputime = None
[133]516    else:
517        main_status = dict(status[1:])
[662]518        main_status['host'] = controls.listHost(machine)
[167]519        start_time = float(main_status.get('start_time', 0))
520        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
521        cpu_time_float = float(main_status.get('cpu_time', 0))
522        cputime = datetime.timedelta(seconds=int(cpu_time_float))
[235]523    checkpoint.checkpoint('Status')
[133]524    display_fields = """name uptime memory state cpu_weight on_reboot
525     on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
526    display_fields = [('name', 'Name'),
[609]527                      ('description', 'Description'),
[133]528                      ('owner', 'Owner'),
[187]529                      ('administrator', 'Administrator'),
[133]530                      ('contact', 'Contact'),
[136]531                      ('type', 'Type'),
[133]532                      'NIC_INFO',
533                      ('uptime', 'uptime'),
534                      ('cputime', 'CPU usage'),
[662]535                      ('host', 'Hosted on'),
[133]536                      ('memory', 'RAM'),
537                      'DISK_INFO',
538                      ('state', 'state (xen format)'),
[536]539                      ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
[133]540                      ('on_reboot', 'Action on VM reboot'),
541                      ('on_poweroff', 'Action on VM poweroff'),
542                      ('on_crash', 'Action on VM crash'),
543                      ('on_xend_start', 'Action on Xen start'),
544                      ('on_xend_stop', 'Action on Xen stop'),
545                      ('bootloader', 'Bootloader options'),
546                      ]
547    fields = []
548    machine_info = {}
[147]549    machine_info['name'] = machine.name
[609]550    machine_info['description'] = machine.description
[136]551    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
[133]552    machine_info['owner'] = machine.owner
[187]553    machine_info['administrator'] = machine.administrator
[133]554    machine_info['contact'] = machine.contact
555
556    nic_fields = getNicInfo(machine_info, machine)
557    nic_point = display_fields.index('NIC_INFO')
[438]558    display_fields = (display_fields[:nic_point] + nic_fields +
[205]559                      display_fields[nic_point+1:])
[133]560
561    disk_fields = getDiskInfo(machine_info, machine)
562    disk_point = display_fields.index('DISK_INFO')
[438]563    display_fields = (display_fields[:disk_point] + disk_fields +
[205]564                      display_fields[disk_point+1:])
[438]565
[211]566    main_status['memory'] += ' MiB'
[133]567    for field, disp in display_fields:
[167]568        if field in ('uptime', 'cputime') and locals()[field] is not None:
[133]569            fields.append((disp, locals()[field]))
[147]570        elif field in machine_info:
571            fields.append((disp, machine_info[field]))
[133]572        elif field in main_status:
573            fields.append((disp, main_status[field]))
574        else:
575            pass
576            #fields.append((disp, None))
[235]577
578    checkpoint.checkpoint('Got fields')
579
580
[572]581    max_mem = validation.maxMemory(machine.owner, state, machine, False)
[235]582    checkpoint.checkpoint('Got mem')
[566]583    max_disk = validation.maxDisk(machine.owner, machine)
[209]584    defaults = Defaults()
[609]585    for name in 'machine_id name description administrator owner memory contact'.split():
[205]586        setattr(defaults, name, getattr(machine, name))
[516]587    defaults.type = machine.type.type_id
[205]588    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
[235]589    checkpoint.checkpoint('Got defaults')
[572]590    d = dict(user=username,
[133]591             on=status is not None,
592             machine=machine,
[205]593             defaults=defaults,
[133]594             has_vnc=has_vnc,
595             uptime=str(uptime),
596             ram=machine.memory,
[144]597             max_mem=max_mem,
598             max_disk=max_disk,
[536]599             owner_help=helppopup("Owner"),
[133]600             fields = fields)
[205]601    return d
[113]602
[632]603def info(username, state, path, fields):
[205]604    """Handler for info on a single VM."""
[572]605    machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
[579]606    d = infoDict(username, state, machine)
[235]607    checkpoint.checkpoint('Got infodict')
608    return templates.info(searchList=[d])
[205]609
[632]610def unauthFront(_, _2, _3, fields):
[510]611    """Information for unauth'd users."""
612    return templates.unauth(searchList=[{'simple' : True}])
613
[867]614def admin(username, state, path, fields):
[633]615    if path == '':
616        return ({'Status': '303 See Other',
[867]617                 'Location': 'admin/'},
[633]618                "You shouldn't see this message.")
[868]619    if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
[867]620        raise InvalidInput('username', username,
[868]621                           'Not in admin group %s.' % config.web.adminacl)
[867]622    newstate = State(username, isadmin=True)
[632]623    newstate.environ = state.environ
624    return handler(username, newstate, path, fields)
625
626def throwError(_, __, ___, ____):
[598]627    """Throw an error, to test the error-tracing mechanisms."""
[602]628    raise RuntimeError("test of the emergency broadcast system")
[598]629
[113]630mapping = dict(list=listVms,
631               vnc=vnc,
[133]632               command=command,
633               modify=modify,
[113]634               info=info,
[139]635               create=create,
[510]636               help=helpHandler,
[598]637               unauth=unauthFront,
[867]638               admin=admin,
[869]639               overlord=admin,
[598]640               errortest=throwError)
[113]641
[205]642def printHeaders(headers):
[438]643    """Print a dictionary as HTTP headers."""
[205]644    for key, value in headers.iteritems():
645        print '%s: %s' % (key, value)
646    print
647
[598]648def send_error_mail(subject, body):
649    import subprocess
[205]650
[863]651    to = config.web.errormail
[598]652    mail = """To: %s
[863]653From: root@%s
[598]654Subject: %s
655
656%s
[863]657""" % (to, config.web.hostname, subject, body)
[1718]658    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
659                         stdin=subprocess.PIPE)
[598]660    p.stdin.write(mail)
661    p.stdin.close()
662    p.wait()
663
[603]664def show_error(op, username, fields, err, emsg, traceback):
665    """Print an error page when an exception occurs"""
666    d = dict(op=op, user=username, fields=fields,
667             errorMessage=str(err), stderr=emsg, traceback=traceback)
668    details = templates.error_raw(searchList=[d])
[1103]669    exclude = config.web.errormail_exclude
670    if username not in exclude and '*' not in exclude:
[627]671        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
672                        details)
[603]673    d['details'] = details
674    return templates.error(searchList=[d])
675
[572]676def getUser(environ):
[205]677    """Return the current user based on the SSL environment variables"""
[1642]678    user = environ.get('REMOTE_USER')
679    if user is None:
680        return
681   
682    if environ.get('AUTH_TYPE') == 'Negotiate':
683        # Convert the krb5 principal into a krb4 username
[1836]684        if not user.endswith('@%s' % config.kerberos.realm):
[1642]685            return
686        else:
687            return user.split('@')[0].replace('/', '.')
688    else:
689        return user
[205]690
[632]691def handler(username, state, path, fields):
692    operation, path = pathSplit(path)
693    if not operation:
694        operation = 'list'
695    print 'Starting', operation
696    fun = mapping.get(operation, badOperation)
697    return fun(username, state, path, fields)
698
[579]699class App:
700    def __init__(self, environ, start_response):
701        self.environ = environ
702        self.start = start_response
[205]703
[579]704        self.username = getUser(environ)
705        self.state = State(self.username)
[581]706        self.state.environ = environ
[205]707
[634]708        random.seed() #sigh
709
[579]710    def __iter__(self):
[632]711        start_time = time.time()
[864]712        database.clear_cache()
[600]713        sys.stderr = StringIO()
[579]714        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
715        operation = self.environ.get('PATH_INFO', '')
716        if not operation:
[633]717            self.start("301 Moved Permanently", [('Location', './')])
[579]718            return
719        if self.username is None:
720            operation = 'unauth'
721
722        try:
723            checkpoint.checkpoint('Before')
[632]724            output = handler(self.username, self.state, operation, fields)
[579]725            checkpoint.checkpoint('After')
726
727            headers = dict(DEFAULT_HEADERS)
728            if isinstance(output, tuple):
729                new_headers, output = output
730                headers.update(new_headers)
731            e = revertStandardError()
732            if e:
[693]733                if hasattr(output, 'addError'):
734                    output.addError(e)
735                else:
736                    # This only happens on redirects, so it'd be a pain to get
737                    # the message to the user.  Maybe in the response is useful.
738                    output = output + '\n\nstderr:\n' + e
[579]739            output_string =  str(output)
740            checkpoint.checkpoint('output as a string')
741        except Exception, err:
742            if not fields.has_key('js'):
743                if isinstance(err, InvalidInput):
744                    self.start('200 OK', [('Content-Type', 'text/html')])
745                    e = revertStandardError()
[603]746                    yield str(invalidInput(operation, self.username, fields,
747                                           err, e))
[579]748                    return
[602]749            import traceback
750            self.start('500 Internal Server Error',
751                       [('Content-Type', 'text/html')])
752            e = revertStandardError()
[603]753            s = show_error(operation, self.username, fields,
[602]754                           err, e, traceback.format_exc())
755            yield str(s)
756            return
[587]757        status = headers.setdefault('Status', '200 OK')
758        del headers['Status']
759        self.start(status, headers.items())
[579]760        yield output_string
[535]761        if fields.has_key('timedebug'):
[579]762            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
[209]763
[579]764def constructor():
[863]765    connect()
[579]766    return App
[535]767
[579]768def main():
769    from flup.server.fcgi_fork import WSGIServer
770    WSGIServer(constructor()).run()
[535]771
[579]772if __name__ == '__main__':
773    main()
Note: See TracBrowser for help on using the repository browser.