source: trunk/packages/invirt-web/code/main.py @ 1559

Last change on this file since 1559 was 1469, checked in by broder, 16 years ago

When getting the authtoken in the web interface, call remctl from the
right place

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