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

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

Add terminal page

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