From: Evan Broder Date: Mon, 21 Dec 2009 04:27:10 +0000 (-0500) Subject: Merge cherrypy-rebased branch of invirt-web into trunk. X-Git-Tag: 0.1.0^0 X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-web.git/commitdiff_plain/a657eaa1d5a78de84fbd8cf4d40c189269fb901f?hp=b3969219ebe0ca5d9ba6e3015d9c6b750a435631 Merge cherrypy-rebased branch of invirt-web into trunk. svn path=/trunk/packages/invirt-web/; revision=2737 --- diff --git a/code/Makefile b/code/Makefile index b114e86..c406438 100644 --- a/code/Makefile +++ b/code/Makefile @@ -1,6 +1,4 @@ -DIRS = templates - -all: kill chmod compile +all: kill chmod chmod: chgrp -R invirt . 2>/dev/null || true @@ -8,13 +6,3 @@ chmod: kill: -pkill main.fcgi - -compile: - for dir in $(DIRS); do \ - (cd $$dir; $(MAKE) all); \ - done - -clean: - for dir in $(DIRS); do \ - (cd $$dir; $(MAKE) clean); \ - done diff --git a/code/auth.fcgi b/code/auth.fcgi new file mode 120000 index 0000000..b94e858 --- /dev/null +++ b/code/auth.fcgi @@ -0,0 +1 @@ +invirt.fcgi \ No newline at end of file diff --git a/code/controls.py b/code/controls.py index e32b1c2..bbc9f8c 100644 --- a/code/controls.py +++ b/code/controls.py @@ -204,17 +204,16 @@ def deleteVM(machine): session.rollback() raise -def commandResult(username, state, fields): +def commandResult(username, state, command_name, machine_id, fields): start_time = 0 - machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine - action = fields.getfirst('action') - cdrom = fields.getfirst('cdrom') + machine = validation.Validate(username, state, machine_id=machine_id).machine + action = command_name + cdrom = fields.get('cdrom') or None if cdrom is not None and not CDROM.query().filter_by(cdrom_id=cdrom).one(): raise CodeError("Invalid cdrom type '%s'" % cdrom) - if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', - 'Delete VM'): + if action not in "reboot create destroy shutdown delete".split(" "): raise CodeError("Invalid action '%s'" % action) - if action == 'Reboot': + if action == 'reboot': if cdrom is not None: out, err = remctl('control', machine.name, 'reboot', cdrom, err=True) @@ -230,13 +229,13 @@ def commandResult(username, state, fields): print >> sys.stderr, err raise CodeError('ERROR on remctl') - elif action == 'Power on': + elif action == 'create': if validation.maxMemory(username, state, machine) < machine.memory: raise InvalidInput('action', 'Power on', "You don't have enough free RAM quota " "to turn on this machine.") bootMachine(machine, cdrom) - elif action == 'Power off': + elif action == 'destroy': out, err = remctl('control', machine.name, 'destroy', err=True) if err: if re.match("machine '.*' is not on", err): @@ -246,7 +245,7 @@ def commandResult(username, state, fields): print >> sys.stderr, 'Error on power off:' print >> sys.stderr, err raise CodeError('ERROR on remctl') - elif action == 'Shutdown': + elif action == 'shutdown': out, err = remctl('control', machine.name, 'shutdown', err=True) if err: if re.match("machine '.*' is not on", err): @@ -256,7 +255,7 @@ def commandResult(username, state, fields): print >> sys.stderr, 'Error on Shutdown:' print >> sys.stderr, err raise CodeError('ERROR on remctl') - elif action == 'Delete VM': + elif action == 'delete': deleteVM(machine) d = dict(user=username, diff --git a/code/dev.conf b/code/dev.conf new file mode 100644 index 0000000..e1c9ddb --- /dev/null +++ b/code/dev.conf @@ -0,0 +1,11 @@ +# For the testing site. + +[global] +server.socket_port = 8080 +log.access_file = "/tmp/invirt-web-access_log.dev" +log.error_file = "/tmp/invirt-web-error_log.dev" +tools.mako.module_directory = "/tmp/invirt-web-templatecache.dev" +tools.basic_auth.on = True +tools.basic_auth.realm = 'Invirt Web DEVELOPMENT' +tools.basic_auth.users = {'quentin': 'quentin','broder': 'broder'} +tools.basic_auth.encrypt = __builtin__.str diff --git a/code/getafsgroups.py b/code/getafsgroups.py index fed3a6b..1c6b82e 100755 --- a/code/getafsgroups.py +++ b/code/getafsgroups.py @@ -33,7 +33,8 @@ def getAfsGroupMembers(group, cell): for c in config.authz.afs.cells: if c.cell == cell and hasattr(c, 'auth'): encrypt = c.auth - subprocess.check_call(['aklog', cell], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if encrypt: + subprocess.check_call(['aklog', cell], stdout=subprocess.PIPE, stderr=subprocess.PIPE) p = subprocess.Popen(["pts", "membership", "-encrypt" if encrypt else '-noauth', group, '-c', cell], stdout=subprocess.PIPE, stderr=subprocess.PIPE) err = p.stderr.read() diff --git a/code/invirt.fcgi b/code/invirt.fcgi new file mode 100755 index 0000000..c01eb7f --- /dev/null +++ b/code/invirt.fcgi @@ -0,0 +1,61 @@ +#!/usr/bin/python +"""Main FastCGI entry point for web interface""" + +import cherrypy +import os +import sys + +import main + +dev = False +base_dir = os.path.dirname(__file__) + +def usage(): + print >>sys.stderr, """%s [config] + +Run server as FastCGI, with CherryPy config from "main.conf". + +With `config`, run standalone with CherryPy config from `config`. + +Run this script as either 'auth.fcgi' or 'unauth.fcgi', to get +the authenticated or unauthenticated site respectively. +""" % sys.argv[0] + sys.exit(2) + +if __name__ == "__main__": + if len(sys.argv) > 2: + usage() + if len(sys.argv) > 1: + if sys.argv[1] in ('-h', '--help'): + usage() + conf_file = sys.argv[1] + dev = True + else: + conf_file = os.path.join(base_dir, 'main.conf') + + app_config = { + '/': { + 'tools.invirtwebstate.on': True, + }, + } + + if os.path.basename(sys.argv[0]).startswith('auth'): + root = main.InvirtWeb() + elif os.path.basename(sys.argv[0]).startswith('unauth'): + root = main.InvirtUnauthWeb() + else: + usage() + + app = cherrypy.tree.mount(root, '/', app_config) + app.merge(conf_file) + cherrypy.config.update(conf_file) + + if dev: + cherrypy.server.quickstart() + cherrypy.engine.start() + cherrypy.engine.block() + else: + cherrypy.engine.start(blocking=False) + from flup.server.fcgi import WSGIServer + server = WSGIServer(cherrypy.tree) + server.run() diff --git a/code/main.conf b/code/main.conf new file mode 100644 index 0000000..6381f19 --- /dev/null +++ b/code/main.conf @@ -0,0 +1,11 @@ +# This file is used when the web interface is loaded as a FastCGI +[global] +#auto_reload doesn't work with FastCGI +engine.auto_reload = False +tools.mako.module_directory = "/tmp/invirt-web-templatecache" +tools.remote_user_login.on = True + +engine.SIGHUP = None +engine.SIGTERM = None + +log.screen = False diff --git a/code/main.fcgi b/code/main.fcgi deleted file mode 100755 index b2c7031..0000000 --- a/code/main.fcgi +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/python -import main -main.main() diff --git a/code/main.py b/code/main.py index 6fdd31a..dd2b591 100755 --- a/code/main.py +++ b/code/main.py @@ -6,24 +6,17 @@ import cPickle import cgi import datetime import hmac +import os import random import sha -import simplejson import sys 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""" if isinstance(sys.stderr, StringIO): @@ -33,8 +26,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,68 +36,360 @@ from invirt.database import Machine, CDROM, session, connect, MachineAccess, Typ from invirt.config import structs as config from invirt.common import InvalidInput, CodeError -def pathSplit(path): - if path.startswith('/'): - path = path[1:] - i = path.find('/') - if i == -1: - i = len(path) - return path[:i], path[i:] +from view import View, revertStandardError + + +static_dir = os.path.join(os.path.dirname(__file__), 'static') +InvirtStatic = cherrypy.tools.staticdir.handler( + root=static_dir, + dir=static_dir, + section='/static') -class Checkpoint: +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): - self.start_time = time.time() - self.checkpoints = [] - - def checkpoint(self, s): - self.checkpoints.append((s, time.time())) - - 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() - -def jquote(string): - return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'" - -def helppopup(subj): - """Return HTML code for a (?) link to a specified help topic""" - return ('(?)') - -def makeErrorPre(old, addition): - if addition is None: - return - if old: - return old[:-6] + '\n----\n' + str(addition) + '' - else: - return '

STDERR:

' + str(addition) + '
' - -Template.database = database -Template.config = config -Template.helppopup = staticmethod(helppopup) -Template.err = None - -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) - - def __str__(self): - return simplejson.dumps(self.data) - - def addError(self, text): - """Add stderr text to be displayed on the website.""" - self.data['err'] = \ - makeErrorPre(self.data.get('err'), text) + 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.""" + d = getListDict(cherrypy.request.login, cherrypy.request.state) + if result is not None: + d['result'] = result + return d + index=list + + @cherrypy.expose + @cherrypy.tools.mako(filename="/help.mako") + def help(self, subject=None, simple=False): + """Handler for help messages.""" + + help_mapping = { + 'Autoinstalls': """ +The autoinstaller builds a minimal Debian or Ubuntu system to run as a +ParaVM. You can access the resulting system by logging into the serial console server +with your Kerberos tickets; there is no root password so sshd will +refuse login.

+ +

Under the covers, the autoinstaller uses our own patched version of +xen-create-image, which is a tool based on debootstrap. If you log +into the serial console while the install is running, you can watch +it. +""", + 'ParaVM Console': """ +ParaVM machines do not support local console access over VNC. To +access the serial console of these machines, you can SSH with Kerberos +to %s, using the name of the machine as your +username.""" % config.console.hostname, + 'HVM/ParaVM': """ +HVM machines use the virtualization features of the processor, while +ParaVM machines rely on a modified kernel to communicate directly with +the hypervisor. HVMs support boot CDs of any operating system, and +the VNC console applet. The three-minute autoinstaller produces +ParaVMs. ParaVMs typically are more efficient, and always support the +console server.

+ +

More details are on the +wiki, including steps to prepare an HVM guest to boot as a ParaVM +(which you can skip by using the autoinstaller to begin with.)

+ +

We recommend using a ParaVM when possible and an HVM when necessary. +""", + 'CPU Weight': """ +Don't ask us! We're as mystified as you are.""", + 'Owner': """ +The owner field is used to determine quotas. It must be the name of a +locker that you are an AFS administrator of. In particular, you or an +AFS group you are a member of must have AFS rlidwka bits on the +locker. You can check who administers the LOCKER locker using the +commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also administrator.""", + 'Administrator': """ +The administrator field determines who can access the console and +power on and off the machine. This can be either a user or a moira +group.""", + 'Quotas': """ +Quotas are determined on a per-locker basis. Each locker may have a +maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4 +active machines.""", + 'Console': """ +Framebuffer: At a Linux boot prompt in your VM, try +setting fb=false to disable the framebuffer. If you don't, +your machine will run just fine, but the applet's display of the +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, or visit http://msca.mit.edu/ if you are staff/faculty to request one. +""" + } + + if not subject: + subject = sorted(help_mapping.keys()) + if not isinstance(subject, list): + subject = [subject] + + return dict(simple=simple, + subjects=subject, + 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 + + @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: + cherrypy.request.params['machine_id'] = int(name) + return self + except ValueError: + return None + + @cherrypy.expose + @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 + d = infoDict(cherrypy.request.login, cherrypy.request.state, machine) + 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. + + Note that due to same-domain restrictions, the applet connects to + the webserver, which needs to forward those requests to the xen + server. The Xen server runs another proxy that (1) authenticates + and (2) finds the correct port for the VM. + + You might want iptables like: + + -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \ + --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 + -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \ + --dport 10003 -j SNAT --to-source 18.187.7.142 + -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \ + --dport 10003 -j ACCEPT + + 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 + token = controls.vnctoken(machine) + host = controls.listHost(machine) + if host: + port = 10003 + [h.hostname for h in config.hosts].index(host) + else: + port = 5900 # dummy + + status = controls.statusInfo(machine) + has_vnc = hasVnc(status) + + d = dict(on=status, + has_vnc=has_vnc, + machine=machine, + hostname=cherrypy.request.local.name, + 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') + if command_name == 'delete': + back = 'list' + try: + 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 = 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)) + elif back == 'info': + raise cherrypy.HTTPRedirect(cherrypy.request.base + + '/machine/%d/' % machine_id, + status=303) + else: + raise InvalidInput('back', back, 'Not a known back page.') + + machine = MachineView() + class Defaults: """Class to store default values for fields.""" @@ -116,6 +399,7 @@ class Defaults: autoinstall = '' name = '' description = '' + administrator = '' type = 'linux-hvm' def __init__(self, max_memory=None, max_disk=None, **kws): @@ -126,17 +410,6 @@ class Defaults: for key in kws: setattr(self, key, kws[key]) - - -DEFAULT_HEADERS = {'Content-Type': 'text/html'} - -def invalidInput(op, username, fields, err, emsg): - """Print an error page when an InvalidInput exception occurs""" - d = dict(op=op, user=username, err_field=err.err_field, - err_value=str(err.err_value), stderr=emsg, - errorMessage=str(err)) - return templates.invalid(searchList=[d]) - def hasVnc(status): """Does the machine with a given status list support VNC?""" if status is None: @@ -147,63 +420,32 @@ def hasVnc(status): return 'location' in d return False -def parseCreate(username, state, fields): - kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()]) - validate = validation.Validate(username, state, strict=True, **kws) - return dict(contact=username, 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)) - -def create(username, state, path, fields): - """Handler for create requests.""" - try: - parsed_fields = parseCreate(username, state, fields) - machine = controls.createVm(username, state, **parsed_fields) - except InvalidInput, err: - pass - else: - err = None - state.clear() #Changed global state - d = getListDict(username, state) - d['err'] = err - if err: - for field in fields.keys(): - setattr(d['defaults'], field, fields.getfirst(field)) - else: - d['new_machine'] = parsed_fields['name'] - return templates.list(searchList=[d]) - def getListDict(username, state): """Gets the list of local variables used by list.tmpl.""" - checkpoint.checkpoint('Starting') machines = state.machines - checkpoint.checkpoint('Got my machines') on = {} has_vnc = {} + installing = {} xmlist = state.xmlist - checkpoint.checkpoint('Got uptimes') - can_clone = 'ice3' not in state.xmlist_raw for m in machines: if m not in xmlist: has_vnc[m] = 'Off' m.uptime = None else: m.uptime = xmlist[m]['uptime'] + installing[m] = bool(xmlist[m].get('autoinstall')) if xmlist[m]['console']: has_vnc[m] = True elif m.type.hvm: has_vnc[m] = "WTF?" else: - has_vnc[m] = "ParaVM"+helppopup("ParaVM Console") + has_vnc[m] = "ParaVM" max_memory = validation.maxMemory(username, state) max_disk = validation.maxDisk(username) - checkpoint.checkpoint('Got max mem/disk') defaults = Defaults(max_memory=max_memory, max_disk=max_disk, owner=username) - checkpoint.checkpoint('Got defaults') def sortkey(machine): return (machine.owner != username, machine.owner, machine.name) machines = sorted(machines, key=sortkey) @@ -214,57 +456,9 @@ def getListDict(username, state): defaults=defaults, machines=machines, has_vnc=has_vnc, - can_clone=can_clone) + installing=installing) return d -def listVms(username, state, path, fields): - """Handler for list requests.""" - checkpoint.checkpoint('Getting list dict') - d = getListDict(username, state) - checkpoint.checkpoint('Got list dict') - return templates.list(searchList=[d]) - -def vnc(username, state, path, fields): - """VNC applet page. - - Note that due to same-domain restrictions, the applet connects to - the webserver, which needs to forward those requests to the xen - server. The Xen server runs another proxy that (1) authenticates - and (2) finds the correct port for the VM. - - You might want iptables like: - - -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \ - --dport 10003 -j DNAT --to-destination 18.181.0.60:10003 - -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \ - --dport 10003 -j SNAT --to-source 18.187.7.142 - -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \ - --dport 10003 -j ACCEPT - - Remember to enable iptables! - echo 1 > /proc/sys/net/ipv4/ip_forward - """ - machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine - - token = controls.vnctoken(machine) - host = controls.listHost(machine) - if host: - port = 10003 + [h.hostname for h in config.hosts].index(host) - else: - port = 5900 # dummy - - status = controls.statusInfo(machine) - has_vnc = hasVnc(status) - - d = dict(user=username, - on=status, - has_vnc=has_vnc, - machine=machine, - hostname=state.environ.get('SERVER_NAME', 'localhost'), - port=port, - authtoken=token) - return templates.vnc(searchList=[d]) - def getHostname(nic): """Find the hostname associated with a NIC. @@ -318,44 +512,18 @@ def getDiskInfo(data_dict, machine): data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.) return disk_fields -def command(username, state, path, fields): - """Handler for running commands like boot and delete on a VM.""" - back = fields.getfirst('back') - try: - d = controls.commandResult(username, state, fields) - if d['command'] == 'Delete VM': - back = 'list' - except InvalidInput, err: - if not back: - raise - print >> sys.stderr, err - result = err - else: - result = 'Success!' - if not back: - return templates.command(searchList=[d]) - if back == 'list': - state.clear() #Changed global state - d = getListDict(username, state) - d['result'] = result - return templates.list(searchList=[d]) - elif back == 'info': - machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine - return ({'Status': '303 See Other', - 'Location': 'info?machine_id=%d' % machine.machine_id}, - "You shouldn't see this message.") - else: - raise InvalidInput('back', back, 'Not a known back page.') - -def modifyDict(username, state, fields): +def modifyDict(username, state, machine_id, fields): """Modify a machine as specified by CGI arguments. - Return a list of local variables for modify.tmpl. + Return a dict containing the machine that was modified. """ olddisk = {} session.begin() try: - kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()]) + kws = dict([(kw, fields[kw]) for kw in + 'owner admin contact name description memory vmtype disksize'.split() + if fields[kw]]) + kws['machine_id'] = machine_id validate = validation.Validate(username, state, **kws) machine = validate.machine oldname = machine.name @@ -402,117 +570,11 @@ def modifyDict(username, state, fields): controls.resizeDisk(oldname, diskname, str(olddisk[diskname])) if hasattr(validate, 'name'): controls.renameMachine(machine, oldname, validate.name) - return dict(user=username, - command="modify", - machine=machine) - -def modify(username, state, path, fields): - """Handler for modifying attributes of a machine.""" - try: - modify_dict = modifyDict(username, state, fields) - except InvalidInput, err: - result = None - machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine - else: - machine = modify_dict['machine'] - result = 'Success!' - err = None - info_dict = infoDict(username, state, machine) - info_dict['err'] = err - if err: - for field in fields.keys(): - setattr(info_dict['defaults'], field, fields.getfirst(field)) - info_dict['result'] = result - return templates.info(searchList=[info_dict]) - - -def helpHandler(username, state, path, fields): - """Handler for help messages.""" - simple = fields.getfirst('simple') - subjects = fields.getlist('subject') - - help_mapping = { - 'Autoinstalls': """ -The autoinstaller builds a minimal Debian or Ubuntu system to run as a -ParaVM. You can access the resulting system by logging into the serial console server -with your Kerberos tickets; there is no root password so sshd will -refuse login.

- -

Under the covers, the autoinstaller uses our own patched version of -xen-create-image, which is a tool based on debootstrap. If you log -into the serial console while the install is running, you can watch -it. -""", - 'ParaVM Console': """ -ParaVM machines do not support local console access over VNC. To -access the serial console of these machines, you can SSH with Kerberos -to %s, using the name of the machine as your -username.""" % config.console.hostname, - 'HVM/ParaVM': """ -HVM machines use the virtualization features of the processor, while -ParaVM machines rely on a modified kernel to communicate directly with -the hypervisor. HVMs support boot CDs of any operating system, and -the VNC console applet. The three-minute autoinstaller produces -ParaVMs. ParaVMs typically are more efficient, and always support the -console server.

- -

More details are on the -wiki, including steps to prepare an HVM guest to boot as a ParaVM -(which you can skip by using the autoinstaller to begin with.)

- -

We recommend using a ParaVM when possible and an HVM when necessary. -""", - 'CPU Weight': """ -Don't ask us! We're as mystified as you are.""", - 'Owner': """ -The owner field is used to determine quotas. It must be the name of a -locker that you are an AFS administrator of. In particular, you or an -AFS group you are a member of must have AFS rlidwka bits on the -locker. You can check who administers the LOCKER locker using the -commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also administrator.""", - 'Administrator': """ -The administrator field determines who can access the console and -power on and off the machine. This can be either a user or a moira -group.""", - 'Quotas': """ -Quotas are determined on a per-locker basis. Each locker may have a -maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4 -active machines.""", - 'Console': """ -Framebuffer: At a Linux boot prompt in your VM, try -setting fb=false to disable the framebuffer. If you don't, -your machine will run just fine, but the applet's display of the -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. -""" - } - - if not subjects: - subjects = sorted(help_mapping.keys()) - - d = dict(user=username, - simple=simple, - subjects=subjects, - mapping=help_mapping) - - return templates.help(searchList=[d]) - - -def badOperation(u, s, p, e): - """Function called when accessing an unknown URI.""" - return ({'Status': '404 Not Found'}, 'Invalid operation.') + return dict(machine=machine) def infoDict(username, state, machine): """Get the variables used by info.tmpl.""" status = controls.statusInfo(machine) - checkpoint.checkpoint('Getting status info') has_vnc = hasVnc(status) if status is None: main_status = dict(name=machine.name, @@ -526,7 +588,6 @@ def infoDict(username, state, machine): uptime = datetime.timedelta(seconds=int(time.time()-start_time)) cpu_time_float = float(main_status.get('cpu_time', 0)) cputime = datetime.timedelta(seconds=int(cpu_time_float)) - checkpoint.checkpoint('Status') display_fields = [('name', 'Name'), ('description', 'Description'), ('owner', 'Owner'), @@ -540,7 +601,6 @@ def infoDict(username, state, machine): ('memory', 'RAM'), 'DISK_INFO', ('state', 'state (xen format)'), - ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')), ] fields = [] machine_info = {} @@ -573,18 +633,14 @@ def infoDict(username, state, machine): pass #fields.append((disp, None)) - checkpoint.checkpoint('Got fields') - - max_mem = validation.maxMemory(machine.owner, state, machine, False) - checkpoint.checkpoint('Got mem') max_disk = validation.maxDisk(machine.owner, machine) defaults = Defaults() for name in 'machine_id name description administrator owner memory contact'.split(): - setattr(defaults, name, getattr(machine, name)) + if getattr(machine, name): + setattr(defaults, name, getattr(machine, name)) defaults.type = machine.type.type_id defaults.disk = "%0.2f" % (machine.disks[0].size/1024.) - checkpoint.checkpoint('Got defaults') d = dict(user=username, on=status is not None, machine=machine, @@ -594,56 +650,9 @@ def infoDict(username, state, machine): ram=machine.memory, max_mem=max_mem, max_disk=max_disk, - owner_help=helppopup("Owner"), fields = fields) return d -def info(username, state, path, fields): - """Handler for info on a single VM.""" - machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine - d = infoDict(username, state, machine) - checkpoint.checkpoint('Got infodict') - return templates.info(searchList=[d]) - -def unauthFront(_, _2, _3, fields): - """Information for unauth'd users.""" - return templates.unauth(searchList=[{'simple' : True, - 'hostname' : socket.getfqdn()}]) - -def admin(username, state, path, fields): - if path == '': - return ({'Status': '303 See Other', - 'Location': 'admin/'}, - "You shouldn't see this message.") - if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'): - raise InvalidInput('username', username, - 'Not in admin group %s.' % config.adminacl) - newstate = State(username, isadmin=True) - newstate.environ = state.environ - return handler(username, newstate, path, fields) - -def throwError(_, __, ___, ____): - """Throw an error, to test the error-tracing mechanisms.""" - raise RuntimeError("test of the emergency broadcast system") - -mapping = dict(list=listVms, - vnc=vnc, - command=command, - modify=modify, - info=info, - create=create, - help=helpHandler, - unauth=unauthFront, - admin=admin, - overlord=admin, - errortest=throwError) - -def printHeaders(headers): - """Print a dictionary as HTTP headers.""" - for key, value in headers.iteritems(): - print '%s: %s' % (key, value) - print - def send_error_mail(subject, body): import subprocess @@ -660,113 +669,4 @@ Subject: %s p.stdin.close() p.wait() -def show_error(op, username, fields, err, emsg, traceback): - """Print an error page when an exception occurs""" - d = dict(op=op, user=username, fields=fields, - errorMessage=str(err), stderr=emsg, traceback=traceback) - details = templates.error_raw(searchList=[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, username, err), - details) - d['details'] = details - return templates.error(searchList=[d]) - -def getUser(environ): - """Return the current user based on the SSL environment variables""" - user = environ.get('REMOTE_USER') - if user is None: - return - - if environ.get('AUTH_TYPE') == 'Negotiate': - # Convert the krb5 principal into a krb4 username - if not user.endswith('@%s' % config.kerberos.realm): - return - else: - return user.split('@')[0].replace('/', '.') - else: - return user - -def handler(username, state, path, fields): - operation, path = pathSplit(path) - if not operation: - operation = 'list' - print 'Starting', operation - fun = mapping.get(operation, badOperation) - return fun(username, state, path, fields) - -class App: - def __init__(self, environ, start_response): - self.environ = environ - self.start = start_response - - self.username = getUser(environ) - self.state = State(self.username) - self.state.environ = environ - - random.seed() #sigh - - def __iter__(self): - start_time = time.time() - database.clear_cache() - sys.stderr = StringIO() - fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ) - operation = self.environ.get('PATH_INFO', '') - if not operation: - self.start("301 Moved Permanently", [('Location', './')]) - return - if self.username is None: - operation = 'unauth' - - try: - checkpoint.checkpoint('Before') - output = handler(self.username, self.state, operation, fields) - checkpoint.checkpoint('After') - - headers = dict(DEFAULT_HEADERS) - if isinstance(output, tuple): - new_headers, output = output - headers.update(new_headers) - e = revertStandardError() - if e: - if hasattr(output, 'addError'): - output.addError(e) - else: - # This only happens on redirects, so it'd be a pain to get - # the message to the user. Maybe in the response is useful. - output = output + '\n\nstderr:\n' + e - output_string = str(output) - checkpoint.checkpoint('output as a string') - except Exception, err: - if not fields.has_key('js'): - if isinstance(err, InvalidInput): - self.start('200 OK', [('Content-Type', 'text/html')]) - e = revertStandardError() - yield str(invalidInput(operation, self.username, fields, - err, e)) - return - import traceback - self.start('500 Internal Server Error', - [('Content-Type', 'text/html')]) - e = revertStandardError() - s = show_error(operation, self.username, fields, - err, e, traceback.format_exc()) - yield str(s) - return - status = headers.setdefault('Status', '200 OK') - del headers['Status'] - self.start(status, headers.items()) - yield output_string - if fields.has_key('timedebug'): - yield '

%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 diff --git a/code/static/power_installing.png b/code/static/power_installing.png new file mode 100644 index 0000000..434e048 Binary files /dev/null and b/code/static/power_installing.png differ diff --git a/code/static/style.css b/code/static/style.css index 49fe8e2..a764e17 100644 --- a/code/static/style.css +++ b/code/static/style.css @@ -104,3 +104,7 @@ tr.stripedrow { #machinelist td, #machinelist th { padding: 0.1em 0.5em; } + +form { + display: inline; +} diff --git a/code/templates/Makefile b/code/templates/Makefile deleted file mode 100644 index aa083e2..0000000 --- a/code/templates/Makefile +++ /dev/null @@ -1,13 +0,0 @@ -TEMPLATES=$(wildcard *.tmpl) -OUTPUTS=$(TEMPLATES:.tmpl=.py) - -all: ${OUTPUTS} - -%.py: %.tmpl - cheetah compile $< - -#${OUTPUTS}:${TEMPLATES} -# cheetah compile $^ - -clean: - @rm -f ${OUTPUTS} *.pyo *.pyc *.py.bak diff --git a/code/templates/__init__.py b/code/templates/__init__.py deleted file mode 100644 index b2b23da..0000000 --- a/code/templates/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -__all__ = 'info command error error_raw help invalid list unauth vnc'.split() -for _name in __all__: - try: - _module = __import__(_name, globals(), {}, [_name]) - globals()[_name] = getattr(_module, _name) - except ImportError, e: - import sys - print >> sys.stderr, 'Importing template "%s" raised error: %s' % (_name, e) - diff --git a/code/templates/command.mako b/code/templates/command.mako new file mode 100644 index 0000000..17a16ed --- /dev/null +++ b/code/templates/command.mako @@ -0,0 +1,9 @@ +<%page expression_filter="h" /> +<%inherit file="skeleton.mako" /> + +<%def name="title()"> +$command ${machine.name} + + +

${command} ${machine.name} was successful.

+

Return

diff --git a/code/templates/command.tmpl b/code/templates/command.tmpl deleted file mode 100644 index 904cba7..0000000 --- a/code/templates/command.tmpl +++ /dev/null @@ -1,16 +0,0 @@ -#from skeleton import skeleton -#extends skeleton - -#def title -$command $machine.name -#end def - - -#def body -

$command ${machine.name} was successful.

-#if $command == "Delete VM" or True -

Return

-#else -

Return

-#end if -#end def diff --git a/code/templates/create.tmpl b/code/templates/create.tmpl deleted file mode 100644 index 8123d23..0000000 --- a/code/templates/create.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -#from skeleton import skeleton -#extends skeleton - -#def title -Created! -#end def - -#def body -#if $machine -

Congratulations! You have a new machine named ${machine.name}.

-#else -

Odd... no error, but no machine.

-#end if -

Return

-#end def diff --git a/code/templates/error.tmpl b/code/templates/error.mako similarity index 57% rename from code/templates/error.tmpl rename to code/templates/error.mako index ff9b4e0..1edb1b4 100644 --- a/code/templates/error.tmpl +++ b/code/templates/error.mako @@ -1,20 +1,18 @@ -#from skeleton import skeleton -#extends skeleton +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> -#def title +<%def name="title()"> ERROR! -#end def + -#def body

Uh-oh! We experienced an error. Sorry about that. We've gotten mail about it.

-

Feel free to poke us at xvm@mit.edu if this bug is +

Feel free to poke us at ${config.contact} if this bug is consistently biting you and we don't seem to be fixing it.

In case you're curious, the gory details are below.

-$details
+${details}
 
-#end def diff --git a/code/templates/error_raw.mako b/code/templates/error_raw.mako new file mode 100644 index 0000000..a8cf902 --- /dev/null +++ b/code/templates/error_raw.mako @@ -0,0 +1,12 @@ +Error on operation ${op} for user ${user}: ${errorMessage} + +Fields: +%for f in fields: +${f}=${fields[f]} +%endfor + +Error output: +${stderr}\ +---- end error output + +${traceback} diff --git a/code/templates/error_raw.tmpl b/code/templates/error_raw.tmpl deleted file mode 100644 index 5ae1490..0000000 --- a/code/templates/error_raw.tmpl +++ /dev/null @@ -1,12 +0,0 @@ -Error on operation $op for user $user: $errorMessage - -Fields: -#for $f in $fields: -$f=$fields[$f].value -#end for - -Error output: -$stderr#slurp ----- end error output - -$traceback diff --git a/code/templates/functions.mako b/code/templates/functions.mako new file mode 100644 index 0000000..4f23635 --- /dev/null +++ b/code/templates/functions.mako @@ -0,0 +1,54 @@ +<%page expression_filter="h"/> +<%def name="databaseList(lst, default, onchange, name, id, valueattr, descattr)"> + + + +<%def name="cdromList(default='', onchange=None)"> +${databaseList(sorted(database.CDROM.query(), key=lambda x: x.description), + default, onchange, 'cdrom', 'cdromlist', 'cdrom_id', 'description')|n} + + +<%def name="autoList(default='', onchange=None)"> +${databaseList(sorted(database.Autoinstall.query(), key=lambda x: x.description), + default, onchange, 'autoinstall', 'autoinstalllist', 'autoinstall_id', 'description')|n} + + +<%def name="vmTypeList(default=None)"> +% for vmtype in (('linux-hvm', 'HVM'), ('linux', 'ParaVM'), ): + +% endfor + + +<%def name="errorRow(value, err)"> +% if err and err.err_field == value: + +${str(err)} + +% endif + + +<%! +def jquote(string): + return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'" + +def nl2br(string): + return string.replace('\n', '
') +%> + +<%def name="helppopup(subj)"> +## Return HTML code for a (?) link to a specified help topic +(?) + diff --git a/code/templates/functions.tmpl b/code/templates/functions.tmpl deleted file mode 100644 index 305b4e8..0000000 --- a/code/templates/functions.tmpl +++ /dev/null @@ -1,63 +0,0 @@ -#filter WebSafe -#def databaseList($lst, $default, $onchange, $name, $id, $valueattr, $descattr) - -#end def - -#def cdromList($default="", $onchange=None) -#filter None -$databaseList(sorted($database.CDROM.query(), key=lambda x: x.description), - default, onchange, 'cdrom', 'cdromlist', 'cdrom_id', 'description') -#end filter -#end def - -#def autoList($default="", $onchange=None) -#filter None -$databaseList(sorted($database.Autoinstall.query(), key=lambda x: x.description), - default, onchange, 'autoinstall', 'autoinstalllist', 'autoinstall_id', 'description') -#end filter -#end def - -#def vmTypeList($default=None) -#for $vmtype in (('linux-hvm', 'HVM'), ('linux', 'ParaVM'), ) - -#end for -#end def - -#def addError(txt) -#if $varExists('txt') -#set global $error_text = $error_text + '----\n' + $txt -#else -#set global $error_text = $txt -#end if -#end def - -#def errorRow($value, $err) -#if $err and $err.err_field == $value - -${str($err)} - -#end if -#end def -#filter None -$full_body -#end filter -#end filter diff --git a/code/templates/helloworld.mako b/code/templates/helloworld.mako new file mode 100644 index 0000000..2cbb063 --- /dev/null +++ b/code/templates/helloworld.mako @@ -0,0 +1,19 @@ +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> + +

Hello world!

+ +

kwargs:

+
+${repr(kwargs)}
+
+ +

Your request:

+ +
+${repr(dir(request))}
+
+ +<%def name="title()"> +helloworld + diff --git a/code/templates/help.mako b/code/templates/help.mako new file mode 100644 index 0000000..ea27773 --- /dev/null +++ b/code/templates/help.mako @@ -0,0 +1,37 @@ +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> + +<%! + pageclass = 'help' +%> + +<%def name="title()"> +% if len(subjects) == 1: +Help on ${subjects[0]} +% else: +Help +% endif + + +% if not simple: +

Topics: +% for key in sorted(mapping): +${key} +% endfor +

+

+See also tips and HOWTOs on the wiki. +

+% endif + +% for subject in subjects: +% if subject in mapping: +

${subject}

+

${mapping[subject]|n}

+% else: +

Unknown subject '${subject}'.

+% endif +% endfor +% if simple: +Close +% endif diff --git a/code/templates/help.tmpl b/code/templates/help.tmpl deleted file mode 100644 index 61061ee..0000000 --- a/code/templates/help.tmpl +++ /dev/null @@ -1,39 +0,0 @@ -#from skeleton import skeleton -#extends skeleton - -#attr pageclass = 'help' - -#def title -#if len($subjects) == 1 -Help on $subjects[0] -#else -Help -#end if -#end def - - -#def body -#if not $simple -

Topics: -#for $key in sorted($mapping) -$key -#end for -

-

-See also tips and HOWTOs on the wiki. -

-#end if -#for $subject in $subjects -#if $subject in $mapping -

$subject

-#filter None -

$mapping[$subject]

-#end filter -#else -

Unknown subject '$subject'.

-#end if -#end for -#if $simple -Close -#end if -#end def diff --git a/code/templates/info.mako b/code/templates/info.mako new file mode 100644 index 0000000..0d1b66b --- /dev/null +++ b/code/templates/info.mako @@ -0,0 +1,103 @@ +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> + +<%def name="title()"> +Info on ${machine.name} + + +<%def name="infoTable()"> +

Info

+ + % for key, value in fields: + + % endfor +
${key}:${value}
+ + +<%def name="commands()"> +% if on: + % if not machine.type.hvm: + Console access: type + ssh ${machine.name}@${config.console.hostname} + on Athena. (more info) + % elif has_vnc: + Get Console + % else: + VNC console not enabled; still booting? + % endif +% endif +<%def name="command_button(title, value, cdrom=False, extra='')"> +
+ + +% if cdrom: + Boot CD: ${self.fn.cdromList()} +% endif +
+ +
+ % if on: + ${command_button("Power off", "destroy")} + ${command_button("Shutdown", "shutdown")} + ${command_button("Reboot", "reboot", cdrom=True)} + % else: + ${command_button("Power on", "create", cdrom=True)} + % endif +
+
+ ${command_button("Delete VM", "delete", extra='''onclick="return confirm('Are you sure that you want to delete this VM?');"''')} +
+ + + +<%def name="modifyForm()"> +% if err: +

We had a problem with your request:

+% elif new_machine: +

Successfully modified.

+% endif +% if on: +(To edit ram, disk size, or machine name, turn off the machine first.) +% endif +
+ + + +${self.fn.errorRow('owner', err)} + +${self.fn.errorRow('administrator', err)} + +${self.fn.errorRow('contact', err)} +% if not on: + +${self.fn.errorRow('name', err)} + + + + + +${self.fn.errorRow('memory', err)} + +${self.fn.errorRow('disk', err)} +% else: +${self.fn.errorRow('name', err)} +${self.fn.errorRow('memory', err)} +${self.fn.errorRow('disk', err)} +% endif + +
Description:
Owner${self.fn.helppopup("Owner")}:
Administrator${self.fn.helppopup("Administrator")}:
Contact email:
Machine Name:.${config.dns.domains[0]}
HVM/ParaVM${self.fn.helppopup('HVM/ParaVM')}${self.fn.vmTypeList(defaults.type)}
Ram:MiB (max ${max_mem})
Disk:GiB (max ${max_disk})WARNING: Modifying disk size may corrupt your data.
+
+ + +
+ ${infoTable()} +
+ +

Commands

+
+ ${commands()} +
+

Settings

+
+ ${modifyForm()} +
diff --git a/code/templates/info.tmpl b/code/templates/info.tmpl deleted file mode 100644 index d872fce..0000000 --- a/code/templates/info.tmpl +++ /dev/null @@ -1,142 +0,0 @@ -#from skeleton import skeleton -#extends skeleton - -#def title -Info on $machine.name -#end def - -#def infoTable() -

Info

- - #for $key, $value in $fields - - #end for -
$key:$value
-#end def - -#def commands() -#if $on - #if not $machine.type.hvm - Console access: type - ssh $machine.name@$config.console.hostname - on Athena. (more info) - #elif $has_vnc - Get Console - #else - VNC console not enabled; still booting? - #end if -#end if -
- - -
- #if $on - - - - #else - - #end if -
-
- Boot CD: -#filter None -$cdromList()#slurp -#end filter -
-
- -
-
-#end def - -#def modifyForm() -#if $err -

We had a problem with your request:

-#else if $varExists('new_machine') -

Successfully modified.

-#end if -#if $on -(To edit ram, disk size, or machine name, turn off the machine first.) -#end if -
- - - - -#filter None -$errorRow('owner', $err) -#end filter - -#filter None -$errorRow('administrator', $err) -#end filter - -#filter None -$errorRow('contact', $err) -#end filter -#if not $on - -#filter None -$errorRow('name', $err) -#end filter - - - - - -#filter None -$errorRow('memory', $err) -#end filter - -#filter None -$errorRow('disk', $err) -#end filter -#else -#filter None -$errorRow('name', $err) -$errorRow('memory', $err) -$errorRow('disk', $err) -#end filter -#end if - -
Description:
Owner#slurp -#filter None -$helppopup("Owner")#slurp -#end filter -:
Administrator#slurp -#filter None -$helppopup("Administrator")#slurp -#end filter -:
Contact email:
Machine Name:.${config.dns.domains[0]}
HVM/ParaVM#slurp -#filter None -$helppopup('HVM/ParaVM')#slurp -#end filter -#slurp -#filter None -$vmTypeList($defaults.type)#slurp -#end filter -
Ram:MiB (max $max_mem)
Disk:GiB (max $max_disk)WARNING: Modifying disk size may corrupt your data.
-
-#end def - -#def body -
-#filter None - $infoTable() -#end filter -
- -

Commands

-
-#filter None - $commands() -#end filter -
-

Settings

-
-#filter None - $modifyForm() -#end filter -
-#end def diff --git a/code/templates/invalid.mako b/code/templates/invalid.mako new file mode 100644 index 0000000..c597d18 --- /dev/null +++ b/code/templates/invalid.mako @@ -0,0 +1,16 @@ +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> + +<%def name="title()"> +Invalid Input + + +

Your input was bad:

+ + + +%if stderr: +

Printed to standard error:

+
${stderr}
+%endif +
Fieldvaluereason
${err_field}${err_value}${errorMessage}
diff --git a/code/templates/invalid.tmpl b/code/templates/invalid.tmpl deleted file mode 100644 index 952e1c3..0000000 --- a/code/templates/invalid.tmpl +++ /dev/null @@ -1,18 +0,0 @@ -#from skeleton import skeleton -#extends skeleton - -#def title -Invalid Input -#end def - -#def body -

Your input was bad:

- - - -#if $stderr -

Printed to standard error:

-
$stderr
-#end if -
operationFieldvaluereason
$op$err_field$err_value$errorMessage
-#end def diff --git a/code/templates/list.mako b/code/templates/list.mako new file mode 100644 index 0000000..d513763 --- /dev/null +++ b/code/templates/list.mako @@ -0,0 +1,164 @@ +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> +<%! + import datetime +%> + + +<%def name="title()"> +VM List + + +<%def name="createForm()"> +% if cant_add_vm: +

${cant_add_vm}

+% else: +

Create a new VM

+% if err: +

We had a problem with your request:

+% elif new_machine: +

Congratulations! You successfully created a new VM called ${new_machine}.

+% endif +
+ + + ${self.fn.errorRow('create', err)} + + + + + ${self.fn.errorRow('name', err)} + + + + + ${self.fn.errorRow('description', err)} + + + + + ${self.fn.errorRow('memory', err)} + + + + + ${self.fn.errorRow('disk', err)} + + + + + ${self.fn.errorRow('vmtype', err)} + ${self.fn.errorRow('autoinstall', err)} + + + + + + + + + ${self.fn.errorRow('cdrom', err)} + ${self.fn.errorRow('cdrom', err)} + + + + + ${self.fn.errorRow('owner', err)} +
Name.${config.dns.domains[0]}
Description
Memory MiB (${max_memory} max)
Disk GiB (${"%0.1f" % (max_disk-0.05)} max)
HVM/ParaVM${self.fn.helppopup('HVM/ParaVM')} + ${self.fn.vmTypeList(defaults.type)} +
Autoinstall${self.fn.helppopup('Autoinstalls')} +${self.fn.autoList(defaults.cdrom, "$('cd_or_auto_auto').checked = true;$('cdromlist').value = '';$('vmtype-linux').checked = true")} + (experimental; 2-3 minutes, and you have a machine with empty root password.) + +
Boot CD +${self.fn.cdromList(defaults.cdrom, "$('cd_or_auto_cd').checked = true;$('autoinstalllist').value = '';$('vmtype-linux-hvm').checked = true")} +
Owner
+
+ Windows notes: ${self.fn.helppopup('Windows')} +
+% endif + + +<%def name="machineRow(machine)"> + + + % if machine.uptime and installing[machine]: + Installing... + % else: +
+ + + +
+ % endif + + ${machine.name} + ${machine.memory}M + ${machine.owner} + ${machine.administrator} +% if machine.nics: + ${', '.join(nic.ip for nic in machine.nics)} +% else: + +% endif +\ +% if machine.uptime: +${datetime.timedelta(seconds=int(machine.uptime))}\ +% endif + + \ +% if has_vnc[machine] == True: +Console\ +% elif has_vnc[machine] == 'ParaVM': +ParaVM${self.fn.helppopup("ParaVM Console")} +% elif has_vnc[machine] != 'Off': +${has_vnc[machine]} +% endif + + + + ${machine.description|self.fn.module.nl2br} + + + +<%def name="machineList(machines)"> + + + + + + + + + + + +% for machine in machines: + ${machineRow(machine)} +% endfor +
NameMemoryOwner${self.fn.helppopup('Owner')}Administrator${self.fn.helppopup('Administrator')}IPUptimeVNC
+ + + + +

What is XVM?

+% if not machines: +

You don't currently control any VMs.

+% endif +

refresh

+
+ ${machineList(machines)} +
+${createForm()} diff --git a/code/templates/list.tmpl b/code/templates/list.tmpl deleted file mode 100644 index 8a0231e..0000000 --- a/code/templates/list.tmpl +++ /dev/null @@ -1,214 +0,0 @@ -#from skeleton import skeleton -#from invirt.config import structs as config -#extends skeleton -#import datetime - - -#def title -VM List -#end def - -#def createForm() -#if $cant_add_vm -

$cant_add_vm

-#else -

Create a new VM

-#if $err -

We had a problem with your request:

-#else if $varExists('new_machine') -

Congratulations! You successfully created a new VM called $new_machine.

-#end if -
- - -#filter None - $errorRow('create', $err) -#end filter - - - - -#filter None -$errorRow('name', $err) -#end filter - - - - -#filter None -$errorRow('description', $err) -#end filter - - - - -#filter None -$errorRow('memory', $err) -#end filter - - - - -#filter None -$errorRow('disk', $err) -#end filter - - - - -#filter None -$errorRow('vmtype', $err) -#end filter -#filter None -$errorRow('autoinstall', $err) -#end filter - - - - - - - - -$errorRow('cdrom', $err) -$errorRow('cdrom', $err) - - - - -#filter None - $errorRow('owner', $err) -#end filter -
Name.${config.dns.domains[0]}
Description
Memory MiB ($max_memory max)
Disk GiB (${"%0.1f" % ($max_disk-0.05)} max)
HVM/ParaVM#slurp -#filter None -$helppopup('HVM/ParaVM')#slurp -#end filter - -#filter None -$vmTypeList($defaults.type) -#end filter -
Autoinstall#slurp -#filter None -$helppopup('Autoinstalls')#slurp -#end filter - -#filter None -$autoList($defaults.cdrom, "$('cd_or_auto_auto').checked = true;$('cdromlist').value = '';$('vmtype-linux').checked = true") - (experimental; 2-3 minutes, and you have a machine with empty root password.) -#end filter - -
Boot CD -#filter None -$cdromList($defaults.cdrom, "$('cd_or_auto_cd').checked = true;$('autoinstalllist').value = '';$('vmtype-linux-hvm').checked = true") -#end filter -
Owner
-
- Windows notes: #slurp -#filter None -$helppopup('Windows')#slurp -#end filter -
-#end if -#end def - -#def machineRow($machine) - - -
- - - -
- - $machine.name - ${machine.memory}M - $machine.owner - $machine.administrator -#if $machine.nics -#set $nic = $machine.nics[0] - $nic.ip -#else - -#end if -#slurp -#if $machine.uptime -${datetime.timedelta(seconds=int(machine.uptime))}#slurp -#end if - - #slurp -#if $has_vnc[$machine] == True -Console#slurp -#else if $has_vnc[$machine] != 'Off' -#filter None -$has_vnc[$machine] -#end filter -#end if - - - - $machine.description - -#end def - -#def machineList($machines) - - - - - - - - - - - - #for $machine in $machines: - #filter None - $machineRow($machine) - #end filter - #end for -
NameMemoryOwner#slurp -#filter None -$helppopup('Owner')#slurp -#end filter -Administrator#slurp -#filter None -$helppopup('Administrator')#slurp -#end filter -IPUptimeVNC
- - -#end def - - -#def body -

What is XVM?

-#if not $machines -

You don't currently control any VMs.

-#end if -

refresh

-
- #filter None - $machineList($machines) - #end filter -
-#filter None -$createForm() -#end filter -#end def diff --git a/code/templates/skeleton.mako b/code/templates/skeleton.mako new file mode 100644 index 0000000..a53d2bf --- /dev/null +++ b/code/templates/skeleton.mako @@ -0,0 +1,88 @@ +<%page expression_filter="h"/> +<%namespace name="fn" file="functions.mako" inheritable="True"/> + + +${self.title()} — XVM + + + + + + + + + +% if False: +
+

We are in the process of modifying the service. Things likely will not work.

+
+% endif + +% if error_text is not UNDEFINED: +
+

STDERR:

${error_text}
+
+% endif + +% if not simple: +% if cherrypy.request.login: +

Welcome, ${cherrypy.request.login}. +% if cherrypy.request.state.isadmin: +You are currently authenticated as an administrator. +% endif +

+% endif + + + +% endif + +
+% if result: +${result} +% endif +
+ +% if not simple: +

${self.title()} — XVM

+% endif +${next.body()} +% if not simple: +
+Questions? Contact xvm@mit.edu. +% endif + + + +<%def name="title()"> +XVM + diff --git a/code/templates/skeleton.tmpl b/code/templates/skeleton.tmpl deleted file mode 100644 index e71348c..0000000 --- a/code/templates/skeleton.tmpl +++ /dev/null @@ -1,80 +0,0 @@ -#from functions import functions -#extends functions - -#def full_body - - -$title — XVM - - - - - - - - -#if False -
-

We are in the process of modifying the service. Things likely will not work.

-
-#end if - -
-#if $varExists('error_text') -

STDERR:

$error_text
-#end if -
- -#if not $varExists('simple') or not $simple -

Welcome, $user.

- - -#end if -
-#if $varExists('result') -$result -#end if -
- -#if not $varExists('simple') or not $simple -

$title — XVM

-#end if -#filter None -$body -#end filter -#if not $varExists('simple') or not $simple -
-Questions? Contact xvm@mit.edu. -#end if - - -#end def diff --git a/code/templates/unauth.tmpl b/code/templates/unauth.mako similarity index 90% rename from code/templates/unauth.tmpl rename to code/templates/unauth.mako index 0715f2c..562481f 100644 --- a/code/templates/unauth.tmpl +++ b/code/templates/unauth.mako @@ -1,12 +1,10 @@ -#from skeleton import skeleton -#extends skeleton +<%page expression_filter="h"/> +<%inherit file="skeleton.mako" /> - -#def title +<%def name="title()"> Intro -#end def + -#def body

XVM — Virtual Servers for MIT

xvm.mit.edu is a virtualization service for the @@ -18,7 +16,7 @@ to any Athena account holder.

MIT users:

Log in to XVM now +href="https://${config.web.hostname}/"> Log in to XVM now

You'll need an MIT personal certificate.

@@ -56,5 +54,3 @@ contributors.

Questions and feedback welcome at xvm@mit.edu.

- -#end def diff --git a/code/templates/vnc.mako b/code/templates/vnc.mako new file mode 100644 index 0000000..867a145 --- /dev/null +++ b/code/templates/vnc.mako @@ -0,0 +1,23 @@ +<%page expression_filter="h" /> +<%inherit file="skeleton.mako" /> + +<%def name="title()"> +Console to ${machine.name} + + + +% if not on: +

Your machine appears to be off.

+% elif not has_vnc: +

Your machine appears to not be accepting VNC connections. Perhaps you have a ParaVM machine?

+% endif + +

See tips about framebuffer and other issues.

+ + + + + + + diff --git a/code/templates/vnc.tmpl b/code/templates/vnc.tmpl deleted file mode 100644 index c61b4f6..0000000 --- a/code/templates/vnc.tmpl +++ /dev/null @@ -1,25 +0,0 @@ -#from skeleton import skeleton -#extends skeleton - -#def title -Console to $machine.name -#end def - -#def body - -#if not $on -

Your machine appears to be off.

-#else if not $has_vnc -

Your machine appears to not be accepting VNC connections. Perhaps you have a ParaVM machine?

-#end if -

See tips about framebuffer and other issues.

- - - - - - - - -#end def diff --git a/code/unauth.fcgi b/code/unauth.fcgi new file mode 120000 index 0000000..b94e858 --- /dev/null +++ b/code/unauth.fcgi @@ -0,0 +1 @@ +invirt.fcgi \ No newline at end of file diff --git a/code/validation.py b/code/validation.py old mode 100644 new mode 100755 index 875115c..9b7a0b0 --- a/code/validation.py +++ b/code/validation.py @@ -258,9 +258,6 @@ def testContact(user, contact, machine=None): raise InvalidInput('contact', contact, "Not a valid email.") return contact -def testDisk(user, disksize, machine=None): - return disksize - def testName(user, name, machine=None): if name is None: return None diff --git a/code/view.py b/code/view.py new file mode 100644 index 0000000..9452cf0 --- /dev/null +++ b/code/view.py @@ -0,0 +1,168 @@ +import os, sys + +import cherrypy +from mako.template import Template +from mako.lookup import TemplateLookup +import simplejson +import datetime, decimal +from StringIO import StringIO +from invirt.config import structs as config +from webcommon import State + + +class MakoHandler(cherrypy.dispatch.LateParamPageHandler): + """Callable which processes a dictionary, returning the rendered body.""" + + def __init__(self, template, next_handler, + content_type='text/html; charset=utf-8'): + self.template = template + self.next_handler = next_handler + self.content_type = content_type + + def __call__(self): + env = globals().copy() + env.update(self.next_handler()) + cherrypy.response.headers['Content-Type'] = self.content_type + return self.template.render(**env) + + +class MakoLoader(object): + + def __init__(self): + self.lookups = {} + + def get_lookup(self, directories, module_directory=None, + collection_size=-1, imports=[], **kwargs): + # Find the appropriate template lookup. + key = (tuple(directories), module_directory) + try: + lookup = self.lookups[key] + except KeyError: + lookup = TemplateLookup(directories=directories, + module_directory=module_directory, + collection_size=collection_size, + default_filters=['decode.utf8'], + input_encoding='utf-8', + output_encoding='utf-8', + imports=imports, + ) + self.lookups[key] = lookup + return lookup + + def __call__(self, filename, directories, module_directory=None, + collection_size=-1, content_type='text/html; charset=utf-8', + imports=[]): + cherrypy.request.lookup = lookup = self.get_lookup( + directories, module_directory, collection_size, imports) + cherrypy.request.template = t = lookup.get_template(filename) + cherrypy.request.handler = MakoHandler( + t, cherrypy.request.handler, content_type) + +cherrypy.tools.mako = cherrypy.Tool('on_start_resource', MakoLoader()) + + +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 catchStderr(): + old_handler = cherrypy.request.handler + def wrapper(*args, **kwargs): + sys.stderr = StringIO() + ret = old_handler(*args, **kwargs) + e = revertStandardError() + if e: + if isinstance(ret, dict): + ret["error_text"] = e + return ret + if old_handler: + cherrypy.request.handler = wrapper + +cherrypy.tools.catch_stderr = cherrypy.Tool('before_handler', catchStderr) + + +class JSONEncoder(simplejson.JSONEncoder): + def default(self, obj): + if isinstance(obj, datetime.datetime): + return str(obj) + elif isinstance(obj, decimal.Decimal): + return float(obj) + else: + return simplejson.JSONEncoder.default(self, obj) + + +def jsonify_tool_callback(*args, **kwargs): + if not cherrypy.request.cached: + response = cherrypy.response + response.headers['Content-Type'] = 'text/javascript' + response.body = JSONEncoder().iterencode(response.body) + +cherrypy.tools.jsonify = cherrypy.Tool('before_finalize', + jsonify_tool_callback, priority=30) + + +def require_login(): + """If the user isn't logged in, raise 403 with an error.""" + if cherrypy.request.login is False: + raise cherrypy.HTTPError(403, + "You are not authorized to access that resource") + +cherrypy.tools.require_login = cherrypy.Tool('on_start_resource', + require_login, priority=150) + + +def require_POST(): + """If the request isn't a POST request, raise 405 Method Not Allowed""" + if cherrypy.request.method != "POST": + raise cherrypy.HTTPError(405, + "You must submit this request with POST") + +cherrypy.tools.require_POST = cherrypy.Tool('on_start_resource', + require_POST, priority=150) + + +def remote_user_login(): + """Get remote user from SSL or GSSAPI, and store in request object. + +Get the current user based on environment variables set by SSL or +GSSAPI, and store it in the attribute cherrpy.request.login. + +Per the CherryPy API (http://www.cherrypy.org/wiki/RequestObject#login), +the attribute is set to the username on successful login, to False on +failed login, and is left at None if the user attempted no authentication. +""" + environ = cherrypy.request.wsgi_environ + user = environ.get('REMOTE_USER') + if user is None: + return + if environ.get('AUTH_TYPE') == 'Negotiate': + # Convert the krb5 principal into a krb4 username + if not user.endswith('@%s' % config.kerberos.realm): + cherrypy.request.login = False # failed to log in + else: + cherrypy.request.login = user.split('@')[0].replace('/', '.') + else: + cherrypy.request.login = user + +cherrypy.tools.remote_user_login = cherrypy.Tool('on_start_resource', + remote_user_login, priority=50) + + +def invirtwebstate_init(): + """Initialize the cherrypy.request.state object from Invirt""" + if not hasattr(cherrypy.request, "state"): + cherrypy.request.state = State(cherrypy.request.login) + +cherrypy.tools.invirtwebstate = cherrypy.Tool('on_start_resource', + invirtwebstate_init, priority=100) + + +class View(object): + _cp_config = {'tools.mako.directories': + [os.path.join(os.path.dirname(__file__),'templates')]} diff --git a/debian/changelog b/debian/changelog index 5479ee5..b37745c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,21 @@ +invirt-web (0.1.0) unstable; urgency=low + + [Quentin Smith] + * Switch to CherryPy in place of our home-grown web framework. + * Switch from the Cheetah templating engine to the Mako templating engine. + * New URI scheme: /machine// + rather than /?machine_id= . + * Fix power-on/power-off/reboot buttons for IE <=8. + * Move some bits of presentation code from Python into templates. + * Clarify that Windows licenses are available from MIT for staff. + + [Evan Broder] + * Show newlines from descriptions in list page. + * Only aklog to a cell if encryption is actually needed. + * Re-arrange the authz configuration. + + -- Greg Price Sat, 19 Dec 2009 21:53:40 -0500 + invirt-web (0.0.24) unstable; urgency=low * Update authorization code for new config structure. diff --git a/debian/control b/debian/control index 386b107..e22a9ff 100644 --- a/debian/control +++ b/debian/control @@ -16,8 +16,9 @@ Depends: ${misc:Depends}, libapache2-mod-auth-sslcert, libapache2-mod-auth-kerb, debathena-ssl-certificates, # python libraries - python-flup, python-cheetah, python-simplejson, - python-dns, python-dnspython, + python-flup, python-simplejson, + python-dns, python-dnspython, python-cherrypy3, + python-mako, # misc kstart, debathena-afs-config, openafs-modules-xen, diff --git a/debian/copyright b/debian/copyright index 361ca8f..e37a78f 100644 --- a/debian/copyright +++ b/debian/copyright @@ -14,3 +14,8 @@ Copyright : On Debian systems, the complete text of the GNU General Public License can be found in the file /usr/share/common-licenses/GPL. + +The file "code/static/power_installing.png" is from the Human-O2 icon +set by Oliver Scholtz and is released under the "GNU/GPL" (source: +http://www.iconfinder.net/icondetails/24350/128/ - +http://schollidesign.deviantart.com/art/Human-O2-Iconset-105344123) diff --git a/files/etc/apache2/sites-available/default.mako b/files/etc/apache2/sites-available/default.mako index dcbc191..ac0cf4f 100644 --- a/files/etc/apache2/sites-available/default.mako +++ b/files/etc/apache2/sites-available/default.mako @@ -22,10 +22,8 @@ NameVirtualHost *:80 RewriteRule ^/admin/static(.*) /static/$1 [L] RewriteRule ^/trac(.*) ${tracuri}$1 [R,L] RewriteRule ^/invirt - [L] - RewriteRule ^/sipb-xen(.*) /invirt$1 [PT] RewriteRule ^/kill.cgi - [L] - RewriteRule ^/~ - [L] - RewriteRule ^/(.*) /var/www/invirt-web/main.fcgi/$1 [L] + RewriteRule ^/(.*) /var/www/invirt-web/unauth.fcgi/$1 [L] ErrorLog /var/log/apache2/error.log diff --git a/files/etc/apache2/sites-available/ssl.mako b/files/etc/apache2/sites-available/ssl.mako index 3ce7e5f..372563f 100644 --- a/files/etc/apache2/sites-available/ssl.mako +++ b/files/etc/apache2/sites-available/ssl.mako @@ -26,8 +26,7 @@ ${caller.body()} RewriteRule ^/admin/static(.*) /static/$1 [L] RewriteRule ^/trac(.*) ${tracuri}$1 [R,L] RewriteRule ^/kill.cgi - [L] - RewriteRule ^/~ - [L] - RewriteRule ^/(.*) /var/www/invirt-web/main.fcgi/$1 [L] + RewriteRule ^/(.*) /var/www/invirt-web/auth.fcgi/$1 [L] RewriteLog /var/log/apache2/rewrite.log RewriteLogLevel 0