X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-web.git/blobdiff_plain/e73cda9748a8726531dba4b309783dde1597549a..812770288a8001b9c4c3e42f24be7cbb754202d6:/code/main.py
diff --git a/code/main.py b/code/main.py
index ba892bf..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,26 +39,98 @@ 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.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)
+ return self
+ 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
@@ -132,7 +198,7 @@ console will suffer artifacts.
""",
'Windows': """
Windows Vista: The Vista image is licensed for all MIT students and will automatically activate off the network; see the licensing confirmation e-mail for details. The installer requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).
-Windows XP: 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.
+Windows XP: 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 http://msca.mit.edu/ if you are staff/faculty to request one.
"""
}
@@ -146,21 +212,69 @@ console will suffer artifacts.
mapping=help_mapping)
help._cp_config['tools.require_login.on'] = False
+ def parseCreate(self, fields):
+ 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))
+
+ @cherrypy.expose
+ @cherrypy.tools.mako(filename="/list.mako")
+ @cherrypy.tools.require_POST()
+ def create(self, **fields):
+ """Handler for create requests."""
+ try:
+ parsed_fields = self.parseCreate(fields)
+ machine = controls.createVm(cherrypy.request.login,
+ cherrypy.request.state, **parsed_fields)
+ except InvalidInput, err:
+ pass
+ else:
+ err = None
+ cherrypy.request.state.clear() #Changed global state
+ d = getListDict(cherrypy.request.login, cherrypy.request.state)
+ d['err'] = err
+ if err:
+ for field, value in fields.items():
+ setattr(d['defaults'], field, value)
+ else:
+ d['new_machine'] = parsed_fields['name']
+ return d
+
@cherrypy.expose
@cherrypy.tools.mako(filename="/helloworld.mako")
def helloworld(self, **kwargs):
return {'request': cherrypy.request, 'kwargs': kwargs}
helloworld._cp_config['tools.require_login.on'] = False
- 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
+ @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):
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
@@ -169,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.
@@ -197,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:
@@ -216,87 +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."""
- if cherrypy.request.method != "POST":
- raise InvalidInput("request.method", command_name, "You must execute commands via POST")
- 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