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
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
[2186]15import socket
[2663]16import cherrypy
[205]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):
[599]22        return ''
[205]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
[235]36import templates
[113]37from Cheetah.Template import Template
[209]38import validation
[446]39import cache_acls
[1612]40from webcommon import State
[209]41import controls
[632]42from getafsgroups import getAfsGroupMembers
[865]43from invirt import database
[1001]44from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
[863]45from invirt.config import structs as config
[1612]46from invirt.common import InvalidInput, CodeError
[113]47
[2664]48from view import View
49
50class InvirtWeb(View):
51    def __init__(self):
52        super(self.__class__,self).__init__()
53        connect()
[2665]54        self._cp_config['tools.require_login.on'] = True
[2674]55        self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
56                                                 'from invirt import database']
[2664]57
[2690]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)
[2674]67
[2664]68    @cherrypy.expose
[2665]69    @cherrypy.tools.mako(filename="/list.mako")
[2684]70    def list(self, result=None):
[2665]71        """Handler for list requests."""
72        checkpoint.checkpoint('Getting list dict')
[2669]73        d = getListDict(cherrypy.request.login, cherrypy.request.state)
[2684]74        if result is not None:
75            d['result'] = result
[2665]76        checkpoint.checkpoint('Got list dict')
[2668]77        return d
[2665]78    index=list
79
80    @cherrypy.expose
[2676]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>
[2686]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.
[2676]145"""
146            }
147
148        if not subject:
149            subject = sorted(help_mapping.keys())
150        if not isinstance(subject, list):
151            subject = [subject]
152
[2677]153        return dict(simple=simple,
[2676]154                    subjects=subject,
155                    mapping=help_mapping)
156    help._cp_config['tools.require_login.on'] = False
157
[2685]158    def parseCreate(self, fields):
[2687]159        kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
[2685]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
[2676]166    @cherrypy.expose
[2685]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
[2665]189    @cherrypy.tools.mako(filename="/helloworld.mako")
[2675]190    def helloworld(self, **kwargs):
191        return {'request': cherrypy.request, 'kwargs': kwargs}
[2665]192    helloworld._cp_config['tools.require_login.on'] = False
[2664]193
[2689]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
[2678]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
[2680]222        @cherrypy.expose
[2691]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
[2680]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
[2684]285        @cherrypy.expose
286        @cherrypy.tools.mako(filename="/command.mako")
[2685]287        @cherrypy.tools.require_POST()
[2684]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.')
[2680]311
[2678]312    machine = MachineView()
313
[632]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
[235]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
[205]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>'
[139]344
[864]345Template.database = database
[866]346Template.config = config
[205]347Template.err = None
[139]348
[205]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)
[139]357
[205]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 = ''
[443]371    autoinstall = ''
[205]372    name = ''
[609]373    description = ''
[2691]374    administrator = ''
[515]375    type = 'linux-hvm'
376
[205]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:
[1964]381            self.disk = min(self.disk, max_disk)
[205]382        for key in kws:
383            setattr(self, key, kws[key])
384
385
386
[209]387DEFAULT_HEADERS = {'Content-Type': 'text/html'}
[205]388
[572]389def invalidInput(op, username, fields, err, emsg):
[153]390    """Print an error page when an InvalidInput exception occurs"""
[572]391    d = dict(op=op, user=username, err_field=err.err_field,
[153]392             err_value=str(err.err_value), stderr=emsg,
393             errorMessage=str(err))
[235]394    return templates.invalid(searchList=[d])
[153]395
[119]396def hasVnc(status):
[133]397    """Does the machine with a given status list support VNC?"""
[119]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
[134]406
[572]407def getListDict(username, state):
[438]408    """Gets the list of local variables used by list.tmpl."""
[535]409    checkpoint.checkpoint('Starting')
[572]410    machines = state.machines
[235]411    checkpoint.checkpoint('Got my machines')
[133]412    on = {}
[119]413    has_vnc = {}
[2687]414    installing = {}
[572]415    xmlist = state.xmlist
[235]416    checkpoint.checkpoint('Got uptimes')
[136]417    for m in machines:
[535]418        if m not in xmlist:
[144]419            has_vnc[m] = 'Off'
[535]420            m.uptime = None
[136]421        else:
[535]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:
[2679]428                has_vnc[m] = "ParaVM"
[2687]429            if xmlist[m].get('autoinstall'):
430                installing[m] = True
431            else:
432                installing[m] = False
[572]433    max_memory = validation.maxMemory(username, state)
434    max_disk = validation.maxDisk(username)
[235]435    checkpoint.checkpoint('Got max mem/disk')
[205]436    defaults = Defaults(max_memory=max_memory,
437                        max_disk=max_disk,
[1739]438                        owner=username)
[235]439    checkpoint.checkpoint('Got defaults')
[424]440    def sortkey(machine):
[572]441        return (machine.owner != username, machine.owner, machine.name)
[424]442    machines = sorted(machines, key=sortkey)
[572]443    d = dict(user=username,
444             cant_add_vm=validation.cantAddVm(username, state),
[205]445             max_memory=max_memory,
[144]446             max_disk=max_disk,
[205]447             defaults=defaults,
[113]448             machines=machines,
[540]449             has_vnc=has_vnc,
[2687]450             installing=installing)
[205]451    return d
[113]452
[252]453def getHostname(nic):
[438]454    """Find the hostname associated with a NIC.
455
456    XXX this should be merged with the similar logic in DNS and DHCP.
457    """
[1976]458    if nic.hostname:
459        hostname = nic.hostname
[252]460    elif nic.machine:
[1976]461        hostname = nic.machine.name
[252]462    else:
463        return None
[1976]464    if '.' in hostname:
465        return hostname
466    else:
467        return hostname + '.' + config.dns.domains[0]
[252]468
[133]469def getNicInfo(data_dict, machine):
[145]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    """
[133]475    data_dict['num_nics'] = len(machine.nics)
[227]476    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
[133]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])
[1976]483        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
[133]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):
[145]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    """
[133]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
[438]501        disk_fields.extend([(x % name, y % name) for x, y in
[205]502                            disk_fields_template])
[211]503        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
[133]504    return disk_fields
505
[2691]506def modifyDict(username, state, machine_id, fields):
[438]507    """Modify a machine as specified by CGI arguments.
508
[2691]509    Return a dict containing the machine that was modified.
[438]510    """
[177]511    olddisk = {}
[1013]512    session.begin()
[161]513    try:
[2691]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
[572]516        validate = validation.Validate(username, state, **kws)
517        machine = validate.machine
[161]518        oldname = machine.name
[153]519
[572]520        if hasattr(validate, 'memory'):
521            machine.memory = validate.memory
[438]522
[572]523        if hasattr(validate, 'vmtype'):
524            machine.type = validate.vmtype
[440]525
[572]526        if hasattr(validate, 'disksize'):
527            disksize = validate.disksize
[177]528            disk = machine.disks[0]
529            if disk.size != disksize:
530                olddisk[disk.guest_device_name] = disksize
531                disk.size = disksize
[1013]532                session.save_or_update(disk)
[438]533
[446]534        update_acl = False
[572]535        if hasattr(validate, 'owner') and validate.owner != machine.owner:
536            machine.owner = validate.owner
[446]537            update_acl = True
[572]538        if hasattr(validate, 'name'):
[586]539            machine.name = validate.name
[1977]540            for n in machine.nics:
541                if n.hostname == oldname:
542                    n.hostname = validate.name
[609]543        if hasattr(validate, 'description'):
544            machine.description = validate.description
[572]545        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
546            machine.administrator = validate.admin
[446]547            update_acl = True
[572]548        if hasattr(validate, 'contact'):
549            machine.contact = validate.contact
[438]550
[1013]551        session.save_or_update(machine)
[446]552        if update_acl:
553            cache_acls.refreshMachine(machine)
[1013]554        session.commit()
[161]555    except:
[1013]556        session.rollback()
[163]557        raise
[177]558    for diskname in olddisk:
[209]559        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
[572]560    if hasattr(validate, 'name'):
561        controls.renameMachine(machine, oldname, validate.name)
[2691]562    return dict(machine=machine)
[438]563
[161]564
[632]565def badOperation(u, s, p, e):
[438]566    """Function called when accessing an unknown URI."""
[607]567    return ({'Status': '404 Not Found'}, 'Invalid operation.')
[205]568
[579]569def infoDict(username, state, machine):
[438]570    """Get the variables used by info.tmpl."""
[209]571    status = controls.statusInfo(machine)
[235]572    checkpoint.checkpoint('Getting status info')
[133]573    has_vnc = hasVnc(status)
574    if status is None:
575        main_status = dict(name=machine.name,
576                           memory=str(machine.memory))
[205]577        uptime = None
578        cputime = None
[133]579    else:
580        main_status = dict(status[1:])
[662]581        main_status['host'] = controls.listHost(machine)
[167]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))
[235]586    checkpoint.checkpoint('Status')
[133]587    display_fields = [('name', 'Name'),
[609]588                      ('description', 'Description'),
[133]589                      ('owner', 'Owner'),
[187]590                      ('administrator', 'Administrator'),
[133]591                      ('contact', 'Contact'),
[136]592                      ('type', 'Type'),
[133]593                      'NIC_INFO',
594                      ('uptime', 'uptime'),
595                      ('cputime', 'CPU usage'),
[662]596                      ('host', 'Hosted on'),
[133]597                      ('memory', 'RAM'),
598                      'DISK_INFO',
599                      ('state', 'state (xen format)'),
600                      ]
601    fields = []
602    machine_info = {}
[147]603    machine_info['name'] = machine.name
[609]604    machine_info['description'] = machine.description
[136]605    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
[133]606    machine_info['owner'] = machine.owner
[187]607    machine_info['administrator'] = machine.administrator
[133]608    machine_info['contact'] = machine.contact
609
610    nic_fields = getNicInfo(machine_info, machine)
611    nic_point = display_fields.index('NIC_INFO')
[438]612    display_fields = (display_fields[:nic_point] + nic_fields +
[205]613                      display_fields[nic_point+1:])
[133]614
615    disk_fields = getDiskInfo(machine_info, machine)
616    disk_point = display_fields.index('DISK_INFO')
[438]617    display_fields = (display_fields[:disk_point] + disk_fields +
[205]618                      display_fields[disk_point+1:])
[438]619
[211]620    main_status['memory'] += ' MiB'
[133]621    for field, disp in display_fields:
[167]622        if field in ('uptime', 'cputime') and locals()[field] is not None:
[133]623            fields.append((disp, locals()[field]))
[147]624        elif field in machine_info:
625            fields.append((disp, machine_info[field]))
[133]626        elif field in main_status:
627            fields.append((disp, main_status[field]))
628        else:
629            pass
630            #fields.append((disp, None))
[235]631
632    checkpoint.checkpoint('Got fields')
633
634
[572]635    max_mem = validation.maxMemory(machine.owner, state, machine, False)
[235]636    checkpoint.checkpoint('Got mem')
[566]637    max_disk = validation.maxDisk(machine.owner, machine)
[209]638    defaults = Defaults()
[609]639    for name in 'machine_id name description administrator owner memory contact'.split():
[2691]640        if getattr(machine, name):
641            setattr(defaults, name, getattr(machine, name))
[516]642    defaults.type = machine.type.type_id
[205]643    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
[235]644    checkpoint.checkpoint('Got defaults')
[572]645    d = dict(user=username,
[133]646             on=status is not None,
647             machine=machine,
[205]648             defaults=defaults,
[133]649             has_vnc=has_vnc,
650             uptime=str(uptime),
651             ram=machine.memory,
[144]652             max_mem=max_mem,
653             max_disk=max_disk,
[133]654             fields = fields)
[205]655    return d
[113]656
[632]657def unauthFront(_, _2, _3, fields):
[510]658    """Information for unauth'd users."""
[2182]659    return templates.unauth(searchList=[{'simple' : True, 
[2185]660            'hostname' : socket.getfqdn()}])
[510]661
[2680]662mapping = dict(
[2690]663               unauth=unauthFront)
[113]664
[205]665def printHeaders(headers):
[438]666    """Print a dictionary as HTTP headers."""
[205]667    for key, value in headers.iteritems():
668        print '%s: %s' % (key, value)
669    print
670
[598]671def send_error_mail(subject, body):
672    import subprocess
[205]673
[863]674    to = config.web.errormail
[598]675    mail = """To: %s
[863]676From: root@%s
[598]677Subject: %s
678
679%s
[863]680""" % (to, config.web.hostname, subject, body)
[1718]681    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
682                         stdin=subprocess.PIPE)
[598]683    p.stdin.write(mail)
684    p.stdin.close()
685    p.wait()
686
[603]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])
[1103]692    exclude = config.web.errormail_exclude
693    if username not in exclude and '*' not in exclude:
[627]694        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
695                        details)
[603]696    d['details'] = details
697    return templates.error(searchList=[d])
698
[632]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
[579]707class App:
708    def __init__(self, environ, start_response):
709        self.environ = environ
710        self.start = start_response
[205]711
[579]712        self.username = getUser(environ)
713        self.state = State(self.username)
[581]714        self.state.environ = environ
[205]715
[634]716        random.seed() #sigh
717
[579]718    def __iter__(self):
[632]719        start_time = time.time()
[864]720        database.clear_cache()
[600]721        sys.stderr = StringIO()
[579]722        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
723        operation = self.environ.get('PATH_INFO', '')
724        if not operation:
[633]725            self.start("301 Moved Permanently", [('Location', './')])
[579]726            return
727        if self.username is None:
728            operation = 'unauth'
729
730        try:
731            checkpoint.checkpoint('Before')
[632]732            output = handler(self.username, self.state, operation, fields)
[579]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:
[693]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
[579]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()
[603]754                    yield str(invalidInput(operation, self.username, fields,
755                                           err, e))
[579]756                    return
[602]757            import traceback
758            self.start('500 Internal Server Error',
759                       [('Content-Type', 'text/html')])
760            e = revertStandardError()
[603]761            s = show_error(operation, self.username, fields,
[602]762                           err, e, traceback.format_exc())
763            yield str(s)
764            return
[587]765        status = headers.setdefault('Status', '200 OK')
766        del headers['Status']
767        self.start(status, headers.items())
[579]768        yield output_string
[535]769        if fields.has_key('timedebug'):
[579]770            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
[209]771
[579]772def constructor():
[863]773    connect()
[579]774    return App
[535]775
[579]776def main():
777    from flup.server.fcgi_fork import WSGIServer
778    WSGIServer(constructor()).run()
[535]779
[579]780if __name__ == '__main__':
781    main()
Note: See TracBrowser for help on using the repository browser.