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

Last change on this file since 1339 was 1318, checked in by price, 16 years ago

sipb-xen-www -> invirt-web

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