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

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

Make modify form work

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