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

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

Ensure Kerberos tickets get passed to the ssh that ajaxterm spawns

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