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

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

Move the help popup code into the templates where it belongs

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