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

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

Move machine-related pages into a REST-style URL.

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