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

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

Unauthenticated front page

  • 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 InvirtUnauthWeb(View):
57    @cherrypy.expose
58    @cherrypy.tools.mako(filename="/unauth.mako")
59    def index(self):
60        return {'simple': True}
61
62class InvirtWeb(View):
63    def __init__(self):
64        super(self.__class__,self).__init__()
65        connect()
66        self._cp_config['tools.require_login.on'] = True
67        self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
68                                                 'from invirt import database']
69
70    def __getattr__(self, name):
71        if name in ("admin", "overlord"):
72            if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz[0].cell):
73                raise InvalidInput('username', cherrypy.request.login,
74                                   'Not in admin group %s.' % config.adminacl)
75            cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
76            return self
77        else:
78            return super(InvirtWeb, self).__getattr__(name)
79
80    @cherrypy.expose
81    @cherrypy.tools.mako(filename="/list.mako")
82    def list(self, result=None):
83        """Handler for list requests."""
84        checkpoint.checkpoint('Getting list dict')
85        d = getListDict(cherrypy.request.login, cherrypy.request.state)
86        if result is not None:
87            d['result'] = result
88        checkpoint.checkpoint('Got list dict')
89        return d
90    index=list
91
92    @cherrypy.expose
93    @cherrypy.tools.mako(filename="/help.mako")
94    def help(self, subject=None, simple=False):
95        """Handler for help messages."""
96
97        help_mapping = {
98            'Autoinstalls': """
99The autoinstaller builds a minimal Debian or Ubuntu system to run as a
100ParaVM.  You can access the resulting system by logging into the <a
101href="help?simple=true&subject=ParaVM+Console">serial console server</a>
102with your Kerberos tickets; there is no root password so sshd will
103refuse login.</p>
104
105<p>Under the covers, the autoinstaller uses our own patched version of
106xen-create-image, which is a tool based on debootstrap.  If you log
107into the serial console while the install is running, you can watch
108it.
109""",
110            'ParaVM Console': """
111ParaVM machines do not support local console access over VNC.  To
112access the serial console of these machines, you can SSH with Kerberos
113to %s, using the name of the machine as your
114username.""" % config.console.hostname,
115            'HVM/ParaVM': """
116HVM machines use the virtualization features of the processor, while
117ParaVM machines rely on a modified kernel to communicate directly with
118the hypervisor.  HVMs support boot CDs of any operating system, and
119the VNC console applet.  The three-minute autoinstaller produces
120ParaVMs.  ParaVMs typically are more efficient, and always support the
121<a href="help?subject=ParaVM+Console">console server</a>.</p>
122
123<p>More details are <a
124href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
125wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
126(which you can skip by using the autoinstaller to begin with.)</p>
127
128<p>We recommend using a ParaVM when possible and an HVM when necessary.
129""",
130            'CPU Weight': """
131Don't ask us!  We're as mystified as you are.""",
132            'Owner': """
133The owner field is used to determine <a
134href="help?subject=Quotas">quotas</a>.  It must be the name of a
135locker that you are an AFS administrator of.  In particular, you or an
136AFS group you are a member of must have AFS rlidwka bits on the
137locker.  You can check who administers the LOCKER locker using the
138commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
139href="help?subject=Administrator">administrator</a>.""",
140            'Administrator': """
141The administrator field determines who can access the console and
142power on and off the machine.  This can be either a user or a moira
143group.""",
144            'Quotas': """
145Quotas are determined on a per-locker basis.  Each locker may have a
146maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
147active machines.""",
148            'Console': """
149<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
150setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
151your machine will run just fine, but the applet's display of the
152console will suffer artifacts.
153""",
154            'Windows': """
155<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>
156<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.
157"""
158            }
159
160        if not subject:
161            subject = sorted(help_mapping.keys())
162        if not isinstance(subject, list):
163            subject = [subject]
164
165        return dict(simple=simple,
166                    subjects=subject,
167                    mapping=help_mapping)
168    help._cp_config['tools.require_login.on'] = False
169
170    def parseCreate(self, fields):
171        kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
172        validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
173        return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
174                    disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
175                    cdrom=getattr(validate, 'cdrom', None),
176                    autoinstall=getattr(validate, 'autoinstall', None))
177
178    @cherrypy.expose
179    @cherrypy.tools.mako(filename="/list.mako")
180    @cherrypy.tools.require_POST()
181    def create(self, **fields):
182        """Handler for create requests."""
183        try:
184            parsed_fields = self.parseCreate(fields)
185            machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
186        except InvalidInput, err:
187            pass
188        else:
189            err = None
190        cherrypy.request.state.clear() #Changed global state
191        d = getListDict(cherrypy.request.login, cherrypy.request.state)
192        d['err'] = err
193        if err:
194            for field in fields.keys():
195                setattr(d['defaults'], field, fields.get(field))
196        else:
197            d['new_machine'] = parsed_fields['name']
198        return d
199
200    @cherrypy.expose
201    @cherrypy.tools.mako(filename="/helloworld.mako")
202    def helloworld(self, **kwargs):
203        return {'request': cherrypy.request, 'kwargs': kwargs}
204    helloworld._cp_config['tools.require_login.on'] = False
205
206    @cherrypy.expose
207    def errortest(self):
208        """Throw an error, to test the error-tracing mechanisms."""
209        raise RuntimeError("test of the emergency broadcast system")
210
211    class MachineView(View):
212        # This is hairy. Fix when CherryPy 3.2 is out. (rename to
213        # _cp_dispatch, and parse the argument as a list instead of
214        # string
215
216        def __getattr__(self, name):
217            try:
218                machine_id = int(name)
219                cherrypy.request.params['machine_id'] = machine_id
220                return self
221            except ValueError:
222                return None
223
224        @cherrypy.expose
225        @cherrypy.tools.mako(filename="/info.mako")
226        def info(self, machine_id):
227            """Handler for info on a single VM."""
228            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
229            d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
230            checkpoint.checkpoint('Got infodict')
231            return d
232        index = info
233
234        @cherrypy.expose
235        @cherrypy.tools.mako(filename="/info.mako")
236        @cherrypy.tools.require_POST()
237        def modify(self, machine_id, **fields):
238            """Handler for modifying attributes of a machine."""
239            try:
240                modify_dict = modifyDict(cherrypy.request.login, cherrypy.request.state, machine_id, fields)
241            except InvalidInput, err:
242                result = None
243                machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
244            else:
245                machine = modify_dict['machine']
246                result = 'Success!'
247                err = None
248            info_dict = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
249            info_dict['err'] = err
250            if err:
251                for field in fields.keys():
252                    setattr(info_dict['defaults'], field, fields.get(field))
253            info_dict['result'] = result
254            return info_dict
255
256        @cherrypy.expose
257        @cherrypy.tools.mako(filename="/vnc.mako")
258        def vnc(self, machine_id):
259            """VNC applet page.
260
261            Note that due to same-domain restrictions, the applet connects to
262            the webserver, which needs to forward those requests to the xen
263            server.  The Xen server runs another proxy that (1) authenticates
264            and (2) finds the correct port for the VM.
265
266            You might want iptables like:
267
268            -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
269            --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
270            -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
271            --dport 10003 -j SNAT --to-source 18.187.7.142
272            -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
273            --dport 10003 -j ACCEPT
274
275            Remember to enable iptables!
276            echo 1 > /proc/sys/net/ipv4/ip_forward
277            """
278            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
279
280            token = controls.vnctoken(machine)
281            host = controls.listHost(machine)
282            if host:
283                port = 10003 + [h.hostname for h in config.hosts].index(host)
284            else:
285                port = 5900 # dummy
286
287            status = controls.statusInfo(machine)
288            has_vnc = hasVnc(status)
289
290            d = dict(on=status,
291                     has_vnc=has_vnc,
292                     machine=machine,
293                     hostname=cherrypy.request.local.name,
294                     port=port,
295                     authtoken=token)
296            return d
297        @cherrypy.expose
298        @cherrypy.tools.mako(filename="/command.mako")
299        @cherrypy.tools.require_POST()
300        def command(self, command_name, machine_id, **kwargs):
301            """Handler for running commands like boot and delete on a VM."""
302            back = kwargs.get('back', None)
303            try:
304                d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
305                if d['command'] == 'Delete VM':
306                    back = 'list'
307            except InvalidInput, err:
308                if not back:
309                    raise
310                print >> sys.stderr, err
311                result = err
312            else:
313                result = 'Success!'
314                if not back:
315                    return d
316            if back == 'list':
317                cherrypy.request.state.clear() #Changed global state
318                raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
319            elif back == 'info':
320                raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
321            else:
322                raise InvalidInput('back', back, 'Not a known back page.')
323
324        atmulti = ajaxterm.Multiplex()
325        atsessions = {}
326        atsessions_lock = threading.Lock()
327
328        @cherrypy.expose
329        @cherrypy.tools.mako(filename="/terminal.mako")
330        def terminal(self, machine_id):
331            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
332
333            status = controls.statusInfo(machine)
334            has_vnc = hasVnc(status)
335
336            d = dict(on=status,
337                     has_vnc=has_vnc,
338                     machine=machine,
339                     hostname=cherrypy.request.local.name)
340            return d
341
342        @cherrypy.expose
343        @cherrypy.tools.require_POST()
344        @cherrypy.tools.gzip()
345        def at(self, machine_id, k=None, c=0, h=None):
346            machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
347            with self.atsessions_lock:
348                if machine_id in self.atsessions:
349                    term = self.atsessions[machine_id]
350                else:
351                    print >>sys.stderr, "spawning new session for terminal to ",machine_id
352                    invirt.remctl.checkKinit(principal='daemon/'+config.web.hostname)
353                    term = self.atmulti.create(
354                        ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
355                        )
356                    # Clear out old sessions when fd is reused
357                    for key in self.atsessions:
358                        if self.atsessions[key] == term:
359                            del self.atsessions[key]
360                    self.atsessions[machine_id] = term
361                if k:
362                    self.atmulti.proc_write(term,k)
363                time.sleep(0.002)
364                dump=self.atmulti.dump(term,c,h)
365                cherrypy.response.headers['Content-Type']='text/xml'
366                if isinstance(dump,str):
367                    return dump
368                else:
369                    print "Removing session for", machine_id,"because we received",repr(dump)
370                    del self.atsessions[machine_id]
371                    return '<?xml version="1.0"?><idem></idem>'
372
373    machine = MachineView()
374
375def pathSplit(path):
376    if path.startswith('/'):
377        path = path[1:]
378    i = path.find('/')
379    if i == -1:
380        i = len(path)
381    return path[:i], path[i:]
382
383class Checkpoint:
384    def __init__(self):
385        self.start_time = time.time()
386        self.checkpoints = []
387
388    def checkpoint(self, s):
389        self.checkpoints.append((s, time.time()))
390
391    def __str__(self):
392        return ('Timing info:\n%s\n' %
393                '\n'.join(['%s: %s' % (d, t - self.start_time) for
394                           (d, t) in self.checkpoints]))
395
396checkpoint = Checkpoint()
397
398def makeErrorPre(old, addition):
399    if addition is None:
400        return
401    if old:
402        return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
403    else:
404        return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
405
406Template.database = database
407Template.config = config
408Template.err = None
409
410class JsonDict:
411    """Class to store a dictionary that will be converted to JSON"""
412    def __init__(self, **kws):
413        self.data = kws
414        if 'err' in kws:
415            err = kws['err']
416            del kws['err']
417            self.addError(err)
418
419    def __str__(self):
420        return simplejson.dumps(self.data)
421
422    def addError(self, text):
423        """Add stderr text to be displayed on the website."""
424        self.data['err'] = \
425            makeErrorPre(self.data.get('err'), text)
426
427class Defaults:
428    """Class to store default values for fields."""
429    memory = 256
430    disk = 4.0
431    cdrom = ''
432    autoinstall = ''
433    name = ''
434    description = ''
435    administrator = ''
436    type = 'linux-hvm'
437
438    def __init__(self, max_memory=None, max_disk=None, **kws):
439        if max_memory is not None:
440            self.memory = min(self.memory, max_memory)
441        if max_disk is not None:
442            self.disk = min(self.disk, max_disk)
443        for key in kws:
444            setattr(self, key, kws[key])
445
446
447
448DEFAULT_HEADERS = {'Content-Type': 'text/html'}
449
450def invalidInput(op, username, fields, err, emsg):
451    """Print an error page when an InvalidInput exception occurs"""
452    d = dict(op=op, user=username, err_field=err.err_field,
453             err_value=str(err.err_value), stderr=emsg,
454             errorMessage=str(err))
455    return templates.invalid(searchList=[d])
456
457def hasVnc(status):
458    """Does the machine with a given status list support VNC?"""
459    if status is None:
460        return False
461    for l in status:
462        if l[0] == 'device' and l[1][0] == 'vfb':
463            d = dict(l[1][1:])
464            return 'location' in d
465    return False
466
467
468def getListDict(username, state):
469    """Gets the list of local variables used by list.tmpl."""
470    checkpoint.checkpoint('Starting')
471    machines = state.machines
472    checkpoint.checkpoint('Got my machines')
473    on = {}
474    has_vnc = {}
475    installing = {}
476    xmlist = state.xmlist
477    checkpoint.checkpoint('Got uptimes')
478    for m in machines:
479        if m not in xmlist:
480            has_vnc[m] = 'Off'
481            m.uptime = None
482        else:
483            m.uptime = xmlist[m]['uptime']
484            if xmlist[m]['console']:
485                has_vnc[m] = True
486            elif m.type.hvm:
487                has_vnc[m] = "WTF?"
488            else:
489                has_vnc[m] = "ParaVM"
490            if xmlist[m].get('autoinstall'):
491                installing[m] = True
492            else:
493                installing[m] = False
494    max_memory = validation.maxMemory(username, state)
495    max_disk = validation.maxDisk(username)
496    checkpoint.checkpoint('Got max mem/disk')
497    defaults = Defaults(max_memory=max_memory,
498                        max_disk=max_disk,
499                        owner=username)
500    checkpoint.checkpoint('Got defaults')
501    def sortkey(machine):
502        return (machine.owner != username, machine.owner, machine.name)
503    machines = sorted(machines, key=sortkey)
504    d = dict(user=username,
505             cant_add_vm=validation.cantAddVm(username, state),
506             max_memory=max_memory,
507             max_disk=max_disk,
508             defaults=defaults,
509             machines=machines,
510             has_vnc=has_vnc,
511             installing=installing)
512    return d
513
514def getHostname(nic):
515    """Find the hostname associated with a NIC.
516
517    XXX this should be merged with the similar logic in DNS and DHCP.
518    """
519    if nic.hostname:
520        hostname = nic.hostname
521    elif nic.machine:
522        hostname = nic.machine.name
523    else:
524        return None
525    if '.' in hostname:
526        return hostname
527    else:
528        return hostname + '.' + config.dns.domains[0]
529
530def getNicInfo(data_dict, machine):
531    """Helper function for info, get data on nics for a machine.
532
533    Modifies data_dict to include the relevant data, and returns a list
534    of (key, name) pairs to display "name: data_dict[key]" to the user.
535    """
536    data_dict['num_nics'] = len(machine.nics)
537    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
538                           ('nic%s_mac', 'NIC %s MAC Addr'),
539                           ('nic%s_ip', 'NIC %s IP'),
540                           ]
541    nic_fields = []
542    for i in range(len(machine.nics)):
543        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
544        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
545        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
546        data_dict['nic%s_ip' % i] = machine.nics[i].ip
547    if len(machine.nics) == 1:
548        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
549    return nic_fields
550
551def getDiskInfo(data_dict, machine):
552    """Helper function for info, get data on disks for a machine.
553
554    Modifies data_dict to include the relevant data, and returns a list
555    of (key, name) pairs to display "name: data_dict[key]" to the user.
556    """
557    data_dict['num_disks'] = len(machine.disks)
558    disk_fields_template = [('%s_size', '%s size')]
559    disk_fields = []
560    for disk in machine.disks:
561        name = disk.guest_device_name
562        disk_fields.extend([(x % name, y % name) for x, y in
563                            disk_fields_template])
564        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
565    return disk_fields
566
567def modifyDict(username, state, machine_id, fields):
568    """Modify a machine as specified by CGI arguments.
569
570    Return a dict containing the machine that was modified.
571    """
572    olddisk = {}
573    session.begin()
574    try:
575        kws = dict([(kw, fields.get(kw)) for kw in 'owner admin contact name description memory vmtype disksize'.split() if fields.get(kw)])
576        kws['machine_id'] = machine_id
577        validate = validation.Validate(username, state, **kws)
578        machine = validate.machine
579        oldname = machine.name
580
581        if hasattr(validate, 'memory'):
582            machine.memory = validate.memory
583
584        if hasattr(validate, 'vmtype'):
585            machine.type = validate.vmtype
586
587        if hasattr(validate, 'disksize'):
588            disksize = validate.disksize
589            disk = machine.disks[0]
590            if disk.size != disksize:
591                olddisk[disk.guest_device_name] = disksize
592                disk.size = disksize
593                session.save_or_update(disk)
594
595        update_acl = False
596        if hasattr(validate, 'owner') and validate.owner != machine.owner:
597            machine.owner = validate.owner
598            update_acl = True
599        if hasattr(validate, 'name'):
600            machine.name = validate.name
601            for n in machine.nics:
602                if n.hostname == oldname:
603                    n.hostname = validate.name
604        if hasattr(validate, 'description'):
605            machine.description = validate.description
606        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
607            machine.administrator = validate.admin
608            update_acl = True
609        if hasattr(validate, 'contact'):
610            machine.contact = validate.contact
611
612        session.save_or_update(machine)
613        if update_acl:
614            cache_acls.refreshMachine(machine)
615        session.commit()
616    except:
617        session.rollback()
618        raise
619    for diskname in olddisk:
620        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
621    if hasattr(validate, 'name'):
622        controls.renameMachine(machine, oldname, validate.name)
623    return dict(machine=machine)
624
625
626def badOperation(u, s, p, e):
627    """Function called when accessing an unknown URI."""
628    return ({'Status': '404 Not Found'}, 'Invalid operation.')
629
630def infoDict(username, state, machine):
631    """Get the variables used by info.tmpl."""
632    status = controls.statusInfo(machine)
633    checkpoint.checkpoint('Getting status info')
634    has_vnc = hasVnc(status)
635    if status is None:
636        main_status = dict(name=machine.name,
637                           memory=str(machine.memory))
638        uptime = None
639        cputime = None
640    else:
641        main_status = dict(status[1:])
642        main_status['host'] = controls.listHost(machine)
643        start_time = float(main_status.get('start_time', 0))
644        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
645        cpu_time_float = float(main_status.get('cpu_time', 0))
646        cputime = datetime.timedelta(seconds=int(cpu_time_float))
647    checkpoint.checkpoint('Status')
648    display_fields = [('name', 'Name'),
649                      ('description', 'Description'),
650                      ('owner', 'Owner'),
651                      ('administrator', 'Administrator'),
652                      ('contact', 'Contact'),
653                      ('type', 'Type'),
654                      'NIC_INFO',
655                      ('uptime', 'uptime'),
656                      ('cputime', 'CPU usage'),
657                      ('host', 'Hosted on'),
658                      ('memory', 'RAM'),
659                      'DISK_INFO',
660                      ('state', 'state (xen format)'),
661                      ]
662    fields = []
663    machine_info = {}
664    machine_info['name'] = machine.name
665    machine_info['description'] = machine.description
666    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
667    machine_info['owner'] = machine.owner
668    machine_info['administrator'] = machine.administrator
669    machine_info['contact'] = machine.contact
670
671    nic_fields = getNicInfo(machine_info, machine)
672    nic_point = display_fields.index('NIC_INFO')
673    display_fields = (display_fields[:nic_point] + nic_fields +
674                      display_fields[nic_point+1:])
675
676    disk_fields = getDiskInfo(machine_info, machine)
677    disk_point = display_fields.index('DISK_INFO')
678    display_fields = (display_fields[:disk_point] + disk_fields +
679                      display_fields[disk_point+1:])
680
681    main_status['memory'] += ' MiB'
682    for field, disp in display_fields:
683        if field in ('uptime', 'cputime') and locals()[field] is not None:
684            fields.append((disp, locals()[field]))
685        elif field in machine_info:
686            fields.append((disp, machine_info[field]))
687        elif field in main_status:
688            fields.append((disp, main_status[field]))
689        else:
690            pass
691            #fields.append((disp, None))
692
693    checkpoint.checkpoint('Got fields')
694
695
696    max_mem = validation.maxMemory(machine.owner, state, machine, False)
697    checkpoint.checkpoint('Got mem')
698    max_disk = validation.maxDisk(machine.owner, machine)
699    defaults = Defaults()
700    for name in 'machine_id name description administrator owner memory contact'.split():
701        if getattr(machine, name):
702            setattr(defaults, name, getattr(machine, name))
703    defaults.type = machine.type.type_id
704    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
705    checkpoint.checkpoint('Got defaults')
706    d = dict(user=username,
707             on=status is not None,
708             machine=machine,
709             defaults=defaults,
710             has_vnc=has_vnc,
711             uptime=str(uptime),
712             ram=machine.memory,
713             max_mem=max_mem,
714             max_disk=max_disk,
715             fields = fields)
716    return d
717
718mapping = dict()
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.