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

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

Fix typo

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