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

Last change on this file since 2691 was 2691, checked in by broder, 14 years ago

Make modify form work

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