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

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

Use browser-based dupe suppression, so multiple clients can connect to the same terminal and not miss updates

  • Property svn:executable set to *
File size: 30.6 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
[2452]305        @cherrypy.tools.require_POST()
[2440]306        @cherrypy.tools.gzip()
[2454]307        def at(self, machine_id, k=None, c=0, h=None):
[2433]308            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
[2449]309            with self.atsessions_lock:
310                if machine_id in self.atsessions:
311                    term = self.atsessions[machine_id]
312                else:
313                    print >>sys.stderr, "spawning new session for terminal to ",machine_id
[2450]314                    term = self.atmulti.create(
[2449]315                        ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
316                        )
[2450]317                    # Clear out old sessions when fd is reused
318                    for key in self.atsessions:
319                        if self.atsessions[key] == term:
320                            del self.atsessions[key]
321                    self.atsessions[machine_id] = term
[2449]322                if k:
323                    self.atmulti.proc_write(term,k)
324                time.sleep(0.002)
[2454]325                dump=self.atmulti.dump(term,c,h)
[2449]326                cherrypy.response.headers['Content-Type']='text/xml'
327                if isinstance(dump,str):
328                    return dump
329                else:
330                    print "Removing session for", machine_id
331                    del self.atsessions[machine_id]
332                    return '<?xml version="1.0"?><idem></idem>'
[2433]333
[2413]334    machine = MachineView()
335
[632]336def pathSplit(path):
337    if path.startswith('/'):
338        path = path[1:]
339    i = path.find('/')
340    if i == -1:
341        i = len(path)
342    return path[:i], path[i:]
343
[235]344class Checkpoint:
345    def __init__(self):
346        self.start_time = time.time()
347        self.checkpoints = []
348
349    def checkpoint(self, s):
350        self.checkpoints.append((s, time.time()))
351
352    def __str__(self):
353        return ('Timing info:\n%s\n' %
354                '\n'.join(['%s: %s' % (d, t - self.start_time) for
355                           (d, t) in self.checkpoints]))
356
357checkpoint = Checkpoint()
358
[205]359def makeErrorPre(old, addition):
360    if addition is None:
361        return
362    if old:
363        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
364    else:
365        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
[139]366
[864]367Template.database = database
[866]368Template.config = config
[205]369Template.err = None
[139]370
[205]371class JsonDict:
372    """Class to store a dictionary that will be converted to JSON"""
373    def __init__(self, **kws):
374        self.data = kws
375        if 'err' in kws:
376            err = kws['err']
377            del kws['err']
378            self.addError(err)
[139]379
[205]380    def __str__(self):
381        return simplejson.dumps(self.data)
382
383    def addError(self, text):
384        """Add stderr text to be displayed on the website."""
385        self.data['err'] = \
386            makeErrorPre(self.data.get('err'), text)
387
388class Defaults:
389    """Class to store default values for fields."""
390    memory = 256
391    disk = 4.0
392    cdrom = ''
[443]393    autoinstall = ''
[205]394    name = ''
[609]395    description = ''
[515]396    type = 'linux-hvm'
397
[205]398    def __init__(self, max_memory=None, max_disk=None, **kws):
399        if max_memory is not None:
400            self.memory = min(self.memory, max_memory)
401        if max_disk is not None:
[1964]402            self.disk = min(self.disk, max_disk)
[205]403        for key in kws:
404            setattr(self, key, kws[key])
405
406
407
[209]408DEFAULT_HEADERS = {'Content-Type': 'text/html'}
[205]409
[572]410def invalidInput(op, username, fields, err, emsg):
[153]411    """Print an error page when an InvalidInput exception occurs"""
[572]412    d = dict(op=op, user=username, err_field=err.err_field,
[153]413             err_value=str(err.err_value), stderr=emsg,
414             errorMessage=str(err))
[235]415    return templates.invalid(searchList=[d])
[153]416
[119]417def hasVnc(status):
[133]418    """Does the machine with a given status list support VNC?"""
[119]419    if status is None:
420        return False
421    for l in status:
422        if l[0] == 'device' and l[1][0] == 'vfb':
423            d = dict(l[1][1:])
424            return 'location' in d
425    return False
426
[134]427
[572]428def getListDict(username, state):
[438]429    """Gets the list of local variables used by list.tmpl."""
[535]430    checkpoint.checkpoint('Starting')
[572]431    machines = state.machines
[235]432    checkpoint.checkpoint('Got my machines')
[133]433    on = {}
[119]434    has_vnc = {}
[2424]435    installing = {}
[572]436    xmlist = state.xmlist
[235]437    checkpoint.checkpoint('Got uptimes')
[136]438    for m in machines:
[535]439        if m not in xmlist:
[144]440            has_vnc[m] = 'Off'
[535]441            m.uptime = None
[136]442        else:
[535]443            m.uptime = xmlist[m]['uptime']
444            if xmlist[m]['console']:
445                has_vnc[m] = True
446            elif m.type.hvm:
447                has_vnc[m] = "WTF?"
448            else:
[2412]449                has_vnc[m] = "ParaVM"
[2424]450            if xmlist[m].get('autoinstall'):
451                installing[m] = True
452            else:
453                installing[m] = False
[572]454    max_memory = validation.maxMemory(username, state)
455    max_disk = validation.maxDisk(username)
[235]456    checkpoint.checkpoint('Got max mem/disk')
[205]457    defaults = Defaults(max_memory=max_memory,
458                        max_disk=max_disk,
[1739]459                        owner=username)
[235]460    checkpoint.checkpoint('Got defaults')
[424]461    def sortkey(machine):
[572]462        return (machine.owner != username, machine.owner, machine.name)
[424]463    machines = sorted(machines, key=sortkey)
[572]464    d = dict(user=username,
465             cant_add_vm=validation.cantAddVm(username, state),
[205]466             max_memory=max_memory,
[144]467             max_disk=max_disk,
[205]468             defaults=defaults,
[113]469             machines=machines,
[540]470             has_vnc=has_vnc,
[2424]471             installing=installing)
[205]472    return d
[113]473
[252]474def getHostname(nic):
[438]475    """Find the hostname associated with a NIC.
476
477    XXX this should be merged with the similar logic in DNS and DHCP.
478    """
[1976]479    if nic.hostname:
480        hostname = nic.hostname
[252]481    elif nic.machine:
[1976]482        hostname = nic.machine.name
[252]483    else:
484        return None
[1976]485    if '.' in hostname:
486        return hostname
487    else:
488        return hostname + '.' + config.dns.domains[0]
[252]489
[133]490def getNicInfo(data_dict, machine):
[145]491    """Helper function for info, get data on nics for a machine.
492
493    Modifies data_dict to include the relevant data, and returns a list
494    of (key, name) pairs to display "name: data_dict[key]" to the user.
495    """
[133]496    data_dict['num_nics'] = len(machine.nics)
[227]497    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
[133]498                           ('nic%s_mac', 'NIC %s MAC Addr'),
499                           ('nic%s_ip', 'NIC %s IP'),
500                           ]
501    nic_fields = []
502    for i in range(len(machine.nics)):
503        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
[1976]504        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
[133]505        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
506        data_dict['nic%s_ip' % i] = machine.nics[i].ip
507    if len(machine.nics) == 1:
508        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
509    return nic_fields
510
511def getDiskInfo(data_dict, machine):
[145]512    """Helper function for info, get data on disks for a machine.
513
514    Modifies data_dict to include the relevant data, and returns a list
515    of (key, name) pairs to display "name: data_dict[key]" to the user.
516    """
[133]517    data_dict['num_disks'] = len(machine.disks)
518    disk_fields_template = [('%s_size', '%s size')]
519    disk_fields = []
520    for disk in machine.disks:
521        name = disk.guest_device_name
[438]522        disk_fields.extend([(x % name, y % name) for x, y in
[205]523                            disk_fields_template])
[211]524        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
[133]525    return disk_fields
526
[572]527def modifyDict(username, state, fields):
[438]528    """Modify a machine as specified by CGI arguments.
529
530    Return a list of local variables for modify.tmpl.
531    """
[177]532    olddisk = {}
[1013]533    session.begin()
[161]534    try:
[609]535        kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
[572]536        validate = validation.Validate(username, state, **kws)
537        machine = validate.machine
[161]538        oldname = machine.name
[153]539
[572]540        if hasattr(validate, 'memory'):
541            machine.memory = validate.memory
[438]542
[572]543        if hasattr(validate, 'vmtype'):
544            machine.type = validate.vmtype
[440]545
[572]546        if hasattr(validate, 'disksize'):
547            disksize = validate.disksize
[177]548            disk = machine.disks[0]
549            if disk.size != disksize:
550                olddisk[disk.guest_device_name] = disksize
551                disk.size = disksize
[1013]552                session.save_or_update(disk)
[438]553
[446]554        update_acl = False
[572]555        if hasattr(validate, 'owner') and validate.owner != machine.owner:
556            machine.owner = validate.owner
[446]557            update_acl = True
[572]558        if hasattr(validate, 'name'):
[586]559            machine.name = validate.name
[1977]560            for n in machine.nics:
561                if n.hostname == oldname:
562                    n.hostname = validate.name
[609]563        if hasattr(validate, 'description'):
564            machine.description = validate.description
[572]565        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
566            machine.administrator = validate.admin
[446]567            update_acl = True
[572]568        if hasattr(validate, 'contact'):
569            machine.contact = validate.contact
[438]570
[1013]571        session.save_or_update(machine)
[446]572        if update_acl:
573            cache_acls.refreshMachine(machine)
[1013]574        session.commit()
[161]575    except:
[1013]576        session.rollback()
[163]577        raise
[177]578    for diskname in olddisk:
[209]579        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
[572]580    if hasattr(validate, 'name'):
581        controls.renameMachine(machine, oldname, validate.name)
582    return dict(user=username,
583                command="modify",
[205]584                machine=machine)
[438]585
[632]586def modify(username, state, path, fields):
[205]587    """Handler for modifying attributes of a machine."""
588    try:
[572]589        modify_dict = modifyDict(username, state, fields)
[205]590    except InvalidInput, err:
[207]591        result = None
[572]592        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
[205]593    else:
594        machine = modify_dict['machine']
[209]595        result = 'Success!'
[205]596        err = None
[585]597    info_dict = infoDict(username, state, machine)
[205]598    info_dict['err'] = err
599    if err:
600        for field in fields.keys():
601            setattr(info_dict['defaults'], field, fields.getfirst(field))
[207]602    info_dict['result'] = result
[235]603    return templates.info(searchList=[info_dict])
[161]604
[632]605def badOperation(u, s, p, e):
[438]606    """Function called when accessing an unknown URI."""
[607]607    return ({'Status': '404 Not Found'}, 'Invalid operation.')
[205]608
[579]609def infoDict(username, state, machine):
[438]610    """Get the variables used by info.tmpl."""
[209]611    status = controls.statusInfo(machine)
[235]612    checkpoint.checkpoint('Getting status info')
[133]613    has_vnc = hasVnc(status)
614    if status is None:
615        main_status = dict(name=machine.name,
616                           memory=str(machine.memory))
[205]617        uptime = None
618        cputime = None
[133]619    else:
620        main_status = dict(status[1:])
[662]621        main_status['host'] = controls.listHost(machine)
[167]622        start_time = float(main_status.get('start_time', 0))
623        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
624        cpu_time_float = float(main_status.get('cpu_time', 0))
625        cputime = datetime.timedelta(seconds=int(cpu_time_float))
[235]626    checkpoint.checkpoint('Status')
[133]627    display_fields = [('name', 'Name'),
[609]628                      ('description', 'Description'),
[133]629                      ('owner', 'Owner'),
[187]630                      ('administrator', 'Administrator'),
[133]631                      ('contact', 'Contact'),
[136]632                      ('type', 'Type'),
[133]633                      'NIC_INFO',
634                      ('uptime', 'uptime'),
635                      ('cputime', 'CPU usage'),
[662]636                      ('host', 'Hosted on'),
[133]637                      ('memory', 'RAM'),
638                      'DISK_INFO',
639                      ('state', 'state (xen format)'),
640                      ]
641    fields = []
642    machine_info = {}
[147]643    machine_info['name'] = machine.name
[609]644    machine_info['description'] = machine.description
[136]645    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
[133]646    machine_info['owner'] = machine.owner
[187]647    machine_info['administrator'] = machine.administrator
[133]648    machine_info['contact'] = machine.contact
649
650    nic_fields = getNicInfo(machine_info, machine)
651    nic_point = display_fields.index('NIC_INFO')
[438]652    display_fields = (display_fields[:nic_point] + nic_fields +
[205]653                      display_fields[nic_point+1:])
[133]654
655    disk_fields = getDiskInfo(machine_info, machine)
656    disk_point = display_fields.index('DISK_INFO')
[438]657    display_fields = (display_fields[:disk_point] + disk_fields +
[205]658                      display_fields[disk_point+1:])
[438]659
[211]660    main_status['memory'] += ' MiB'
[133]661    for field, disp in display_fields:
[167]662        if field in ('uptime', 'cputime') and locals()[field] is not None:
[133]663            fields.append((disp, locals()[field]))
[147]664        elif field in machine_info:
665            fields.append((disp, machine_info[field]))
[133]666        elif field in main_status:
667            fields.append((disp, main_status[field]))
668        else:
669            pass
670            #fields.append((disp, None))
[235]671
672    checkpoint.checkpoint('Got fields')
673
674
[572]675    max_mem = validation.maxMemory(machine.owner, state, machine, False)
[235]676    checkpoint.checkpoint('Got mem')
[566]677    max_disk = validation.maxDisk(machine.owner, machine)
[209]678    defaults = Defaults()
[609]679    for name in 'machine_id name description administrator owner memory contact'.split():
[205]680        setattr(defaults, name, getattr(machine, name))
[516]681    defaults.type = machine.type.type_id
[205]682    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
[235]683    checkpoint.checkpoint('Got defaults')
[572]684    d = dict(user=username,
[133]685             on=status is not None,
686             machine=machine,
[205]687             defaults=defaults,
[133]688             has_vnc=has_vnc,
689             uptime=str(uptime),
690             ram=machine.memory,
[144]691             max_mem=max_mem,
692             max_disk=max_disk,
[133]693             fields = fields)
[205]694    return d
[113]695
[632]696def unauthFront(_, _2, _3, fields):
[510]697    """Information for unauth'd users."""
[2182]698    return templates.unauth(searchList=[{'simple' : True, 
[2185]699            'hostname' : socket.getfqdn()}])
[510]700
[867]701def admin(username, state, path, fields):
[633]702    if path == '':
703        return ({'Status': '303 See Other',
[867]704                 'Location': 'admin/'},
[633]705                "You shouldn't see this message.")
[2217]706    if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
[867]707        raise InvalidInput('username', username,
[2217]708                           'Not in admin group %s.' % config.adminacl)
[867]709    newstate = State(username, isadmin=True)
[632]710    newstate.environ = state.environ
711    return handler(username, newstate, path, fields)
712
[2414]713mapping = dict(
[133]714               modify=modify,
[598]715               unauth=unauthFront,
[867]716               admin=admin,
[2428]717               overlord=admin)
[113]718
[205]719def printHeaders(headers):
[438]720    """Print a dictionary as HTTP headers."""
[205]721    for key, value in headers.iteritems():
722        print '%s: %s' % (key, value)
723    print
724
[598]725def send_error_mail(subject, body):
726    import subprocess
[205]727
[863]728    to = config.web.errormail
[598]729    mail = """To: %s
[863]730From: root@%s
[598]731Subject: %s
732
733%s
[863]734""" % (to, config.web.hostname, subject, body)
[1718]735    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
736                         stdin=subprocess.PIPE)
[598]737    p.stdin.write(mail)
738    p.stdin.close()
739    p.wait()
740
[603]741def show_error(op, username, fields, err, emsg, traceback):
742    """Print an error page when an exception occurs"""
743    d = dict(op=op, user=username, fields=fields,
744             errorMessage=str(err), stderr=emsg, traceback=traceback)
745    details = templates.error_raw(searchList=[d])
[1103]746    exclude = config.web.errormail_exclude
747    if username not in exclude and '*' not in exclude:
[627]748        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
749                        details)
[603]750    d['details'] = details
751    return templates.error(searchList=[d])
752
[632]753def handler(username, state, path, fields):
754    operation, path = pathSplit(path)
755    if not operation:
756        operation = 'list'
757    print 'Starting', operation
758    fun = mapping.get(operation, badOperation)
759    return fun(username, state, path, fields)
760
[579]761class App:
762    def __init__(self, environ, start_response):
763        self.environ = environ
764        self.start = start_response
[205]765
[579]766        self.username = getUser(environ)
767        self.state = State(self.username)
[581]768        self.state.environ = environ
[205]769
[634]770        random.seed() #sigh
771
[579]772    def __iter__(self):
[632]773        start_time = time.time()
[864]774        database.clear_cache()
[600]775        sys.stderr = StringIO()
[579]776        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
777        operation = self.environ.get('PATH_INFO', '')
778        if not operation:
[633]779            self.start("301 Moved Permanently", [('Location', './')])
[579]780            return
781        if self.username is None:
782            operation = 'unauth'
783
784        try:
785            checkpoint.checkpoint('Before')
[632]786            output = handler(self.username, self.state, operation, fields)
[579]787            checkpoint.checkpoint('After')
788
789            headers = dict(DEFAULT_HEADERS)
790            if isinstance(output, tuple):
791                new_headers, output = output
792                headers.update(new_headers)
793            e = revertStandardError()
794            if e:
[693]795                if hasattr(output, 'addError'):
796                    output.addError(e)
797                else:
798                    # This only happens on redirects, so it'd be a pain to get
799                    # the message to the user.  Maybe in the response is useful.
800                    output = output + '\n\nstderr:\n' + e
[579]801            output_string =  str(output)
802            checkpoint.checkpoint('output as a string')
803        except Exception, err:
804            if not fields.has_key('js'):
805                if isinstance(err, InvalidInput):
806                    self.start('200 OK', [('Content-Type', 'text/html')])
807                    e = revertStandardError()
[603]808                    yield str(invalidInput(operation, self.username, fields,
809                                           err, e))
[579]810                    return
[602]811            import traceback
812            self.start('500 Internal Server Error',
813                       [('Content-Type', 'text/html')])
814            e = revertStandardError()
[603]815            s = show_error(operation, self.username, fields,
[602]816                           err, e, traceback.format_exc())
817            yield str(s)
818            return
[587]819        status = headers.setdefault('Status', '200 OK')
820        del headers['Status']
821        self.start(status, headers.items())
[579]822        yield output_string
[535]823        if fields.has_key('timedebug'):
[579]824            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
[209]825
[579]826def constructor():
[863]827    connect()
[579]828    return App
[535]829
[579]830def main():
831    from flup.server.fcgi_fork import WSGIServer
832    WSGIServer(constructor()).run()
[535]833
[579]834if __name__ == '__main__':
835    main()
Note: See TracBrowser for help on using the repository browser.