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

Last change on this file since 2449 was 2449, checked in by ecprice, 15 years ago

Fix race condition in ajaxterm

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