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

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

Implement administrator mode

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