From: Evan Broder 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} +%def> + +
${command} ${machine.name} was successful.
+ 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 - -#else - -#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 - -#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> -#def bodyUh-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> + +<%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> + +<%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> + +<%def name="vmTypeList(default=None)"> +% for vmtype in (('linux-hvm', 'HVM'), ('linux', 'ParaVM'), ): + +% endfor +%def> + +<%def name="errorRow(value, err)"> +% if err and err.err_field == value: +
Hello world!
+ +kwargs:
++${repr(kwargs)} ++ +
Your request:
+ ++${repr(dir(request))} ++ +<%def name="title()"> +helloworld +%def> 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 +%def> + +% 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: +${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 -$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> + +<%def name="infoTable()"> +${key}: | ${value} |
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 + +%def> + +$key: | $value |
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 - -#end def - -#def body -Your input was bad:
+Field | value | reason |
${err_field} | ${err_value} | ${errorMessage} |
Your input was bad:
-operation | Field | value | reason |
$op | $err_field | $err_value | $errorMessage |
${cant_add_vm}
+% else: +We had a problem with your request:
+% elif new_machine: +Congratulations! You successfully created a new VM called ${new_machine}.
+% endif + +% endif +%def> + +<%def name="machineRow(machine)"> ++ | Name | +Memory | +Owner${self.fn.helppopup('Owner')} | +Administrator${self.fn.helppopup('Administrator')} | +IP | +Uptime | +VNC | +
---|
You don't currently control any VMs.
+% endif + +$cant_add_vm
-#else -We had a problem with your request:
-#else if $varExists('new_machine') -Congratulations! You successfully created a new VM called $new_machine.
-#end if - -#end if -#end def - -#def machineRow($machine) -- | Name | -Memory | -Owner#slurp -#filter None -$helppopup('Owner')#slurp -#end filter - | -Administrator#slurp -#filter None -$helppopup('Administrator')#slurp -#end filter - | -IP | -Uptime | -VNC | -
---|
You don't currently control any VMs.
-#end if - -We are in the process of modifying the service. Things likely will not work.
+STDERR:
${error_text}+
Welcome, ${cherrypy.request.login}. +% if cherrypy.request.state.isadmin: +You are currently authenticated as an administrator. +% endif +
+% endif + + + +% endif + +We are in the process of modifying the service. Things likely will not work.
-STDERR:
$error_text-#end if -
Welcome, $user.
- - -#end if -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} +%def> + + +% 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/