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

Last change on this file since 2676 was 2676, checked in by broder, 14 years ago

Implement help handler

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