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

Last change on this file since 2594 was 2217, checked in by broder, 16 years ago

Use the newly globalized adminacl instead of the old web.adminacl.

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