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

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

Clear stale fds out of cache in ajaxterm

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