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

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

Port list to Mako

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