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

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

Compress ajaxterm redraws with gzip, to reduce latency

  • Property svn:executable set to *
File size: 30.1 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        @cherrypy.tools.gzip()
301        def at(self, machine_id, k=None, c=0, force=0):
302            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
303            if machine_id in self.atsessions:
304                term = self.atsessions[machine_id]
305            else:
306                print >>sys.stderr, "spawning new session for terminal to ",machine_id
307                term = self.atsessions[machine_id] = self.atmulti.create(
308                    ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
309                    )
310            if k:
311                self.atmulti.proc_write(term,k)
312            time.sleep(0.002)
313            dump=self.atmulti.dump(term,c,int(force))
314            cherrypy.response.headers['Content-Type']='text/xml'
315            if isinstance(dump,str):
316                return dump
317            else:
318                print "Removing session for", machine_id
319                del self.atsessions[machine_id]
320                return '<?xml version="1.0"?><idem></idem>'
321
322    machine = MachineView()
323
324def pathSplit(path):
325    if path.startswith('/'):
326        path = path[1:]
327    i = path.find('/')
328    if i == -1:
329        i = len(path)
330    return path[:i], path[i:]
331
332class Checkpoint:
333    def __init__(self):
334        self.start_time = time.time()
335        self.checkpoints = []
336
337    def checkpoint(self, s):
338        self.checkpoints.append((s, time.time()))
339
340    def __str__(self):
341        return ('Timing info:\n%s\n' %
342                '\n'.join(['%s: %s' % (d, t - self.start_time) for
343                           (d, t) in self.checkpoints]))
344
345checkpoint = Checkpoint()
346
347def makeErrorPre(old, addition):
348    if addition is None:
349        return
350    if old:
351        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
352    else:
353        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
354
355Template.database = database
356Template.config = config
357Template.err = None
358
359class JsonDict:
360    """Class to store a dictionary that will be converted to JSON"""
361    def __init__(self, **kws):
362        self.data = kws
363        if 'err' in kws:
364            err = kws['err']
365            del kws['err']
366            self.addError(err)
367
368    def __str__(self):
369        return simplejson.dumps(self.data)
370
371    def addError(self, text):
372        """Add stderr text to be displayed on the website."""
373        self.data['err'] = \
374            makeErrorPre(self.data.get('err'), text)
375
376class Defaults:
377    """Class to store default values for fields."""
378    memory = 256
379    disk = 4.0
380    cdrom = ''
381    autoinstall = ''
382    name = ''
383    description = ''
384    type = 'linux-hvm'
385
386    def __init__(self, max_memory=None, max_disk=None, **kws):
387        if max_memory is not None:
388            self.memory = min(self.memory, max_memory)
389        if max_disk is not None:
390            self.disk = min(self.disk, max_disk)
391        for key in kws:
392            setattr(self, key, kws[key])
393
394
395
396DEFAULT_HEADERS = {'Content-Type': 'text/html'}
397
398def invalidInput(op, username, fields, err, emsg):
399    """Print an error page when an InvalidInput exception occurs"""
400    d = dict(op=op, user=username, err_field=err.err_field,
401             err_value=str(err.err_value), stderr=emsg,
402             errorMessage=str(err))
403    return templates.invalid(searchList=[d])
404
405def hasVnc(status):
406    """Does the machine with a given status list support VNC?"""
407    if status is None:
408        return False
409    for l in status:
410        if l[0] == 'device' and l[1][0] == 'vfb':
411            d = dict(l[1][1:])
412            return 'location' in d
413    return False
414
415
416def getListDict(username, state):
417    """Gets the list of local variables used by list.tmpl."""
418    checkpoint.checkpoint('Starting')
419    machines = state.machines
420    checkpoint.checkpoint('Got my machines')
421    on = {}
422    has_vnc = {}
423    installing = {}
424    xmlist = state.xmlist
425    checkpoint.checkpoint('Got uptimes')
426    for m in machines:
427        if m not in xmlist:
428            has_vnc[m] = 'Off'
429            m.uptime = None
430        else:
431            m.uptime = xmlist[m]['uptime']
432            if xmlist[m]['console']:
433                has_vnc[m] = True
434            elif m.type.hvm:
435                has_vnc[m] = "WTF?"
436            else:
437                has_vnc[m] = "ParaVM"
438            if xmlist[m].get('autoinstall'):
439                installing[m] = True
440            else:
441                installing[m] = False
442    max_memory = validation.maxMemory(username, state)
443    max_disk = validation.maxDisk(username)
444    checkpoint.checkpoint('Got max mem/disk')
445    defaults = Defaults(max_memory=max_memory,
446                        max_disk=max_disk,
447                        owner=username)
448    checkpoint.checkpoint('Got defaults')
449    def sortkey(machine):
450        return (machine.owner != username, machine.owner, machine.name)
451    machines = sorted(machines, key=sortkey)
452    d = dict(user=username,
453             cant_add_vm=validation.cantAddVm(username, state),
454             max_memory=max_memory,
455             max_disk=max_disk,
456             defaults=defaults,
457             machines=machines,
458             has_vnc=has_vnc,
459             installing=installing)
460    return d
461
462def getHostname(nic):
463    """Find the hostname associated with a NIC.
464
465    XXX this should be merged with the similar logic in DNS and DHCP.
466    """
467    if nic.hostname:
468        hostname = nic.hostname
469    elif nic.machine:
470        hostname = nic.machine.name
471    else:
472        return None
473    if '.' in hostname:
474        return hostname
475    else:
476        return hostname + '.' + config.dns.domains[0]
477
478def getNicInfo(data_dict, machine):
479    """Helper function for info, get data on nics for a machine.
480
481    Modifies data_dict to include the relevant data, and returns a list
482    of (key, name) pairs to display "name: data_dict[key]" to the user.
483    """
484    data_dict['num_nics'] = len(machine.nics)
485    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
486                           ('nic%s_mac', 'NIC %s MAC Addr'),
487                           ('nic%s_ip', 'NIC %s IP'),
488                           ]
489    nic_fields = []
490    for i in range(len(machine.nics)):
491        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
492        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
493        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
494        data_dict['nic%s_ip' % i] = machine.nics[i].ip
495    if len(machine.nics) == 1:
496        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
497    return nic_fields
498
499def getDiskInfo(data_dict, machine):
500    """Helper function for info, get data on disks for a machine.
501
502    Modifies data_dict to include the relevant data, and returns a list
503    of (key, name) pairs to display "name: data_dict[key]" to the user.
504    """
505    data_dict['num_disks'] = len(machine.disks)
506    disk_fields_template = [('%s_size', '%s size')]
507    disk_fields = []
508    for disk in machine.disks:
509        name = disk.guest_device_name
510        disk_fields.extend([(x % name, y % name) for x, y in
511                            disk_fields_template])
512        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
513    return disk_fields
514
515def modifyDict(username, state, fields):
516    """Modify a machine as specified by CGI arguments.
517
518    Return a list of local variables for modify.tmpl.
519    """
520    olddisk = {}
521    session.begin()
522    try:
523        kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
524        validate = validation.Validate(username, state, **kws)
525        machine = validate.machine
526        oldname = machine.name
527
528        if hasattr(validate, 'memory'):
529            machine.memory = validate.memory
530
531        if hasattr(validate, 'vmtype'):
532            machine.type = validate.vmtype
533
534        if hasattr(validate, 'disksize'):
535            disksize = validate.disksize
536            disk = machine.disks[0]
537            if disk.size != disksize:
538                olddisk[disk.guest_device_name] = disksize
539                disk.size = disksize
540                session.save_or_update(disk)
541
542        update_acl = False
543        if hasattr(validate, 'owner') and validate.owner != machine.owner:
544            machine.owner = validate.owner
545            update_acl = True
546        if hasattr(validate, 'name'):
547            machine.name = validate.name
548            for n in machine.nics:
549                if n.hostname == oldname:
550                    n.hostname = validate.name
551        if hasattr(validate, 'description'):
552            machine.description = validate.description
553        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
554            machine.administrator = validate.admin
555            update_acl = True
556        if hasattr(validate, 'contact'):
557            machine.contact = validate.contact
558
559        session.save_or_update(machine)
560        if update_acl:
561            cache_acls.refreshMachine(machine)
562        session.commit()
563    except:
564        session.rollback()
565        raise
566    for diskname in olddisk:
567        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
568    if hasattr(validate, 'name'):
569        controls.renameMachine(machine, oldname, validate.name)
570    return dict(user=username,
571                command="modify",
572                machine=machine)
573
574def modify(username, state, path, fields):
575    """Handler for modifying attributes of a machine."""
576    try:
577        modify_dict = modifyDict(username, state, fields)
578    except InvalidInput, err:
579        result = None
580        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
581    else:
582        machine = modify_dict['machine']
583        result = 'Success!'
584        err = None
585    info_dict = infoDict(username, state, machine)
586    info_dict['err'] = err
587    if err:
588        for field in fields.keys():
589            setattr(info_dict['defaults'], field, fields.getfirst(field))
590    info_dict['result'] = result
591    return templates.info(searchList=[info_dict])
592
593def badOperation(u, s, p, e):
594    """Function called when accessing an unknown URI."""
595    return ({'Status': '404 Not Found'}, 'Invalid operation.')
596
597def infoDict(username, state, machine):
598    """Get the variables used by info.tmpl."""
599    status = controls.statusInfo(machine)
600    checkpoint.checkpoint('Getting status info')
601    has_vnc = hasVnc(status)
602    if status is None:
603        main_status = dict(name=machine.name,
604                           memory=str(machine.memory))
605        uptime = None
606        cputime = None
607    else:
608        main_status = dict(status[1:])
609        main_status['host'] = controls.listHost(machine)
610        start_time = float(main_status.get('start_time', 0))
611        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
612        cpu_time_float = float(main_status.get('cpu_time', 0))
613        cputime = datetime.timedelta(seconds=int(cpu_time_float))
614    checkpoint.checkpoint('Status')
615    display_fields = [('name', 'Name'),
616                      ('description', 'Description'),
617                      ('owner', 'Owner'),
618                      ('administrator', 'Administrator'),
619                      ('contact', 'Contact'),
620                      ('type', 'Type'),
621                      'NIC_INFO',
622                      ('uptime', 'uptime'),
623                      ('cputime', 'CPU usage'),
624                      ('host', 'Hosted on'),
625                      ('memory', 'RAM'),
626                      'DISK_INFO',
627                      ('state', 'state (xen format)'),
628                      ]
629    fields = []
630    machine_info = {}
631    machine_info['name'] = machine.name
632    machine_info['description'] = machine.description
633    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
634    machine_info['owner'] = machine.owner
635    machine_info['administrator'] = machine.administrator
636    machine_info['contact'] = machine.contact
637
638    nic_fields = getNicInfo(machine_info, machine)
639    nic_point = display_fields.index('NIC_INFO')
640    display_fields = (display_fields[:nic_point] + nic_fields +
641                      display_fields[nic_point+1:])
642
643    disk_fields = getDiskInfo(machine_info, machine)
644    disk_point = display_fields.index('DISK_INFO')
645    display_fields = (display_fields[:disk_point] + disk_fields +
646                      display_fields[disk_point+1:])
647
648    main_status['memory'] += ' MiB'
649    for field, disp in display_fields:
650        if field in ('uptime', 'cputime') and locals()[field] is not None:
651            fields.append((disp, locals()[field]))
652        elif field in machine_info:
653            fields.append((disp, machine_info[field]))
654        elif field in main_status:
655            fields.append((disp, main_status[field]))
656        else:
657            pass
658            #fields.append((disp, None))
659
660    checkpoint.checkpoint('Got fields')
661
662
663    max_mem = validation.maxMemory(machine.owner, state, machine, False)
664    checkpoint.checkpoint('Got mem')
665    max_disk = validation.maxDisk(machine.owner, machine)
666    defaults = Defaults()
667    for name in 'machine_id name description administrator owner memory contact'.split():
668        setattr(defaults, name, getattr(machine, name))
669    defaults.type = machine.type.type_id
670    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
671    checkpoint.checkpoint('Got defaults')
672    d = dict(user=username,
673             on=status is not None,
674             machine=machine,
675             defaults=defaults,
676             has_vnc=has_vnc,
677             uptime=str(uptime),
678             ram=machine.memory,
679             max_mem=max_mem,
680             max_disk=max_disk,
681             fields = fields)
682    return d
683
684def unauthFront(_, _2, _3, fields):
685    """Information for unauth'd users."""
686    return templates.unauth(searchList=[{'simple' : True, 
687            'hostname' : socket.getfqdn()}])
688
689def admin(username, state, path, fields):
690    if path == '':
691        return ({'Status': '303 See Other',
692                 'Location': 'admin/'},
693                "You shouldn't see this message.")
694    if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
695        raise InvalidInput('username', username,
696                           'Not in admin group %s.' % config.adminacl)
697    newstate = State(username, isadmin=True)
698    newstate.environ = state.environ
699    return handler(username, newstate, path, fields)
700
701mapping = dict(
702               modify=modify,
703               unauth=unauthFront,
704               admin=admin,
705               overlord=admin)
706
707def printHeaders(headers):
708    """Print a dictionary as HTTP headers."""
709    for key, value in headers.iteritems():
710        print '%s: %s' % (key, value)
711    print
712
713def send_error_mail(subject, body):
714    import subprocess
715
716    to = config.web.errormail
717    mail = """To: %s
718From: root@%s
719Subject: %s
720
721%s
722""" % (to, config.web.hostname, subject, body)
723    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
724                         stdin=subprocess.PIPE)
725    p.stdin.write(mail)
726    p.stdin.close()
727    p.wait()
728
729def show_error(op, username, fields, err, emsg, traceback):
730    """Print an error page when an exception occurs"""
731    d = dict(op=op, user=username, fields=fields,
732             errorMessage=str(err), stderr=emsg, traceback=traceback)
733    details = templates.error_raw(searchList=[d])
734    exclude = config.web.errormail_exclude
735    if username not in exclude and '*' not in exclude:
736        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
737                        details)
738    d['details'] = details
739    return templates.error(searchList=[d])
740
741def handler(username, state, path, fields):
742    operation, path = pathSplit(path)
743    if not operation:
744        operation = 'list'
745    print 'Starting', operation
746    fun = mapping.get(operation, badOperation)
747    return fun(username, state, path, fields)
748
749class App:
750    def __init__(self, environ, start_response):
751        self.environ = environ
752        self.start = start_response
753
754        self.username = getUser(environ)
755        self.state = State(self.username)
756        self.state.environ = environ
757
758        random.seed() #sigh
759
760    def __iter__(self):
761        start_time = time.time()
762        database.clear_cache()
763        sys.stderr = StringIO()
764        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
765        operation = self.environ.get('PATH_INFO', '')
766        if not operation:
767            self.start("301 Moved Permanently", [('Location', './')])
768            return
769        if self.username is None:
770            operation = 'unauth'
771
772        try:
773            checkpoint.checkpoint('Before')
774            output = handler(self.username, self.state, operation, fields)
775            checkpoint.checkpoint('After')
776
777            headers = dict(DEFAULT_HEADERS)
778            if isinstance(output, tuple):
779                new_headers, output = output
780                headers.update(new_headers)
781            e = revertStandardError()
782            if e:
783                if hasattr(output, 'addError'):
784                    output.addError(e)
785                else:
786                    # This only happens on redirects, so it'd be a pain to get
787                    # the message to the user.  Maybe in the response is useful.
788                    output = output + '\n\nstderr:\n' + e
789            output_string =  str(output)
790            checkpoint.checkpoint('output as a string')
791        except Exception, err:
792            if not fields.has_key('js'):
793                if isinstance(err, InvalidInput):
794                    self.start('200 OK', [('Content-Type', 'text/html')])
795                    e = revertStandardError()
796                    yield str(invalidInput(operation, self.username, fields,
797                                           err, e))
798                    return
799            import traceback
800            self.start('500 Internal Server Error',
801                       [('Content-Type', 'text/html')])
802            e = revertStandardError()
803            s = show_error(operation, self.username, fields,
804                           err, e, traceback.format_exc())
805            yield str(s)
806            return
807        status = headers.setdefault('Status', '200 OK')
808        del headers['Status']
809        self.start(status, headers.items())
810        yield output_string
811        if fields.has_key('timedebug'):
812            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
813
814def constructor():
815    connect()
816    return App
817
818def main():
819    from flup.server.fcgi_fork import WSGIServer
820    WSGIServer(constructor()).run()
821
822if __name__ == '__main__':
823    main()
Note: See TracBrowser for help on using the repository browser.