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

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

Upgrade info page to Mako

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