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

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

Connect to the database on init

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