Changeset 2737 for trunk/packages


Ignore:
Timestamp:
Dec 20, 2009, 11:27:10 PM (15 years ago)
Author:
broder
Message:

Merge cherrypy-rebased branch of invirt-web into trunk.

Location:
trunk/packages/invirt-web
Files:
15 deleted
12 edited
19 copied

Legend:

Unmodified
Added
Removed
  • trunk/packages/invirt-web

  • trunk/packages/invirt-web/code/Makefile

    r2231 r2737  
    1 DIRS = templates
    2 
    3 all: kill chmod compile
     1all: kill chmod
    42
    53chmod:
     
    97kill:
    108        -pkill main.fcgi
    11 
    12 compile:
    13         for dir in $(DIRS); do \
    14                 (cd $$dir; $(MAKE) all); \
    15         done
    16 
    17 clean:
    18         for dir in $(DIRS); do \
    19                 (cd $$dir; $(MAKE) clean); \
    20         done
  • trunk/packages/invirt-web/code/controls.py

    r2295 r2737  
    205205        raise
    206206
    207 def commandResult(username, state, fields):
     207def commandResult(username, state, command_name, machine_id, fields):
    208208    start_time = 0
    209     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
    210     action = fields.getfirst('action')
    211     cdrom = fields.getfirst('cdrom')
     209    machine = validation.Validate(username, state, machine_id=machine_id).machine
     210    action = command_name
     211    cdrom = fields.get('cdrom') or None
    212212    if cdrom is not None and not CDROM.query().filter_by(cdrom_id=cdrom).one():
    213213        raise CodeError("Invalid cdrom type '%s'" % cdrom)   
    214     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown',
    215                       'Delete VM'):
     214    if action not in "reboot create destroy shutdown delete".split(" "):
    216215        raise CodeError("Invalid action '%s'" % action)
    217     if action == 'Reboot':
     216    if action == 'reboot':
    218217        if cdrom is not None:
    219218            out, err = remctl('control', machine.name, 'reboot', cdrom,
     
    231230                raise CodeError('ERROR on remctl')
    232231               
    233     elif action == 'Power on':
     232    elif action == 'create':
    234233        if validation.maxMemory(username, state, machine) < machine.memory:
    235234            raise InvalidInput('action', 'Power on',
     
    237236                               "to turn on this machine.")
    238237        bootMachine(machine, cdrom)
    239     elif action == 'Power off':
     238    elif action == 'destroy':
    240239        out, err = remctl('control', machine.name, 'destroy', err=True)
    241240        if err:
     
    247246                print >> sys.stderr, err
    248247                raise CodeError('ERROR on remctl')
    249     elif action == 'Shutdown':
     248    elif action == 'shutdown':
    250249        out, err = remctl('control', machine.name, 'shutdown', err=True)
    251250        if err:
     
    257256                print >> sys.stderr, err
    258257                raise CodeError('ERROR on remctl')
    259     elif action == 'Delete VM':
     258    elif action == 'delete':
    260259        deleteVM(machine)
    261260
  • trunk/packages/invirt-web/code/getafsgroups.py

    r2590 r2737  
    3434        if c.cell == cell and hasattr(c, 'auth'):
    3535            encrypt = c.auth
    36     subprocess.check_call(['aklog', cell], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
     36    if encrypt:
     37        subprocess.check_call(['aklog', cell], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    3738    p = subprocess.Popen(["pts", "membership", "-encrypt" if encrypt else '-noauth', group, '-c', cell],
    3839                         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  • trunk/packages/invirt-web/code/main.py

    r2217 r2737  
    77import datetime
    88import hmac
     9import os
    910import random
    1011import sha
    11 import simplejson
    1212import sys
    1313import time
    1414import urllib
    1515import socket
     16import cherrypy
     17from cherrypy import _cperror
    1618from StringIO import StringIO
    17 
    18 def revertStandardError():
    19     """Move stderr to stdout, and return the contents of the old stderr."""
    20     errio = sys.stderr
    21     if not isinstance(errio, StringIO):
    22         return ''
    23     sys.stderr = sys.stdout
    24     errio.seek(0)
    25     return errio.read()
    2619
    2720def printError():
     
    3427    atexit.register(printError)
    3528
    36 import templates
    37 from Cheetah.Template import Template
    3829import validation
    3930import cache_acls
     
    4637from invirt.common import InvalidInput, CodeError
    4738
    48 def pathSplit(path):
    49     if path.startswith('/'):
    50         path = path[1:]
    51     i = path.find('/')
    52     if i == -1:
    53         i = len(path)
    54     return path[:i], path[i:]
    55 
    56 class Checkpoint:
     39from view import View, revertStandardError
     40
     41
     42static_dir = os.path.join(os.path.dirname(__file__), 'static')
     43InvirtStatic = cherrypy.tools.staticdir.handler(
     44    root=static_dir,
     45    dir=static_dir,
     46    section='/static')
     47
     48class InvirtUnauthWeb(View):
     49    static = InvirtStatic
     50
     51    @cherrypy.expose
     52    @cherrypy.tools.mako(filename="/unauth.mako")
     53    def index(self):
     54        return {'simple': True}
     55
     56class InvirtWeb(View):
    5757    def __init__(self):
    58         self.start_time = time.time()
    59         self.checkpoints = []
    60 
    61     def checkpoint(self, s):
    62         self.checkpoints.append((s, time.time()))
    63 
    64     def __str__(self):
    65         return ('Timing info:\n%s\n' %
    66                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
    67                            (d, t) in self.checkpoints]))
    68 
    69 checkpoint = Checkpoint()
    70 
    71 def jquote(string):
    72     return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
    73 
    74 def helppopup(subj):
    75     """Return HTML code for a (?) link to a specified help topic"""
    76     return ('<span class="helplink"><a href="help?' +
    77             cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
    78             +'" target="_blank" ' +
    79             'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
    80 
    81 def makeErrorPre(old, addition):
    82     if addition is None:
    83         return
    84     if old:
    85         return old[:-6]  + '\n----\n' + str(addition) + '</pre>'
    86     else:
    87         return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
    88 
    89 Template.database = database
    90 Template.config = config
    91 Template.helppopup = staticmethod(helppopup)
    92 Template.err = None
    93 
    94 class JsonDict:
    95     """Class to store a dictionary that will be converted to JSON"""
    96     def __init__(self, **kws):
    97         self.data = kws
    98         if 'err' in kws:
    99             err = kws['err']
    100             del kws['err']
    101             self.addError(err)
    102 
    103     def __str__(self):
    104         return simplejson.dumps(self.data)
    105 
    106     def addError(self, text):
    107         """Add stderr text to be displayed on the website."""
    108         self.data['err'] = \
    109             makeErrorPre(self.data.get('err'), text)
     58        super(self.__class__,self).__init__()
     59        connect()
     60        self._cp_config['tools.require_login.on'] = True
     61        self._cp_config['tools.catch_stderr.on'] = True
     62        self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
     63                                                 'from invirt import database']
     64        self._cp_config['request.error_response'] = self.handle_error
     65
     66    static = InvirtStatic
     67
     68    @cherrypy.expose
     69    @cherrypy.tools.mako(filename="/invalid.mako")
     70    def invalidInput(self):
     71        """Print an error page when an InvalidInput exception occurs"""
     72        err = cherrypy.request.prev.params["err"]
     73        emsg = cherrypy.request.prev.params["emsg"]
     74        d = dict(err_field=err.err_field,
     75                 err_value=str(err.err_value), stderr=emsg,
     76                 errorMessage=str(err))
     77        return d
     78
     79    @cherrypy.expose
     80    @cherrypy.tools.mako(filename="/error.mako")
     81    def error(self):
     82        """Print an error page when an exception occurs"""
     83        op = cherrypy.request.prev.path_info
     84        username = cherrypy.request.login
     85        err = cherrypy.request.prev.params["err"]
     86        emsg = cherrypy.request.prev.params["emsg"]
     87        traceback = cherrypy.request.prev.params["traceback"]
     88        d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
     89                 errorMessage=str(err), stderr=emsg, traceback=traceback)
     90        error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
     91        details = error_raw.render(**d)
     92        exclude = config.web.errormail_exclude
     93        if username not in exclude and '*' not in exclude:
     94            send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
     95                            details)
     96        d['details'] = details
     97        return d
     98
     99    def __getattr__(self, name):
     100        if name in ("admin", "overlord"):
     101            if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
     102                raise InvalidInput('username', cherrypy.request.login,
     103                                   'Not in admin group %s.' % config.adminacl)
     104            cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
     105            return self
     106        else:
     107            return super(InvirtWeb, self).__getattr__(name)
     108
     109    def handle_error(self):
     110        err = sys.exc_info()[1]
     111        if isinstance(err, InvalidInput):
     112            cherrypy.request.params['err'] = err
     113            cherrypy.request.params['emsg'] = revertStandardError()
     114            raise cherrypy.InternalRedirect('/invalidInput')
     115        if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
     116            cherrypy.request.params['err'] = err
     117            cherrypy.request.params['emsg'] = revertStandardError()
     118            cherrypy.request.params['traceback'] = _cperror.format_exc()
     119            raise cherrypy.InternalRedirect('/error')
     120        # fall back to cherrypy default error page
     121        cherrypy.HTTPError(500).set_response()
     122
     123    @cherrypy.expose
     124    @cherrypy.tools.mako(filename="/list.mako")
     125    def list(self, result=None):
     126        """Handler for list requests."""
     127        d = getListDict(cherrypy.request.login, cherrypy.request.state)
     128        if result is not None:
     129            d['result'] = result
     130        return d
     131    index=list
     132
     133    @cherrypy.expose
     134    @cherrypy.tools.mako(filename="/help.mako")
     135    def help(self, subject=None, simple=False):
     136        """Handler for help messages."""
     137
     138        help_mapping = {
     139            'Autoinstalls': """
     140The autoinstaller builds a minimal Debian or Ubuntu system to run as a
     141ParaVM.  You can access the resulting system by logging into the <a
     142href="help?simple=true&subject=ParaVM+Console">serial console server</a>
     143with your Kerberos tickets; there is no root password so sshd will
     144refuse login.</p>
     145
     146<p>Under the covers, the autoinstaller uses our own patched version of
     147xen-create-image, which is a tool based on debootstrap.  If you log
     148into the serial console while the install is running, you can watch
     149it.
     150""",
     151            'ParaVM Console': """
     152ParaVM machines do not support local console access over VNC.  To
     153access the serial console of these machines, you can SSH with Kerberos
     154to %s, using the name of the machine as your
     155username.""" % config.console.hostname,
     156            'HVM/ParaVM': """
     157HVM machines use the virtualization features of the processor, while
     158ParaVM machines rely on a modified kernel to communicate directly with
     159the hypervisor.  HVMs support boot CDs of any operating system, and
     160the VNC console applet.  The three-minute autoinstaller produces
     161ParaVMs.  ParaVMs typically are more efficient, and always support the
     162<a href="help?subject=ParaVM+Console">console server</a>.</p>
     163
     164<p>More details are <a
     165href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
     166wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
     167(which you can skip by using the autoinstaller to begin with.)</p>
     168
     169<p>We recommend using a ParaVM when possible and an HVM when necessary.
     170""",
     171            'CPU Weight': """
     172Don't ask us!  We're as mystified as you are.""",
     173            'Owner': """
     174The owner field is used to determine <a
     175href="help?subject=Quotas">quotas</a>.  It must be the name of a
     176locker that you are an AFS administrator of.  In particular, you or an
     177AFS group you are a member of must have AFS rlidwka bits on the
     178locker.  You can check who administers the LOCKER locker using the
     179commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
     180href="help?subject=Administrator">administrator</a>.""",
     181            'Administrator': """
     182The administrator field determines who can access the console and
     183power on and off the machine.  This can be either a user or a moira
     184group.""",
     185            'Quotas': """
     186Quotas are determined on a per-locker basis.  Each locker may have a
     187maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
     188active machines.""",
     189            'Console': """
     190<strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
     191setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
     192your machine will run just fine, but the applet's display of the
     193console will suffer artifacts.
     194""",
     195            'Windows': """
     196<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>
     197<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.
     198"""
     199            }
     200
     201        if not subject:
     202            subject = sorted(help_mapping.keys())
     203        if not isinstance(subject, list):
     204            subject = [subject]
     205
     206        return dict(simple=simple,
     207                    subjects=subject,
     208                    mapping=help_mapping)
     209    help._cp_config['tools.require_login.on'] = False
     210
     211    def parseCreate(self, fields):
     212        kws = dict([(kw, fields[kw]) for kw in
     213         'name description owner memory disksize vmtype cdrom autoinstall'.split()
     214                    if fields[kw]])
     215        validate = validation.Validate(cherrypy.request.login,
     216                                       cherrypy.request.state,
     217                                       strict=True, **kws)
     218        return dict(contact=cherrypy.request.login, name=validate.name,
     219                    description=validate.description, memory=validate.memory,
     220                    disksize=validate.disksize, owner=validate.owner,
     221                    machine_type=getattr(validate, 'vmtype', Defaults.type),
     222                    cdrom=getattr(validate, 'cdrom', None),
     223                    autoinstall=getattr(validate, 'autoinstall', None))
     224
     225    @cherrypy.expose
     226    @cherrypy.tools.mako(filename="/list.mako")
     227    @cherrypy.tools.require_POST()
     228    def create(self, **fields):
     229        """Handler for create requests."""
     230        try:
     231            parsed_fields = self.parseCreate(fields)
     232            machine = controls.createVm(cherrypy.request.login,
     233                                        cherrypy.request.state, **parsed_fields)
     234        except InvalidInput, err:
     235            pass
     236        else:
     237            err = None
     238        cherrypy.request.state.clear() #Changed global state
     239        d = getListDict(cherrypy.request.login, cherrypy.request.state)
     240        d['err'] = err
     241        if err:
     242            for field, value in fields.items():
     243                setattr(d['defaults'], field, value)
     244        else:
     245            d['new_machine'] = parsed_fields['name']
     246        return d
     247
     248    @cherrypy.expose
     249    @cherrypy.tools.mako(filename="/helloworld.mako")
     250    def helloworld(self, **kwargs):
     251        return {'request': cherrypy.request, 'kwargs': kwargs}
     252    helloworld._cp_config['tools.require_login.on'] = False
     253
     254    @cherrypy.expose
     255    def errortest(self):
     256        """Throw an error, to test the error-tracing mechanisms."""
     257        print >>sys.stderr, "look ma, it's a stderr"
     258        raise RuntimeError("test of the emergency broadcast system")
     259
     260    class MachineView(View):
     261        def __getattr__(self, name):
     262            """Synthesize attributes to allow RESTful URLs like
     263            /machine/13/info. This is hairy. CherryPy 3.2 adds a
     264            method called _cp_dispatch that allows you to explicitly
     265            handle URLs that can't be mapped, and it allows you to
     266            rewrite the path components and continue processing.
     267
     268            This function gets the next path component being resolved
     269            as a string. _cp_dispatch will get an array of strings
     270            representing any subsequent path components as well."""
     271
     272            try:
     273                cherrypy.request.params['machine_id'] = int(name)
     274                return self
     275            except ValueError:
     276                return None
     277
     278        @cherrypy.expose
     279        @cherrypy.tools.mako(filename="/info.mako")
     280        def info(self, machine_id):
     281            """Handler for info on a single VM."""
     282            machine = validation.Validate(cherrypy.request.login,
     283                                          cherrypy.request.state,
     284                                          machine_id=machine_id).machine
     285            d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
     286            return d
     287        index = info
     288
     289        @cherrypy.expose
     290        @cherrypy.tools.mako(filename="/info.mako")
     291        @cherrypy.tools.require_POST()
     292        def modify(self, machine_id, **fields):
     293            """Handler for modifying attributes of a machine."""
     294            try:
     295                modify_dict = modifyDict(cherrypy.request.login,
     296                                         cherrypy.request.state,
     297                                         machine_id, fields)
     298            except InvalidInput, err:
     299                result = None
     300                machine = validation.Validate(cherrypy.request.login,
     301                                              cherrypy.request.state,
     302                                              machine_id=machine_id).machine
     303            else:
     304                machine = modify_dict['machine']
     305                result = 'Success!'
     306                err = None
     307            info_dict = infoDict(cherrypy.request.login,
     308                                 cherrypy.request.state, machine)
     309            info_dict['err'] = err
     310            if err:
     311                for field, value in fields.items():
     312                    setattr(info_dict['defaults'], field, value)
     313            info_dict['result'] = result
     314            return info_dict
     315
     316        @cherrypy.expose
     317        @cherrypy.tools.mako(filename="/vnc.mako")
     318        def vnc(self, machine_id):
     319            """VNC applet page.
     320
     321            Note that due to same-domain restrictions, the applet connects to
     322            the webserver, which needs to forward those requests to the xen
     323            server.  The Xen server runs another proxy that (1) authenticates
     324            and (2) finds the correct port for the VM.
     325
     326            You might want iptables like:
     327
     328            -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
     329            --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
     330            -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
     331            --dport 10003 -j SNAT --to-source 18.187.7.142
     332            -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
     333            --dport 10003 -j ACCEPT
     334
     335            Remember to enable iptables!
     336            echo 1 > /proc/sys/net/ipv4/ip_forward
     337            """
     338            machine = validation.Validate(cherrypy.request.login,
     339                                          cherrypy.request.state,
     340                                          machine_id=machine_id).machine
     341            token = controls.vnctoken(machine)
     342            host = controls.listHost(machine)
     343            if host:
     344                port = 10003 + [h.hostname for h in config.hosts].index(host)
     345            else:
     346                port = 5900 # dummy
     347
     348            status = controls.statusInfo(machine)
     349            has_vnc = hasVnc(status)
     350
     351            d = dict(on=status,
     352                     has_vnc=has_vnc,
     353                     machine=machine,
     354                     hostname=cherrypy.request.local.name,
     355                     port=port,
     356                     authtoken=token)
     357            return d
     358
     359        @cherrypy.expose
     360        @cherrypy.tools.mako(filename="/command.mako")
     361        @cherrypy.tools.require_POST()
     362        def command(self, command_name, machine_id, **kwargs):
     363            """Handler for running commands like boot and delete on a VM."""
     364            back = kwargs.get('back')
     365            if command_name == 'delete':
     366                back = 'list'
     367            try:
     368                d = controls.commandResult(cherrypy.request.login,
     369                                           cherrypy.request.state,
     370                                           command_name, machine_id, kwargs)
     371            except InvalidInput, err:
     372                if not back:
     373                    raise
     374                print >> sys.stderr, err
     375                result = str(err)
     376            else:
     377                result = 'Success!'
     378                if not back:
     379                    return d
     380            if back == 'list':
     381                cherrypy.request.state.clear() #Changed global state
     382                raise cherrypy.InternalRedirect('/list?result=%s'
     383                                                % urllib.quote(result))
     384            elif back == 'info':
     385                raise cherrypy.HTTPRedirect(cherrypy.request.base
     386                                            + '/machine/%d/' % machine_id,
     387                                            status=303)
     388            else:
     389                raise InvalidInput('back', back, 'Not a known back page.')
     390
     391    machine = MachineView()
     392
    110393
    111394class Defaults:
     
    117400    name = ''
    118401    description = ''
     402    administrator = ''
    119403    type = 'linux-hvm'
    120404
     
    126410        for key in kws:
    127411            setattr(self, key, kws[key])
    128 
    129 
    130 
    131 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
    132 
    133 def invalidInput(op, username, fields, err, emsg):
    134     """Print an error page when an InvalidInput exception occurs"""
    135     d = dict(op=op, user=username, err_field=err.err_field,
    136              err_value=str(err.err_value), stderr=emsg,
    137              errorMessage=str(err))
    138     return templates.invalid(searchList=[d])
    139412
    140413def hasVnc(status):
     
    148421    return False
    149422
    150 def parseCreate(username, state, fields):
    151     kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
    152     validate = validation.Validate(username, state, strict=True, **kws)
    153     return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
    154                 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
    155                 cdrom=getattr(validate, 'cdrom', None),
    156                 autoinstall=getattr(validate, 'autoinstall', None))
    157 
    158 def create(username, state, path, fields):
    159     """Handler for create requests."""
    160     try:
    161         parsed_fields = parseCreate(username, state, fields)
    162         machine = controls.createVm(username, state, **parsed_fields)
    163     except InvalidInput, err:
    164         pass
    165     else:
    166         err = None
    167     state.clear() #Changed global state
    168     d = getListDict(username, state)
    169     d['err'] = err
    170     if err:
    171         for field in fields.keys():
    172             setattr(d['defaults'], field, fields.getfirst(field))
    173     else:
    174         d['new_machine'] = parsed_fields['name']
    175     return templates.list(searchList=[d])
    176 
    177423
    178424def getListDict(username, state):
    179425    """Gets the list of local variables used by list.tmpl."""
    180     checkpoint.checkpoint('Starting')
    181426    machines = state.machines
    182     checkpoint.checkpoint('Got my machines')
    183427    on = {}
    184428    has_vnc = {}
     429    installing = {}
    185430    xmlist = state.xmlist
    186     checkpoint.checkpoint('Got uptimes')
    187     can_clone = 'ice3' not in state.xmlist_raw
    188431    for m in machines:
    189432        if m not in xmlist:
     
    192435        else:
    193436            m.uptime = xmlist[m]['uptime']
     437            installing[m] = bool(xmlist[m].get('autoinstall'))
    194438            if xmlist[m]['console']:
    195439                has_vnc[m] = True
     
    197441                has_vnc[m] = "WTF?"
    198442            else:
    199                 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
     443                has_vnc[m] = "ParaVM"
    200444    max_memory = validation.maxMemory(username, state)
    201445    max_disk = validation.maxDisk(username)
    202     checkpoint.checkpoint('Got max mem/disk')
    203446    defaults = Defaults(max_memory=max_memory,
    204447                        max_disk=max_disk,
    205448                        owner=username)
    206     checkpoint.checkpoint('Got defaults')
    207449    def sortkey(machine):
    208450        return (machine.owner != username, machine.owner, machine.name)
     
    215457             machines=machines,
    216458             has_vnc=has_vnc,
    217              can_clone=can_clone)
     459             installing=installing)
    218460    return d
    219 
    220 def listVms(username, state, path, fields):
    221     """Handler for list requests."""
    222     checkpoint.checkpoint('Getting list dict')
    223     d = getListDict(username, state)
    224     checkpoint.checkpoint('Got list dict')
    225     return templates.list(searchList=[d])
    226 
    227 def vnc(username, state, path, fields):
    228     """VNC applet page.
    229 
    230     Note that due to same-domain restrictions, the applet connects to
    231     the webserver, which needs to forward those requests to the xen
    232     server.  The Xen server runs another proxy that (1) authenticates
    233     and (2) finds the correct port for the VM.
    234 
    235     You might want iptables like:
    236 
    237     -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
    238       --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
    239     -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
    240       --dport 10003 -j SNAT --to-source 18.187.7.142
    241     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
    242       --dport 10003 -j ACCEPT
    243 
    244     Remember to enable iptables!
    245     echo 1 > /proc/sys/net/ipv4/ip_forward
    246     """
    247     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
    248 
    249     token = controls.vnctoken(machine)
    250     host = controls.listHost(machine)
    251     if host:
    252         port = 10003 + [h.hostname for h in config.hosts].index(host)
    253     else:
    254         port = 5900 # dummy
    255 
    256     status = controls.statusInfo(machine)
    257     has_vnc = hasVnc(status)
    258 
    259     d = dict(user=username,
    260              on=status,
    261              has_vnc=has_vnc,
    262              machine=machine,
    263              hostname=state.environ.get('SERVER_NAME', 'localhost'),
    264              port=port,
    265              authtoken=token)
    266     return templates.vnc(searchList=[d])
    267461
    268462def getHostname(nic):
     
    319513    return disk_fields
    320514
    321 def command(username, state, path, fields):
    322     """Handler for running commands like boot and delete on a VM."""
    323     back = fields.getfirst('back')
    324     try:
    325         d = controls.commandResult(username, state, fields)
    326         if d['command'] == 'Delete VM':
    327             back = 'list'
    328     except InvalidInput, err:
    329         if not back:
    330             raise
    331         print >> sys.stderr, err
    332         result = err
    333     else:
    334         result = 'Success!'
    335         if not back:
    336             return templates.command(searchList=[d])
    337     if back == 'list':
    338         state.clear() #Changed global state
    339         d = getListDict(username, state)
    340         d['result'] = result
    341         return templates.list(searchList=[d])
    342     elif back == 'info':
    343         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
    344         return ({'Status': '303 See Other',
    345                  'Location': 'info?machine_id=%d' % machine.machine_id},
    346                 "You shouldn't see this message.")
    347     else:
    348         raise InvalidInput('back', back, 'Not a known back page.')
    349 
    350 def modifyDict(username, state, fields):
     515def modifyDict(username, state, machine_id, fields):
    351516    """Modify a machine as specified by CGI arguments.
    352517
    353     Return a list of local variables for modify.tmpl.
     518    Return a dict containing the machine that was modified.
    354519    """
    355520    olddisk = {}
    356521    session.begin()
    357522    try:
    358         kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
     523        kws = dict([(kw, fields[kw]) for kw in
     524         'owner admin contact name description memory vmtype disksize'.split()
     525                    if fields[kw]])
     526        kws['machine_id'] = machine_id
    359527        validate = validation.Validate(username, state, **kws)
    360528        machine = validate.machine
     
    403571    if hasattr(validate, 'name'):
    404572        controls.renameMachine(machine, oldname, validate.name)
    405     return dict(user=username,
    406                 command="modify",
    407                 machine=machine)
    408 
    409 def modify(username, state, path, fields):
    410     """Handler for modifying attributes of a machine."""
    411     try:
    412         modify_dict = modifyDict(username, state, fields)
    413     except InvalidInput, err:
    414         result = None
    415         machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
    416     else:
    417         machine = modify_dict['machine']
    418         result = 'Success!'
    419         err = None
    420     info_dict = infoDict(username, state, machine)
    421     info_dict['err'] = err
    422     if err:
    423         for field in fields.keys():
    424             setattr(info_dict['defaults'], field, fields.getfirst(field))
    425     info_dict['result'] = result
    426     return templates.info(searchList=[info_dict])
    427 
    428 
    429 def helpHandler(username, state, path, fields):
    430     """Handler for help messages."""
    431     simple = fields.getfirst('simple')
    432     subjects = fields.getlist('subject')
    433 
    434     help_mapping = {
    435                     'Autoinstalls': """
    436 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
    437 ParaVM.  You can access the resulting system by logging into the <a
    438 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
    439 with your Kerberos tickets; there is no root password so sshd will
    440 refuse login.</p>
    441 
    442 <p>Under the covers, the autoinstaller uses our own patched version of
    443 xen-create-image, which is a tool based on debootstrap.  If you log
    444 into the serial console while the install is running, you can watch
    445 it.
    446 """,
    447                     'ParaVM Console': """
    448 ParaVM machines do not support local console access over VNC.  To
    449 access the serial console of these machines, you can SSH with Kerberos
    450 to %s, using the name of the machine as your
    451 username.""" % config.console.hostname,
    452                     'HVM/ParaVM': """
    453 HVM machines use the virtualization features of the processor, while
    454 ParaVM machines rely on a modified kernel to communicate directly with
    455 the hypervisor.  HVMs support boot CDs of any operating system, and
    456 the VNC console applet.  The three-minute autoinstaller produces
    457 ParaVMs.  ParaVMs typically are more efficient, and always support the
    458 <a href="help?subject=ParaVM+Console">console server</a>.</p>
    459 
    460 <p>More details are <a
    461 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
    462 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
    463 (which you can skip by using the autoinstaller to begin with.)</p>
    464 
    465 <p>We recommend using a ParaVM when possible and an HVM when necessary.
    466 """,
    467                     'CPU Weight': """
    468 Don't ask us!  We're as mystified as you are.""",
    469                     'Owner': """
    470 The owner field is used to determine <a
    471 href="help?subject=Quotas">quotas</a>.  It must be the name of a
    472 locker that you are an AFS administrator of.  In particular, you or an
    473 AFS group you are a member of must have AFS rlidwka bits on the
    474 locker.  You can check who administers the LOCKER locker using the
    475 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
    476 href="help?subject=Administrator">administrator</a>.""",
    477                     'Administrator': """
    478 The administrator field determines who can access the console and
    479 power on and off the machine.  This can be either a user or a moira
    480 group.""",
    481                     'Quotas': """
    482 Quotas are determined on a per-locker basis.  Each locker may have a
    483 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
    484 active machines.""",
    485                     'Console': """
    486 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
    487 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
    488 your machine will run just fine, but the applet's display of the
    489 console will suffer artifacts.
    490 """,
    491                     'Windows': """
    492 <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>
    493 <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.
    494 """
    495                     }
    496 
    497     if not subjects:
    498         subjects = sorted(help_mapping.keys())
    499 
    500     d = dict(user=username,
    501              simple=simple,
    502              subjects=subjects,
    503              mapping=help_mapping)
    504 
    505     return templates.help(searchList=[d])
    506 
    507 
    508 def badOperation(u, s, p, e):
    509     """Function called when accessing an unknown URI."""
    510     return ({'Status': '404 Not Found'}, 'Invalid operation.')
     573    return dict(machine=machine)
    511574
    512575def infoDict(username, state, machine):
    513576    """Get the variables used by info.tmpl."""
    514577    status = controls.statusInfo(machine)
    515     checkpoint.checkpoint('Getting status info')
    516578    has_vnc = hasVnc(status)
    517579    if status is None:
     
    527589        cpu_time_float = float(main_status.get('cpu_time', 0))
    528590        cputime = datetime.timedelta(seconds=int(cpu_time_float))
    529     checkpoint.checkpoint('Status')
    530591    display_fields = [('name', 'Name'),
    531592                      ('description', 'Description'),
     
    541602                      'DISK_INFO',
    542603                      ('state', 'state (xen format)'),
    543                       ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
    544604                      ]
    545605    fields = []
     
    574634            #fields.append((disp, None))
    575635
    576     checkpoint.checkpoint('Got fields')
    577 
    578 
    579636    max_mem = validation.maxMemory(machine.owner, state, machine, False)
    580     checkpoint.checkpoint('Got mem')
    581637    max_disk = validation.maxDisk(machine.owner, machine)
    582638    defaults = Defaults()
    583639    for name in 'machine_id name description administrator owner memory contact'.split():
    584         setattr(defaults, name, getattr(machine, name))
     640        if getattr(machine, name):
     641            setattr(defaults, name, getattr(machine, name))
    585642    defaults.type = machine.type.type_id
    586643    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
    587     checkpoint.checkpoint('Got defaults')
    588644    d = dict(user=username,
    589645             on=status is not None,
     
    595651             max_mem=max_mem,
    596652             max_disk=max_disk,
    597              owner_help=helppopup("Owner"),
    598653             fields = fields)
    599654    return d
    600 
    601 def info(username, state, path, fields):
    602     """Handler for info on a single VM."""
    603     machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
    604     d = infoDict(username, state, machine)
    605     checkpoint.checkpoint('Got infodict')
    606     return templates.info(searchList=[d])
    607 
    608 def unauthFront(_, _2, _3, fields):
    609     """Information for unauth'd users."""
    610     return templates.unauth(searchList=[{'simple' : True,
    611             'hostname' : socket.getfqdn()}])
    612 
    613 def admin(username, state, path, fields):
    614     if path == '':
    615         return ({'Status': '303 See Other',
    616                  'Location': 'admin/'},
    617                 "You shouldn't see this message.")
    618     if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
    619         raise InvalidInput('username', username,
    620                            'Not in admin group %s.' % config.adminacl)
    621     newstate = State(username, isadmin=True)
    622     newstate.environ = state.environ
    623     return handler(username, newstate, path, fields)
    624 
    625 def throwError(_, __, ___, ____):
    626     """Throw an error, to test the error-tracing mechanisms."""
    627     raise RuntimeError("test of the emergency broadcast system")
    628 
    629 mapping = dict(list=listVms,
    630                vnc=vnc,
    631                command=command,
    632                modify=modify,
    633                info=info,
    634                create=create,
    635                help=helpHandler,
    636                unauth=unauthFront,
    637                admin=admin,
    638                overlord=admin,
    639                errortest=throwError)
    640 
    641 def printHeaders(headers):
    642     """Print a dictionary as HTTP headers."""
    643     for key, value in headers.iteritems():
    644         print '%s: %s' % (key, value)
    645     print
    646655
    647656def send_error_mail(subject, body):
     
    661670    p.wait()
    662671
    663 def show_error(op, username, fields, err, emsg, traceback):
    664     """Print an error page when an exception occurs"""
    665     d = dict(op=op, user=username, fields=fields,
    666              errorMessage=str(err), stderr=emsg, traceback=traceback)
    667     details = templates.error_raw(searchList=[d])
    668     exclude = config.web.errormail_exclude
    669     if username not in exclude and '*' not in exclude:
    670         send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
    671                         details)
    672     d['details'] = details
    673     return templates.error(searchList=[d])
    674 
    675 def getUser(environ):
    676     """Return the current user based on the SSL environment variables"""
    677     user = environ.get('REMOTE_USER')
    678     if user is None:
    679         return
    680    
    681     if environ.get('AUTH_TYPE') == 'Negotiate':
    682         # Convert the krb5 principal into a krb4 username
    683         if not user.endswith('@%s' % config.kerberos.realm):
    684             return
    685         else:
    686             return user.split('@')[0].replace('/', '.')
    687     else:
    688         return user
    689 
    690 def handler(username, state, path, fields):
    691     operation, path = pathSplit(path)
    692     if not operation:
    693         operation = 'list'
    694     print 'Starting', operation
    695     fun = mapping.get(operation, badOperation)
    696     return fun(username, state, path, fields)
    697 
    698 class App:
    699     def __init__(self, environ, start_response):
    700         self.environ = environ
    701         self.start = start_response
    702 
    703         self.username = getUser(environ)
    704         self.state = State(self.username)
    705         self.state.environ = environ
    706 
    707         random.seed() #sigh
    708 
    709     def __iter__(self):
    710         start_time = time.time()
    711         database.clear_cache()
    712         sys.stderr = StringIO()
    713         fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
    714         operation = self.environ.get('PATH_INFO', '')
    715         if not operation:
    716             self.start("301 Moved Permanently", [('Location', './')])
    717             return
    718         if self.username is None:
    719             operation = 'unauth'
    720 
    721         try:
    722             checkpoint.checkpoint('Before')
    723             output = handler(self.username, self.state, operation, fields)
    724             checkpoint.checkpoint('After')
    725 
    726             headers = dict(DEFAULT_HEADERS)
    727             if isinstance(output, tuple):
    728                 new_headers, output = output
    729                 headers.update(new_headers)
    730             e = revertStandardError()
    731             if e:
    732                 if hasattr(output, 'addError'):
    733                     output.addError(e)
    734                 else:
    735                     # This only happens on redirects, so it'd be a pain to get
    736                     # the message to the user.  Maybe in the response is useful.
    737                     output = output + '\n\nstderr:\n' + e
    738             output_string =  str(output)
    739             checkpoint.checkpoint('output as a string')
    740         except Exception, err:
    741             if not fields.has_key('js'):
    742                 if isinstance(err, InvalidInput):
    743                     self.start('200 OK', [('Content-Type', 'text/html')])
    744                     e = revertStandardError()
    745                     yield str(invalidInput(operation, self.username, fields,
    746                                            err, e))
    747                     return
    748             import traceback
    749             self.start('500 Internal Server Error',
    750                        [('Content-Type', 'text/html')])
    751             e = revertStandardError()
    752             s = show_error(operation, self.username, fields,
    753                            err, e, traceback.format_exc())
    754             yield str(s)
    755             return
    756         status = headers.setdefault('Status', '200 OK')
    757         del headers['Status']
    758         self.start(status, headers.items())
    759         yield output_string
    760         if fields.has_key('timedebug'):
    761             yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
    762 
    763 def constructor():
    764     connect()
    765     return App
    766 
    767 def main():
    768     from flup.server.fcgi_fork import WSGIServer
    769     WSGIServer(constructor()).run()
    770 
    771 if __name__ == '__main__':
    772     main()
     672random.seed() #sigh
  • trunk/packages/invirt-web/code/static/style.css

    r1318 r2737  
    105105  padding: 0.1em 0.5em;
    106106}
     107
     108form {
     109    display: inline;
     110}
  • trunk/packages/invirt-web/code/validation.py

    • Property svn:executable set to *
    r2590 r2737  
    259259    return contact
    260260
    261 def testDisk(user, disksize, machine=None):
    262     return disksize
    263 
    264261def testName(user, name, machine=None):
    265262    if name is None:
  • trunk/packages/invirt-web/debian/changelog

    r2611 r2737  
     1invirt-web (0.1.0) unstable; urgency=low
     2
     3  [Quentin Smith]
     4  * Switch to CherryPy in place of our home-grown web framework.
     5  * Switch from the Cheetah templating engine to the Mako templating engine.
     6  * New URI scheme: /machine/<numeric-id>/<operation>
     7    rather than /<operation>?machine_id=<numeric-id> .
     8  * Fix power-on/power-off/reboot buttons for IE <=8.
     9  * Move some bits of presentation code from Python into templates.
     10  * Clarify that Windows licenses are available from MIT for staff.
     11
     12  [Evan Broder]
     13  * Show newlines from descriptions in list page.
     14  * Only aklog to a cell if encryption is actually needed.
     15  * Re-arrange the authz configuration.
     16
     17 -- Greg Price <price@mit.edu>  Sat, 19 Dec 2009 21:53:40 -0500
     18
    119invirt-web (0.0.24) unstable; urgency=low
    220
  • trunk/packages/invirt-web/debian/control

    r2052 r2737  
    1717 debathena-ssl-certificates,
    1818# python libraries
    19  python-flup, python-cheetah, python-simplejson,
    20  python-dns, python-dnspython,
     19 python-flup, python-simplejson,
     20 python-dns, python-dnspython, python-cherrypy3,
     21 python-mako,
    2122# misc
    2223 kstart,
  • trunk/packages/invirt-web/debian/copyright

    r1318 r2737  
    1515On Debian systems, the complete text of the GNU General Public License
    1616can be found in the file /usr/share/common-licenses/GPL.
     17
     18The file "code/static/power_installing.png" is from the Human-O2 icon
     19set by Oliver Scholtz and is released under the "GNU/GPL" (source:
     20http://www.iconfinder.net/icondetails/24350/128/ -
     21http://schollidesign.deviantart.com/art/Human-O2-Iconset-105344123)
  • trunk/packages/invirt-web/files/etc/apache2/sites-available/default.mako

    r1674 r2737  
    2323        RewriteRule ^/trac(.*) ${tracuri}$1 [R,L]
    2424        RewriteRule ^/invirt - [L]
    25         RewriteRule ^/sipb-xen(.*) /invirt$1 [PT]
    2625        RewriteRule ^/kill.cgi - [L]
    27         RewriteRule ^/~ - [L]
    28         RewriteRule ^/(.*) /var/www/invirt-web/main.fcgi/$1 [L]
     26        RewriteRule ^/(.*) /var/www/invirt-web/unauth.fcgi/$1 [L]
    2927
    3028        ErrorLog /var/log/apache2/error.log
  • trunk/packages/invirt-web/files/etc/apache2/sites-available/ssl.mako

    r2048 r2737  
    2727        RewriteRule ^/trac(.*) ${tracuri}$1 [R,L]
    2828        RewriteRule ^/kill.cgi - [L]
    29         RewriteRule ^/~ - [L]
    30         RewriteRule ^/(.*) /var/www/invirt-web/main.fcgi/$1 [L]
     29        RewriteRule ^/(.*) /var/www/invirt-web/auth.fcgi/$1 [L]
    3130
    3231        RewriteLog /var/log/apache2/rewrite.log
Note: See TracChangeset for help on using the changeset viewer.