X-Git-Url: http://xvm.mit.edu/gitweb/invirt/packages/invirt-web.git/blobdiff_plain/abf533f0d5417dbed5a0544781b1b54ba131084f..52d630b4ed2f7bbdd61b3eada7a2c38d64c077cb:/code/main.py?ds=sidebyside diff --git a/code/main.py b/code/main.py index b0f6674..f235c53 100755 --- a/code/main.py +++ b/code/main.py @@ -8,21 +8,14 @@ import datetime import hmac 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): @@ -32,8 +25,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 @@ -44,13 +35,351 @@ 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 + +class InvirtUnauthWeb(View): + @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 + + @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.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 + + @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)
+ 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.
+
+ 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')
+ try:
+ d = controls.commandResult(cherrypy.request.login,
+ cherrypy.request.state,
+ command_name, machine_id, kwargs)
+ if d['command'] == 'Delete VM':
+ back = 'list'
+ 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 Checkpoint:
def __init__(self):
@@ -67,46 +396,6 @@ class Checkpoint:
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) - class Defaults: """Class to store default values for fields.""" memory = 256 @@ -115,27 +404,17 @@ class Defaults: autoinstall = '' name = '' description = '' + administrator = '' type = 'linux-hvm' def __init__(self, max_memory=None, max_disk=None, **kws): if max_memory is not None: self.memory = min(self.memory, max_memory) if max_disk is not None: - self.max_disk = min(self.disk, max_disk) + self.disk = min(self.disk, max_disk) 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: @@ -146,33 +425,6 @@ 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=validate.vmtype, - 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.""" @@ -181,9 +433,9 @@ def getListDict(username, state): 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' @@ -195,14 +447,17 @@ def getListDict(username, state): elif m.type.hvm: has_vnc[m] = "WTF?" else: - has_vnc[m] = "ParaVM"+helppopup("ParaVM Console") + has_vnc[m] = "ParaVM" + if xmlist[m].get('autoinstall'): + installing[m] = True + else: + installing[m] = False 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, - cdrom='gutsy-i386') + owner=username) checkpoint.checkpoint('Got defaults') def sortkey(machine): return (machine.owner != username, machine.owner, machine.name) @@ -214,69 +469,24 @@ 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. XXX this should be merged with the similar logic in DNS and DHCP. """ - if nic.hostname and '.' in nic.hostname: - return nic.hostname + if nic.hostname: + hostname = nic.hostname elif nic.machine: - return nic.machine.name + '.' + config.dns.domains[0] + hostname = nic.machine.name else: return None - + if '.' in hostname: + return hostname + else: + return hostname + '.' + config.dns.domains[0] def getNicInfo(data_dict, machine): """Helper function for info, get data on nics for a machine. @@ -292,8 +502,7 @@ def getNicInfo(data_dict, machine): nic_fields = [] for i in range(len(machine.nics)): nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template]) - if not i: - data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i]) + data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i]) data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr data_dict['nic%s_ip' % i] = machine.nics[i].ip if len(machine.nics) == 1: @@ -316,44 +525,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 @@ -378,6 +561,9 @@ def modifyDict(username, state, fields): update_acl = True if hasattr(validate, 'name'): machine.name = validate.name + for n in machine.nics: + if n.hostname == oldname: + n.hostname = validate.name if hasattr(validate, 'description'): machine.description = validate.description if hasattr(validate, 'admin') and validate.admin != machine.administrator: @@ -397,107 +583,7 @@ 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.
- -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 megabytes of active ram, 50 gigabytes 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 req uires 512 MB RAM and at least 7.5 GB disk space (15 GB 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."""
@@ -517,8 +603,6 @@ def infoDict(username, state, machine):
cpu_time_float = float(main_status.get('cpu_time', 0))
cputime = datetime.timedelta(seconds=int(cpu_time_float))
checkpoint.checkpoint('Status')
- display_fields = """name uptime memory state cpu_weight on_reboot
- on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
display_fields = [('name', 'Name'),
('description', 'Description'),
('owner', 'Owner'),
@@ -532,13 +616,6 @@ def infoDict(username, state, machine):
('memory', 'RAM'),
'DISK_INFO',
('state', 'state (xen format)'),
- ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
- ('on_reboot', 'Action on VM reboot'),
- ('on_poweroff', 'Action on VM poweroff'),
- ('on_crash', 'Action on VM crash'),
- ('on_xend_start', 'Action on Xen start'),
- ('on_xend_stop', 'Action on Xen stop'),
- ('bootloader', 'Bootloader options'),
]
fields = []
machine_info = {}
@@ -579,7 +656,8 @@ def infoDict(username, state, machine):
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')
@@ -592,55 +670,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}])
-
-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.web.adminacl, 'athena.mit.edu'):
- raise InvalidInput('username', username,
- 'Not in admin group %s.' % config.web.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
@@ -657,113 +689,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.authn[0].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