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

Last change on this file since 2674 was 2674, checked in by broder, 14 years ago

Use global imports for Mako templates

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