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
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="/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
269        @cherrypy.expose
270        @cherrypy.tools.mako(filename="/command.mako")
271        @cherrypy.tools.require_POST()
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.')
295
296        atmulti = ajaxterm.Multiplex()
297        atsessions = {}
298        atsessions_lock = threading.Lock()
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
314        @cherrypy.expose
315        @cherrypy.tools.require_POST()
316        @cherrypy.tools.gzip()
317        def at(self, machine_id, k=None, c=0, h=None):
318            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
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
324                    invirt.remctl.checkKinit(principal='daemon/'+config.web.hostname)
325                    term = self.atmulti.create(
326                        ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
327                        )
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
333                if k:
334                    self.atmulti.proc_write(term,k)
335                time.sleep(0.002)
336                dump=self.atmulti.dump(term,c,h)
337                cherrypy.response.headers['Content-Type']='text/xml'
338                if isinstance(dump,str):
339                    return dump
340                else:
341                    print "Removing session for", machine_id,"because we received",repr(dump)
342                    del self.atsessions[machine_id]
343                    return '<?xml version="1.0"?><idem></idem>'
344
345    machine = MachineView()
346
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
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
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>'
377
378Template.database = database
379Template.config = config
380Template.err = None
381
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)
390
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 = ''
404    autoinstall = ''
405    name = ''
406    description = ''
407    type = 'linux-hvm'
408
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:
413            self.disk = min(self.disk, max_disk)
414        for key in kws:
415            setattr(self, key, kws[key])
416
417
418
419DEFAULT_HEADERS = {'Content-Type': 'text/html'}
420
421def invalidInput(op, username, fields, err, emsg):
422    """Print an error page when an InvalidInput exception occurs"""
423    d = dict(op=op, user=username, err_field=err.err_field,
424             err_value=str(err.err_value), stderr=emsg,
425             errorMessage=str(err))
426    return templates.invalid(searchList=[d])
427
428def hasVnc(status):
429    """Does the machine with a given status list support VNC?"""
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
438
439def getListDict(username, state):
440    """Gets the list of local variables used by list.tmpl."""
441    checkpoint.checkpoint('Starting')
442    machines = state.machines
443    checkpoint.checkpoint('Got my machines')
444    on = {}
445    has_vnc = {}
446    installing = {}
447    xmlist = state.xmlist
448    checkpoint.checkpoint('Got uptimes')
449    for m in machines:
450        if m not in xmlist:
451            has_vnc[m] = 'Off'
452            m.uptime = None
453        else:
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:
460                has_vnc[m] = "ParaVM"
461            if xmlist[m].get('autoinstall'):
462                installing[m] = True
463            else:
464                installing[m] = False
465    max_memory = validation.maxMemory(username, state)
466    max_disk = validation.maxDisk(username)
467    checkpoint.checkpoint('Got max mem/disk')
468    defaults = Defaults(max_memory=max_memory,
469                        max_disk=max_disk,
470                        owner=username)
471    checkpoint.checkpoint('Got defaults')
472    def sortkey(machine):
473        return (machine.owner != username, machine.owner, machine.name)
474    machines = sorted(machines, key=sortkey)
475    d = dict(user=username,
476             cant_add_vm=validation.cantAddVm(username, state),
477             max_memory=max_memory,
478             max_disk=max_disk,
479             defaults=defaults,
480             machines=machines,
481             has_vnc=has_vnc,
482             installing=installing)
483    return d
484
485def getHostname(nic):
486    """Find the hostname associated with a NIC.
487
488    XXX this should be merged with the similar logic in DNS and DHCP.
489    """
490    if nic.hostname:
491        hostname = nic.hostname
492    elif nic.machine:
493        hostname = nic.machine.name
494    else:
495        return None
496    if '.' in hostname:
497        return hostname
498    else:
499        return hostname + '.' + config.dns.domains[0]
500
501def getNicInfo(data_dict, machine):
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    """
507    data_dict['num_nics'] = len(machine.nics)
508    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
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])
515        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
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):
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    """
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
533        disk_fields.extend([(x % name, y % name) for x, y in
534                            disk_fields_template])
535        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
536    return disk_fields
537
538def modifyDict(username, state, fields):
539    """Modify a machine as specified by CGI arguments.
540
541    Return a list of local variables for modify.tmpl.
542    """
543    olddisk = {}
544    session.begin()
545    try:
546        kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
547        validate = validation.Validate(username, state, **kws)
548        machine = validate.machine
549        oldname = machine.name
550
551        if hasattr(validate, 'memory'):
552            machine.memory = validate.memory
553
554        if hasattr(validate, 'vmtype'):
555            machine.type = validate.vmtype
556
557        if hasattr(validate, 'disksize'):
558            disksize = validate.disksize
559            disk = machine.disks[0]
560            if disk.size != disksize:
561                olddisk[disk.guest_device_name] = disksize
562                disk.size = disksize
563                session.save_or_update(disk)
564
565        update_acl = False
566        if hasattr(validate, 'owner') and validate.owner != machine.owner:
567            machine.owner = validate.owner
568            update_acl = True
569        if hasattr(validate, 'name'):
570            machine.name = validate.name
571            for n in machine.nics:
572                if n.hostname == oldname:
573                    n.hostname = validate.name
574        if hasattr(validate, 'description'):
575            machine.description = validate.description
576        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
577            machine.administrator = validate.admin
578            update_acl = True
579        if hasattr(validate, 'contact'):
580            machine.contact = validate.contact
581
582        session.save_or_update(machine)
583        if update_acl:
584            cache_acls.refreshMachine(machine)
585        session.commit()
586    except:
587        session.rollback()
588        raise
589    for diskname in olddisk:
590        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
591    if hasattr(validate, 'name'):
592        controls.renameMachine(machine, oldname, validate.name)
593    return dict(user=username,
594                command="modify",
595                machine=machine)
596
597def modify(username, state, path, fields):
598    """Handler for modifying attributes of a machine."""
599    try:
600        modify_dict = modifyDict(username, state, fields)
601    except InvalidInput, err:
602        result = None
603        machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
604    else:
605        machine = modify_dict['machine']
606        result = 'Success!'
607        err = None
608    info_dict = infoDict(username, state, machine)
609    info_dict['err'] = err
610    if err:
611        for field in fields.keys():
612            setattr(info_dict['defaults'], field, fields.getfirst(field))
613    info_dict['result'] = result
614    return templates.info(searchList=[info_dict])
615
616def badOperation(u, s, p, e):
617    """Function called when accessing an unknown URI."""
618    return ({'Status': '404 Not Found'}, 'Invalid operation.')
619
620def infoDict(username, state, machine):
621    """Get the variables used by info.tmpl."""
622    status = controls.statusInfo(machine)
623    checkpoint.checkpoint('Getting status info')
624    has_vnc = hasVnc(status)
625    if status is None:
626        main_status = dict(name=machine.name,
627                           memory=str(machine.memory))
628        uptime = None
629        cputime = None
630    else:
631        main_status = dict(status[1:])
632        main_status['host'] = controls.listHost(machine)
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))
637    checkpoint.checkpoint('Status')
638    display_fields = [('name', 'Name'),
639                      ('description', 'Description'),
640                      ('owner', 'Owner'),
641                      ('administrator', 'Administrator'),
642                      ('contact', 'Contact'),
643                      ('type', 'Type'),
644                      'NIC_INFO',
645                      ('uptime', 'uptime'),
646                      ('cputime', 'CPU usage'),
647                      ('host', 'Hosted on'),
648                      ('memory', 'RAM'),
649                      'DISK_INFO',
650                      ('state', 'state (xen format)'),
651                      ]
652    fields = []
653    machine_info = {}
654    machine_info['name'] = machine.name
655    machine_info['description'] = machine.description
656    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
657    machine_info['owner'] = machine.owner
658    machine_info['administrator'] = machine.administrator
659    machine_info['contact'] = machine.contact
660
661    nic_fields = getNicInfo(machine_info, machine)
662    nic_point = display_fields.index('NIC_INFO')
663    display_fields = (display_fields[:nic_point] + nic_fields +
664                      display_fields[nic_point+1:])
665
666    disk_fields = getDiskInfo(machine_info, machine)
667    disk_point = display_fields.index('DISK_INFO')
668    display_fields = (display_fields[:disk_point] + disk_fields +
669                      display_fields[disk_point+1:])
670
671    main_status['memory'] += ' MiB'
672    for field, disp in display_fields:
673        if field in ('uptime', 'cputime') and locals()[field] is not None:
674            fields.append((disp, locals()[field]))
675        elif field in machine_info:
676            fields.append((disp, machine_info[field]))
677        elif field in main_status:
678            fields.append((disp, main_status[field]))
679        else:
680            pass
681            #fields.append((disp, None))
682
683    checkpoint.checkpoint('Got fields')
684
685
686    max_mem = validation.maxMemory(machine.owner, state, machine, False)
687    checkpoint.checkpoint('Got mem')
688    max_disk = validation.maxDisk(machine.owner, machine)
689    defaults = Defaults()
690    for name in 'machine_id name description administrator owner memory contact'.split():
691        setattr(defaults, name, getattr(machine, name))
692    defaults.type = machine.type.type_id
693    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
694    checkpoint.checkpoint('Got defaults')
695    d = dict(user=username,
696             on=status is not None,
697             machine=machine,
698             defaults=defaults,
699             has_vnc=has_vnc,
700             uptime=str(uptime),
701             ram=machine.memory,
702             max_mem=max_mem,
703             max_disk=max_disk,
704             fields = fields)
705    return d
706
707def unauthFront(_, _2, _3, fields):
708    """Information for unauth'd users."""
709    return templates.unauth(searchList=[{'simple' : True, 
710            'hostname' : socket.getfqdn()}])
711
712mapping = dict(
713               modify=modify,
714               unauth=unauthFront)
715
716def printHeaders(headers):
717    """Print a dictionary as HTTP headers."""
718    for key, value in headers.iteritems():
719        print '%s: %s' % (key, value)
720    print
721
722def send_error_mail(subject, body):
723    import subprocess
724
725    to = config.web.errormail
726    mail = """To: %s
727From: root@%s
728Subject: %s
729
730%s
731""" % (to, config.web.hostname, subject, body)
732    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
733                         stdin=subprocess.PIPE)
734    p.stdin.write(mail)
735    p.stdin.close()
736    p.wait()
737
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])
743    exclude = config.web.errormail_exclude
744    if username not in exclude and '*' not in exclude:
745        send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
746                        details)
747    d['details'] = details
748    return templates.error(searchList=[d])
749
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
758class App:
759    def __init__(self, environ, start_response):
760        self.environ = environ
761        self.start = start_response
762
763        self.username = getUser(environ)
764        self.state = State(self.username)
765        self.state.environ = environ
766
767        random.seed() #sigh
768
769    def __iter__(self):
770        start_time = time.time()
771        database.clear_cache()
772        sys.stderr = StringIO()
773        fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
774        operation = self.environ.get('PATH_INFO', '')
775        if not operation:
776            self.start("301 Moved Permanently", [('Location', './')])
777            return
778        if self.username is None:
779            operation = 'unauth'
780
781        try:
782            checkpoint.checkpoint('Before')
783            output = handler(self.username, self.state, operation, fields)
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:
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
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()
805                    yield str(invalidInput(operation, self.username, fields,
806                                           err, e))
807                    return
808            import traceback
809            self.start('500 Internal Server Error',
810                       [('Content-Type', 'text/html')])
811            e = revertStandardError()
812            s = show_error(operation, self.username, fields,
813                           err, e, traceback.format_exc())
814            yield str(s)
815            return
816        status = headers.setdefault('Status', '200 OK')
817        del headers['Status']
818        self.start(status, headers.items())
819        yield output_string
820        if fields.has_key('timedebug'):
821            yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
822
823def constructor():
824    connect()
825    return App
826
827def main():
828    from flup.server.fcgi_fork import WSGIServer
829    WSGIServer(constructor()).run()
830
831if __name__ == '__main__':
832    main()
Note: See TracBrowser for help on using the repository browser.