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

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

Port vnc page to Mako and CherryPy?

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