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

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

Allow reconnecting to the same terminal session

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