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

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

Show hostnames for every NIC

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