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

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

Clarify that staff can receive Windows license keys from MIT

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