X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-web.git/blobdiff_plain/542183dd43e3d9c9260b2ce1313126b7f6535f6c..812770288a8001b9c4c3e42f24be7cbb754202d6:/code/main.py diff --git a/code/main.py b/code/main.py index 5de084e..f42e1fd 100755 --- a/code/main.py +++ b/code/main.py @@ -1,28 +1,24 @@ #!/usr/bin/python """Main CGI script for web interface""" +from __future__ import with_statement + import base64 import cPickle import cgi import datetime import hmac +import os import random import sha -import simplejson import sys +import threading import time import urllib import socket import cherrypy +from cherrypy import _cperror from StringIO import StringIO -def revertStandardError(): - """Move stderr to stdout, and return the contents of the old stderr.""" - errio = sys.stderr - if not isinstance(errio, StringIO): - return '' - sys.stderr = sys.stdout - errio.seek(0) - return errio.read() def printError(): """Revert stderr to stdout, and print the contents of stderr""" @@ -33,8 +29,6 @@ if __name__ == '__main__': import atexit atexit.register(printError) -import templates -from Cheetah.Template import Template import validation import cache_acls from webcommon import State @@ -45,19 +39,70 @@ from invirt.database import Machine, CDROM, session, connect, MachineAccess, Typ from invirt.config import structs as config from invirt.common import InvalidInput, CodeError -from view import View +from view import View, revertStandardError +import ajaxterm + + +static_dir = os.path.join(os.path.dirname(__file__), 'static') +InvirtStatic = cherrypy.tools.staticdir.handler( + root=static_dir, + dir=static_dir, + section='/static') + +class InvirtUnauthWeb(View): + static = InvirtStatic + + @cherrypy.expose + @cherrypy.tools.mako(filename="/unauth.mako") + def index(self): + return {'simple': True} class InvirtWeb(View): def __init__(self): super(self.__class__,self).__init__() connect() self._cp_config['tools.require_login.on'] = True + self._cp_config['tools.catch_stderr.on'] = True self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config', 'from invirt import database'] + self._cp_config['request.error_response'] = self.handle_error + + static = InvirtStatic + + @cherrypy.expose + @cherrypy.tools.mako(filename="/invalid.mako") + def invalidInput(self): + """Print an error page when an InvalidInput exception occurs""" + err = cherrypy.request.prev.params["err"] + emsg = cherrypy.request.prev.params["emsg"] + d = dict(err_field=err.err_field, + err_value=str(err.err_value), stderr=emsg, + errorMessage=str(err)) + return d + + @cherrypy.expose + @cherrypy.tools.mako(filename="/error.mako") + def error(self): + """Print an error page when an exception occurs""" + op = cherrypy.request.prev.path_info + username = cherrypy.request.login + err = cherrypy.request.prev.params["err"] + emsg = cherrypy.request.prev.params["emsg"] + traceback = cherrypy.request.prev.params["traceback"] + d = dict(op=op, user=username, fields=cherrypy.request.prev.params, + errorMessage=str(err), stderr=emsg, traceback=traceback) + error_raw = cherrypy.request.lookup.get_template("/error_raw.mako") + details = error_raw.render(**d) + exclude = config.web.errormail_exclude + if username not in exclude and '*' not in exclude: + send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err), + details) + d['details'] = details + return d def __getattr__(self, name): if name in ("admin", "overlord"): - if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz[0].cell): + if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell): raise InvalidInput('username', cherrypy.request.login, 'Not in admin group %s.' % config.adminacl) cherrypy.request.state = State(cherrypy.request.login, isadmin=True) @@ -65,15 +110,27 @@ class InvirtWeb(View): else: return super(InvirtWeb, self).__getattr__(name) + def handle_error(self): + err = sys.exc_info()[1] + if isinstance(err, InvalidInput): + cherrypy.request.params['err'] = err + cherrypy.request.params['emsg'] = revertStandardError() + raise cherrypy.InternalRedirect('/invalidInput') + if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params: + cherrypy.request.params['err'] = err + cherrypy.request.params['emsg'] = revertStandardError() + cherrypy.request.params['traceback'] = _cperror.format_exc() + raise cherrypy.InternalRedirect('/error') + # fall back to cherrypy default error page + cherrypy.HTTPError(500).set_response() + @cherrypy.expose @cherrypy.tools.mako(filename="/list.mako") def list(self, result=None): """Handler for list requests.""" - checkpoint.checkpoint('Getting list dict') d = getListDict(cherrypy.request.login, cherrypy.request.state) if result is not None: d['result'] = result - checkpoint.checkpoint('Got list dict') return d index=list @@ -156,10 +213,16 @@ console will suffer artifacts. help._cp_config['tools.require_login.on'] = False def parseCreate(self, fields): - kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)]) - validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws) - return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory, - disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type), + kws = dict([(kw, fields[kw]) for kw in + 'name description owner memory disksize vmtype cdrom autoinstall'.split() + if fields[kw]]) + validate = validation.Validate(cherrypy.request.login, + cherrypy.request.state, + strict=True, **kws) + return dict(contact=cherrypy.request.login, name=validate.name, + description=validate.description, memory=validate.memory, + disksize=validate.disksize, owner=validate.owner, + machine_type=getattr(validate, 'vmtype', Defaults.type), cdrom=getattr(validate, 'cdrom', None), autoinstall=getattr(validate, 'autoinstall', None)) @@ -170,7 +233,8 @@ console will suffer artifacts. """Handler for create requests.""" try: parsed_fields = self.parseCreate(fields) - machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields) + machine = controls.createVm(cherrypy.request.login, + cherrypy.request.state, **parsed_fields) except InvalidInput, err: pass else: @@ -179,8 +243,8 @@ console will suffer artifacts. d = getListDict(cherrypy.request.login, cherrypy.request.state) d['err'] = err if err: - for field in fields.keys(): - setattr(d['defaults'], field, fields.get(field)) + for field, value in fields.items(): + setattr(d['defaults'], field, value) else: d['new_machine'] = parsed_fields['name'] return d @@ -194,17 +258,23 @@ console will suffer artifacts. @cherrypy.expose def errortest(self): """Throw an error, to test the error-tracing mechanisms.""" + print >>sys.stderr, "look ma, it's a stderr" raise RuntimeError("test of the emergency broadcast system") class MachineView(View): - # This is hairy. Fix when CherryPy 3.2 is out. (rename to - # _cp_dispatch, and parse the argument as a list instead of - # string - def __getattr__(self, name): + """Synthesize attributes to allow RESTful URLs like + /machine/13/info. This is hairy. CherryPy 3.2 adds a + method called _cp_dispatch that allows you to explicitly + handle URLs that can't be mapped, and it allows you to + rewrite the path components and continue processing. + + This function gets the next path component being resolved + as a string. _cp_dispatch will get an array of strings + representing any subsequent path components as well.""" + try: - machine_id = int(name) - cherrypy.request.params['machine_id'] = machine_id + cherrypy.request.params['machine_id'] = int(name) return self except ValueError: return None @@ -213,13 +283,41 @@ console will suffer artifacts. @cherrypy.tools.mako(filename="/info.mako") def info(self, machine_id): """Handler for info on a single VM.""" - machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine + machine = validation.Validate(cherrypy.request.login, + cherrypy.request.state, + machine_id=machine_id).machine d = infoDict(cherrypy.request.login, cherrypy.request.state, machine) - checkpoint.checkpoint('Got infodict') return d index = info @cherrypy.expose + @cherrypy.tools.mako(filename="/info.mako") + @cherrypy.tools.require_POST() + def modify(self, machine_id, **fields): + """Handler for modifying attributes of a machine.""" + try: + modify_dict = modifyDict(cherrypy.request.login, + cherrypy.request.state, + machine_id, fields) + except InvalidInput, err: + result = None + machine = validation.Validate(cherrypy.request.login, + cherrypy.request.state, + machine_id=machine_id).machine + else: + machine = modify_dict['machine'] + result = 'Success!' + err = None + info_dict = infoDict(cherrypy.request.login, + cherrypy.request.state, machine) + info_dict['err'] = err + if err: + for field, value in fields.items(): + setattr(info_dict['defaults'], field, value) + info_dict['result'] = result + return info_dict + + @cherrypy.expose @cherrypy.tools.mako(filename="/vnc.mako") def vnc(self, machine_id): """VNC applet page. @@ -241,8 +339,9 @@ console will suffer artifacts. Remember to enable iptables! echo 1 > /proc/sys/net/ipv4/ip_forward """ - machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine - + machine = validation.Validate(cherrypy.request.login, + cherrypy.request.state, + machine_id=machine_id).machine token = controls.vnctoken(machine) host = controls.listHost(machine) if host: @@ -260,86 +359,83 @@ console will suffer artifacts. port=port, authtoken=token) return d + @cherrypy.expose @cherrypy.tools.mako(filename="/command.mako") @cherrypy.tools.require_POST() def command(self, command_name, machine_id, **kwargs): """Handler for running commands like boot and delete on a VM.""" - back = kwargs.get('back', None) + back = kwargs.get('back') + if command_name == 'delete': + back = 'list' try: - d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs) - if d['command'] == 'Delete VM': - back = 'list' + d = controls.commandResult(cherrypy.request.login, + cherrypy.request.state, + command_name, machine_id, kwargs) except InvalidInput, err: if not back: raise print >> sys.stderr, err - result = err + result = str(err) else: result = 'Success!' if not back: return d if back == 'list': cherrypy.request.state.clear() #Changed global state - raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result)) + raise cherrypy.InternalRedirect('/list?result=%s' + % urllib.quote(result)) elif back == 'info': - raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303) + raise cherrypy.HTTPRedirect(cherrypy.request.base + + '/machine/%d/' % machine_id, + status=303) else: raise InvalidInput('back', back, 'Not a known back page.') - machine = MachineView() - -def pathSplit(path): - if path.startswith('/'): - path = path[1:] - i = path.find('/') - if i == -1: - i = len(path) - return path[:i], path[i:] - -class Checkpoint: - def __init__(self): - self.start_time = time.time() - self.checkpoints = [] - - def checkpoint(self, s): - self.checkpoints.append((s, time.time())) + atmulti = ajaxterm.Multiplex() + atsessions = {} + atsessions_lock = threading.Lock() - def __str__(self): - return ('Timing info:\n%s\n' % - '\n'.join(['%s: %s' % (d, t - self.start_time) for - (d, t) in self.checkpoints])) - -checkpoint = Checkpoint() + @cherrypy.expose + @cherrypy.tools.mako(filename="/terminal.mako") + def terminal(self, machine_id): + machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine -def makeErrorPre(old, addition): - if addition is None: - return - if old: - return old[:-6] + '\n----\n' + str(addition) + '' - else: - return '
STDERR:
' + str(addition) + '' + status = controls.statusInfo(machine) + has_vnc = hasVnc(status) -Template.database = database -Template.config = config -Template.err = None + d = dict(on=status, + has_vnc=has_vnc, + machine=machine, + hostname=cherrypy.request.local.name) + return d -class JsonDict: - """Class to store a dictionary that will be converted to JSON""" - def __init__(self, **kws): - self.data = kws - if 'err' in kws: - err = kws['err'] - del kws['err'] - self.addError(err) + @cherrypy.expose + @cherrypy.tools.gzip() + def at(self, machine_id, k=None, c=0, force=0): + machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine + with self.atsessions_lock: + if machine_id in self.atsessions: + term = self.atsessions[machine_id] + else: + print >>sys.stderr, "spawning new session for terminal to ",machine_id + term = self.atsessions[machine_id] = self.atmulti.create( + ["ssh", "-e","none", "-l", machine.name, config.console.hostname] + ) + if k: + self.atmulti.proc_write(term,k) + time.sleep(0.002) + dump=self.atmulti.dump(term,c,int(force)) + cherrypy.response.headers['Content-Type']='text/xml' + if isinstance(dump,str): + return dump + else: + print "Removing session for", machine_id + del self.atsessions[machine_id] + return '
%s' % cgi.escape(str(checkpoint)) - -def constructor(): - connect() - return App - -def main(): - from flup.server.fcgi_fork import WSGIServer - WSGIServer(constructor()).run() - -if __name__ == '__main__': - main() +random.seed() #sigh