Implements blueprint vnc-console-cleanup
* Creates a unified way to access vnc consoles for xenserver and libvirt * Now supports both java and websocket clients * Removes nova-vncproxy - a replacement version of this (nova-novncproxy) can be found as described in vncconsole.rst * Adds nova-xvpvncproxy, which supports a java vnc client * Adds api extension to access java and novnc access_urls * Fixes proxy server to close/shutdown sockets more cleanly * Address style feedback * Use new-style extension format * Fix setup.py * utils.gen_uuid must be wrapped like str(utils.gen_uuid()) or it can't be serialized Change-Id: I5e42e2f160e8e3476269bd64b0e8aa77e66c918c
This commit is contained in:
parent
5987ed97ff
commit
8d010cacb5
@ -44,7 +44,6 @@ from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import service
|
||||
from nova import utils
|
||||
from nova.vnc import server
|
||||
from nova.objectstore import s3server
|
||||
|
||||
|
||||
@ -60,17 +59,12 @@ if __name__ == '__main__':
|
||||
servers.append(service.WSGIService(api))
|
||||
except (Exception, SystemExit):
|
||||
logging.exception(_('Failed to load %s') % '%s-api' % api)
|
||||
# nova-vncproxy
|
||||
try:
|
||||
servers.append(server.get_wsgi_server())
|
||||
except (Exception, SystemExit):
|
||||
logging.exception(_('Failed to load %s') % 'vncproxy-wsgi')
|
||||
# nova-objectstore
|
||||
try:
|
||||
servers.append(s3server.get_wsgi_server())
|
||||
except (Exception, SystemExit):
|
||||
logging.exception(_('Failed to load %s') % 'objectstore-wsgi')
|
||||
for binary in ['nova-vncproxy', 'nova-compute', 'nova-volume',
|
||||
for binary in ['nova-xvpvncproxy', 'nova-compute', 'nova-volume',
|
||||
'nova-network', 'nova-scheduler', 'nova-vsa']:
|
||||
try:
|
||||
servers.append(service.Service.create(binary=binary))
|
||||
|
48
bin/nova-consoleauth
Executable file
48
bin/nova-consoleauth
Executable file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""VNC Console Proxy Server."""
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import service
|
||||
from nova import utils
|
||||
from nova.consoleauth import manager
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
utils.default_flagfile()
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
|
||||
server = service.Service.create(binary='nova-consoleauth')
|
||||
service.serve(server)
|
||||
service.wait()
|
@ -16,7 +16,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""VNC Console Proxy Server."""
|
||||
"""XVP VNC Console Proxy Server."""
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
@ -35,7 +35,7 @@ from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import service
|
||||
from nova import utils
|
||||
from nova.vnc import server
|
||||
from nova.vnc import xvp_proxy
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@ -43,7 +43,6 @@ if __name__ == "__main__":
|
||||
flags.FLAGS(sys.argv)
|
||||
logging.setup()
|
||||
|
||||
wsgi_server = server.get_wsgi_server()
|
||||
server = service.Service.create(binary='nova-vncproxy')
|
||||
service.serve(wsgi_server, server)
|
||||
wsgi_server = xvp_proxy.get_wsgi_server()
|
||||
service.serve(wsgi_server)
|
||||
service.wait()
|
@ -15,68 +15,117 @@
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Getting Started with the VNC Proxy
|
||||
==================================
|
||||
|
||||
Overview
|
||||
========
|
||||
The VNC Proxy is an OpenStack component that allows users of Nova to access
|
||||
their instances through a websocket enabled browser (like Google Chrome).
|
||||
their instances through vnc clients. In essex and beyond, there is support
|
||||
for for both libvirt and XenServer using both java and websocket cleints.
|
||||
|
||||
A VNC Connection works like so:
|
||||
In general, a VNC console Connection works like so:
|
||||
|
||||
* User connects over an api and gets a url like http://ip:port/?token=xyz
|
||||
* User pastes url in browser
|
||||
* Browser connects to VNC Proxy though a websocket enabled client like noVNC
|
||||
* VNC Proxy authorizes users token, maps the token to a host and port of an
|
||||
* User connects to api and gets an access_url like http://ip:port/?token=xyz
|
||||
* User pastes url in browser or as client parameter
|
||||
* Browser/Client connects to proxy
|
||||
* Proxy authorizes users token, maps the token to a host and port of an
|
||||
instance's VNC server
|
||||
* VNC Proxy initiates connection to VNC server, and continues proxying until
|
||||
* Proxy initiates connection to VNC server, and continues proxying until
|
||||
the session ends
|
||||
|
||||
|
||||
Configuring the VNC Proxy
|
||||
-------------------------
|
||||
nova-vncproxy requires a websocket enabled html client to work properly. At
|
||||
this time, the only tested client is a slightly modified fork of noVNC, which
|
||||
you can at find http://github.com/openstack/noVNC.git
|
||||
|
||||
.. todo:: add instruction for installing from package
|
||||
|
||||
noVNC must be in the location specified by --vncproxy_wwwroot, which defaults
|
||||
to /var/lib/nova/noVNC. nova-vncproxy will fail to launch until this code
|
||||
is properly installed.
|
||||
|
||||
By default, nova-vncproxy binds 0.0.0.0:6080. This can be configured with:
|
||||
|
||||
* :option:`--vncproxy_port=[port]`
|
||||
* :option:`--vncproxy_host=[host]`
|
||||
|
||||
It also binds a separate Flash socket policy listener on 0.0.0.0:843. This
|
||||
can be configured with:
|
||||
|
||||
* :option:`--vncproxy_flash_socket_policy_port=[port]`
|
||||
* :option:`--vncproxy_flash_socket_policy_host=[host]`
|
||||
Note that in general, the vnc proxy performs multiple functions:
|
||||
* Bridges between public network (where clients live) and private network
|
||||
(where vncservers live)
|
||||
* Mediates token authentication
|
||||
* Transparently deals with hypervisor-specific connection details to provide
|
||||
a uniform client experience.
|
||||
|
||||
|
||||
Enabling VNC Consoles in Nova
|
||||
-----------------------------
|
||||
At the moment, VNC support is supported only when using libvirt. To enable VNC
|
||||
Console, configure the following flags:
|
||||
About nova-consoleauth
|
||||
----------------------
|
||||
Both client proxies leverage a shared service to manage token auth called
|
||||
nova-consoleauth. This service must be running in order for for either proxy
|
||||
to work. Many proxies of either type can be run against a single
|
||||
nova-consoleauth service in a cluster configuration.
|
||||
|
||||
* :option:`--vnc_console_proxy_url=http://[proxy_host]:[proxy_port]` -
|
||||
proxy_port defaults to 6080. This url must point to nova-vncproxy
|
||||
Getting an Access Url
|
||||
---------------------
|
||||
Nova provides the ability to create access_urls through the os-consoles extension.
|
||||
Support for accessing this url is provided by novaclient:
|
||||
|
||||
# FIXME (sleepsonthefloor) update this branch name once client code merges
|
||||
git clone https://github.com/cloudbuilders/python-novaclient
|
||||
git checkout vnc_redux
|
||||
. openrc # or whatever you use to load standard nova creds
|
||||
nova get-vnc-console [server_id] [xvpvnc|novnc]
|
||||
|
||||
|
||||
Accessing VNC Consoles with a Java client
|
||||
-----------------------------------------
|
||||
To enable support for the OpenStack java vnc client in nova, nova provides the
|
||||
nova-xvpvncproxy service, which you should run to enable this feature.
|
||||
|
||||
* :option:`--xvpvncproxy_baseurl=[base url for client connections]` -
|
||||
this is the public base url to which clients will connect. "?token=abc"
|
||||
will be added to this url for the purposes of auth.
|
||||
* :option:`--xvpvncproxy_port=[port]` - port to bind (defaults to 6081)
|
||||
* :option:`--xvpvncproxy_host=[host]` - host to bind (defaults to 0.0.0.0)
|
||||
|
||||
As a client, you will need a special Java client, which is
|
||||
a version of TightVNC slightly modified to support our token auth::
|
||||
|
||||
git clone https://github.com/cloudbuilders/nova-xvpvncviewer
|
||||
cd nova-xvpvncviewer
|
||||
make
|
||||
|
||||
Then, to create a session, first request an access url using python-novaclient
|
||||
and then run the client like so::
|
||||
|
||||
# Retrieve access url
|
||||
nova get-vnc-console [server_id] xvpvnc
|
||||
# Run client
|
||||
java -jar VncViewer.jar [access_url]
|
||||
|
||||
|
||||
nova-vncproxy replaced with nova-novncproxy
|
||||
-------------------------------------------
|
||||
The previous vnc proxy, nova-vncproxy, has been removed from the nova source
|
||||
tree and replaced with an improved server that can be found externally at
|
||||
http://github.com/cloudbuilders/noVNC.git (in a branch called vnc_redux while
|
||||
this patch is in review).
|
||||
|
||||
To use this nova-novncproxy:
|
||||
git clone http://github.com/cloudbuilders/noVNC.git
|
||||
git checkout vnc_redux
|
||||
utils/nova-novncproxy --flagfile=[path to flagfile]
|
||||
|
||||
The --flagfile param should point to your nova config that includes the rabbit
|
||||
server address and credentials.
|
||||
|
||||
By default, nova-novncproxy binds 0.0.0.0:6080. This can be configured with:
|
||||
|
||||
* :option:`--novncproxy_baseurl=[base url for client connections]` -
|
||||
this is the public base url to which clients will connect. "?token=abc"
|
||||
will be added to this url for the purposes of auth.
|
||||
* :option:`--novncproxy_port=[port]`
|
||||
* :option:`--novncproxy_host=[host]`
|
||||
|
||||
|
||||
Accessing a vnc console through a web browser
|
||||
---------------------------------------------
|
||||
Retrieving an access_url for a web browser is similar to the flow for
|
||||
the java client:
|
||||
|
||||
# Retrieve access url
|
||||
nova get-vnc-console [server_id] novnc
|
||||
# Then, paste the url into your web browser
|
||||
|
||||
Support for a streamlined flow via dashboard will land in essex.
|
||||
|
||||
|
||||
Important Options
|
||||
-----------------
|
||||
* :option:`--vnc_enabled=[True|False]` - defaults to True. If this flag is
|
||||
not set your instances will launch without vnc support.
|
||||
|
||||
|
||||
Getting an instance's VNC Console
|
||||
---------------------------------
|
||||
You can access an instance's VNC Console url in the following methods:
|
||||
|
||||
* Using the direct api:
|
||||
eg: '``stack --user=admin --project=admin compute get_vnc_console instance_id=1``'
|
||||
* Support for Dashboard, and the Openstack API will be forthcoming
|
||||
|
||||
|
||||
Accessing VNC Consoles without a web browser
|
||||
--------------------------------------------
|
||||
At the moment, VNC Consoles are only supported through the web browser, but
|
||||
more general VNC support is in the works.
|
||||
* :option:`--vncserver_host=[instance vncserver host]` - defaults to 127.0.0.1
|
||||
This is the address that vncservers will bind, and should be overridden in
|
||||
production deployments as a private address. Applies to libvirt only.
|
||||
|
@ -829,15 +829,6 @@ class CloudController(object):
|
||||
instance = self.compute_api.get(context, instance_id)
|
||||
return self.compute_api.get_ajax_console(context, instance)
|
||||
|
||||
def get_vnc_console(self, context, instance_id, **kwargs):
|
||||
"""Returns vnc browser url.
|
||||
|
||||
This is an extension to the normal ec2_api"""
|
||||
ec2_id = instance_id
|
||||
instance_id = ec2utils.ec2_id_to_id(ec2_id)
|
||||
instance = self.compute_api.get(context, instance_id)
|
||||
return self.compute_api.get_vnc_console(context, instance)
|
||||
|
||||
def describe_volumes(self, context, volume_id=None, **kwargs):
|
||||
if volume_id:
|
||||
volumes = []
|
||||
|
79
nova/api/openstack/compute/contrib/consoles.py
Normal file
79
nova/api/openstack/compute/contrib/consoles.py
Normal file
@ -0,0 +1,79 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License
|
||||
|
||||
import webob
|
||||
|
||||
from nova import compute
|
||||
from nova import exception
|
||||
from nova import log as logging
|
||||
from nova.api.openstack import extensions
|
||||
from nova.api.openstack import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger('nova.api.openstack.compute.contrib.console')
|
||||
|
||||
|
||||
class ConsolesController(wsgi.Controller):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.compute_api = compute.API()
|
||||
super(ConsolesController, self).__init__(*args, **kwargs)
|
||||
|
||||
@wsgi.action('os-getVNCConsole')
|
||||
def get_vnc_console(self, req, id, body):
|
||||
"""Get text console output."""
|
||||
context = req.environ['nova.context']
|
||||
|
||||
console_type = body['os-getVNCConsole'].get('type')
|
||||
|
||||
if not console_type:
|
||||
raise webob.exc.HTTPBadRequest(_('Missing type specification'))
|
||||
|
||||
try:
|
||||
instance = self.compute_api.routing_get(context, id)
|
||||
except exception.NotFound:
|
||||
raise webob.exc.HTTPNotFound(_('Instance not found'))
|
||||
|
||||
try:
|
||||
output = self.compute_api.get_vnc_console(context,
|
||||
instance,
|
||||
console_type)
|
||||
except exception.ConsoleTypeInvalid, e:
|
||||
raise webob.exc.HTTPBadRequest(_('Invalid type specification'))
|
||||
except exception.ApiError, e:
|
||||
raise webob.exc.HTTPBadRequest(explanation=e.message)
|
||||
except exception.NotAuthorized, e:
|
||||
raise webob.exc.HTTPUnauthorized()
|
||||
|
||||
return {'console': {'type': console_type, 'url': output['url']}}
|
||||
|
||||
def get_actions(self):
|
||||
"""Return the actions the extension adds, as required by contract."""
|
||||
actions = [extensions.ActionExtension("servers", "os-getVNCConsole",
|
||||
self.get_vnc_console)]
|
||||
return actions
|
||||
|
||||
|
||||
class Consoles(extensions.ExtensionDescriptor):
|
||||
"""Interactive Console support."""
|
||||
name = "Consoles"
|
||||
alias = "os-consoles"
|
||||
namespace = "http://docs.openstack.org/compute/ext/os-consoles/api/v2"
|
||||
updated = "2011-12-23T00:00:00+00:00"
|
||||
|
||||
def get_controller_extensions(self):
|
||||
controller = ConsolesController()
|
||||
extension = extensions.ControllerExtension(self, 'servers', controller)
|
||||
return [extension]
|
@ -50,7 +50,7 @@ LOG = logging.getLogger('nova.compute.api')
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DECLARE('enable_zone_routing', 'nova.scheduler.api')
|
||||
flags.DECLARE('vncproxy_topic', 'nova.vnc')
|
||||
flags.DECLARE('consoleauth_topic', 'nova.consoleauth')
|
||||
flags.DEFINE_integer('find_host_timeout', 30,
|
||||
'Timeout after NN seconds when looking for a host.')
|
||||
|
||||
@ -1571,23 +1571,23 @@ class API(base.Base):
|
||||
output['token'])}
|
||||
|
||||
@wrap_check_policy
|
||||
def get_vnc_console(self, context, instance):
|
||||
"""Get a url to a VNC Console."""
|
||||
output = self._call_compute_message('get_vnc_console',
|
||||
context,
|
||||
instance)
|
||||
rpc.call(context, '%s' % FLAGS.vncproxy_topic,
|
||||
{'method': 'authorize_vnc_console',
|
||||
'args': {'token': output['token'],
|
||||
'host': output['host'],
|
||||
'port': output['port']}})
|
||||
def get_vnc_console(self, context, instance, console_type):
|
||||
"""Get a url to an instance Console."""
|
||||
connect_info = self._call_compute_message('get_vnc_console',
|
||||
context,
|
||||
instance,
|
||||
params={"console_type": console_type})
|
||||
|
||||
# hostignore and portignore are compatibility params for noVNC
|
||||
return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % (
|
||||
FLAGS.vncproxy_url,
|
||||
output['token'],
|
||||
'hostignore',
|
||||
'portignore')}
|
||||
rpc.call(context, '%s' % FLAGS.consoleauth_topic,
|
||||
{'method': 'authorize_console',
|
||||
'args': {'token': connect_info['token'],
|
||||
'console_type': console_type,
|
||||
'host': connect_info['host'],
|
||||
'port': connect_info['port'],
|
||||
'internal_access_path':\
|
||||
connect_info['internal_access_path']}})
|
||||
|
||||
return {'url': connect_info['access_url']}
|
||||
|
||||
@wrap_check_policy
|
||||
def get_console_output(self, context, instance, tail_length=None):
|
||||
|
@ -59,6 +59,7 @@ from nova.notifier import api as notifier
|
||||
from nova import rpc
|
||||
from nova import utils
|
||||
from nova.virt import driver
|
||||
from nova import vnc
|
||||
from nova import volume
|
||||
|
||||
|
||||
@ -1490,12 +1491,30 @@ class ComputeManager(manager.SchedulerDependentManager):
|
||||
|
||||
@exception.wrap_exception(notifier=notifier, publisher_id=publisher_id())
|
||||
@wrap_instance_fault
|
||||
def get_vnc_console(self, context, instance_uuid):
|
||||
def get_vnc_console(self, context, instance_uuid, console_type):
|
||||
"""Return connection information for a vnc console."""
|
||||
context = context.elevated()
|
||||
LOG.debug(_("instance %s: getting vnc console"), instance_uuid)
|
||||
instance_ref = self.db.instance_get_by_uuid(context, instance_uuid)
|
||||
return self.driver.get_vnc_console(instance_ref)
|
||||
|
||||
token = str(utils.gen_uuid())
|
||||
|
||||
if console_type == 'novnc':
|
||||
# For essex, novncproxy_base_url must include the full path
|
||||
# including the html file (like http://myhost/vnc_auto.html)
|
||||
access_url = '%s?token=%s' % (FLAGS.novncproxy_base_url, token)
|
||||
elif console_type == 'xvpvnc':
|
||||
access_url = '%s?token=%s' % (FLAGS.xvpvncproxy_base_url, token)
|
||||
else:
|
||||
raise exception.ConsoleTypeInvalid(console_type=console_type)
|
||||
|
||||
# Retrieve connect info from driver, and then decorate with our
|
||||
# access info token
|
||||
connect_info = self.driver.get_vnc_console(instance_ref)
|
||||
connect_info['token'] = token
|
||||
connect_info['access_url'] = access_url
|
||||
|
||||
return connect_info
|
||||
|
||||
def _attach_volume_boot(self, context, instance, volume, mountpoint):
|
||||
"""Attach a volume to an instance at boot time. So actual attach
|
||||
|
26
nova/consoleauth/__init__.py
Normal file
26
nova/consoleauth/__init__.py
Normal file
@ -0,0 +1,26 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Module to authenticate Consoles."""
|
||||
|
||||
from nova import flags
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('consoleauth_topic', 'consoleauth',
|
||||
'the topic console auth proxy nodes listen on')
|
74
nova/consoleauth/manager.py
Normal file
74
nova/consoleauth/manager.py
Normal file
@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Auth Components for Consoles."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import manager
|
||||
from nova import utils
|
||||
|
||||
|
||||
LOG = logging.getLogger('nova.consoleauth')
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_integer('console_token_ttl', 600,
|
||||
'How many seconds before deleting tokens')
|
||||
flags.DEFINE_string('consoleauth_manager',
|
||||
'nova.consoleauth.manager.ConsoleAuthManager',
|
||||
'Manager for console auth')
|
||||
|
||||
|
||||
class ConsoleAuthManager(manager.Manager):
|
||||
"""Manages token based authentication."""
|
||||
|
||||
def __init__(self, scheduler_driver=None, *args, **kwargs):
|
||||
super(ConsoleAuthManager, self).__init__(*args, **kwargs)
|
||||
self.tokens = {}
|
||||
utils.LoopingCall(self._delete_expired_tokens).start(1)
|
||||
|
||||
def _delete_expired_tokens(self):
|
||||
now = time.time()
|
||||
to_delete = []
|
||||
for k, v in self.tokens.items():
|
||||
if now - v['last_activity_at'] > FLAGS.console_token_ttl:
|
||||
to_delete.append(k)
|
||||
|
||||
for k in to_delete:
|
||||
LOG.audit(_("Deleting Expired Token: (%s)"), k)
|
||||
del self.tokens[k]
|
||||
|
||||
def authorize_console(self, context, token, console_type, host, port,
|
||||
internal_access_path):
|
||||
self.tokens[token] = {'token': token,
|
||||
'console_type': console_type,
|
||||
'host': host,
|
||||
'port': port,
|
||||
'internal_access_path': internal_access_path,
|
||||
'last_activity_at': time.time()}
|
||||
token_dict = self.tokens[token]
|
||||
LOG.audit(_("Received Token: %(token)s, %(token_dict)s)"), locals())
|
||||
|
||||
def check_token(self, context, token):
|
||||
token_valid = token in self.tokens
|
||||
LOG.audit(_("Checking Token: %(token)s, %(token_valid)s)"), locals())
|
||||
if token_valid:
|
||||
return self.tokens[token]
|
@ -679,6 +679,10 @@ class ConsoleNotFoundInPoolForInstance(ConsoleNotFound):
|
||||
"in pool %(pool_id)s could not be found.")
|
||||
|
||||
|
||||
class ConsoleTypeInvalid(Invalid):
|
||||
message = _("Invalid console type %(console_type)s ")
|
||||
|
||||
|
||||
class NoInstanceTypesFound(NotFound):
|
||||
message = _("Zero instance types found.")
|
||||
|
||||
|
97
nova/tests/api/openstack/compute/contrib/test_consoles.py
Normal file
97
nova/tests/api/openstack/compute/contrib/test_consoles.py
Normal file
@ -0,0 +1,97 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
import webob
|
||||
|
||||
from nova import compute
|
||||
from nova import exception
|
||||
from nova import test
|
||||
from nova.tests.api.openstack import fakes
|
||||
|
||||
|
||||
def fake_get_vnc_console(self, _context, _instance, _console_type):
|
||||
return {'url': 'http://fake'}
|
||||
|
||||
|
||||
def fake_get_vnc_console_invalid_type(self, _context,
|
||||
_instance, _console_type):
|
||||
raise exception.ConsoleTypeInvalid()
|
||||
|
||||
|
||||
def fake_get(self, context, instance_uuid):
|
||||
return {'uuid': instance_uuid}
|
||||
|
||||
|
||||
def fake_get_not_found(self, context, instance_uuid):
|
||||
raise exception.NotFound()
|
||||
|
||||
|
||||
class ConsolesExtensionTest(test.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ConsolesExtensionTest, self).setUp()
|
||||
self.stubs.Set(compute.API, 'get_vnc_console',
|
||||
fake_get_vnc_console)
|
||||
self.stubs.Set(compute.API, 'get', fake_get)
|
||||
|
||||
def test_get_vnc_console(self):
|
||||
body = {'os-getVNCConsole': {'type': 'novnc'}}
|
||||
req = webob.Request.blank('/v2/fake/servers/1/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
output = json.loads(res.body)
|
||||
self.assertEqual(res.status_int, 200)
|
||||
self.assertEqual(output,
|
||||
{u'console': {u'url': u'http://fake', u'type': u'novnc'}})
|
||||
|
||||
def test_get_vnc_console_no_type(self):
|
||||
self.stubs.Set(compute.API, 'get', fake_get)
|
||||
body = {'os-getVNCConsole': {}}
|
||||
req = webob.Request.blank('/v2/fake/servers/1/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 400)
|
||||
|
||||
def test_get_vnc_console_no_instance(self):
|
||||
self.stubs.Set(compute.API, 'get', fake_get_not_found)
|
||||
body = {'os-getVNCConsole': {'type': 'novnc'}}
|
||||
req = webob.Request.blank('/v2/fake/servers/1/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 404)
|
||||
|
||||
def test_get_vnc_console_invalid_type(self):
|
||||
self.stubs.Set(compute.API, 'get', fake_get)
|
||||
body = {'os-getVNCConsole': {'type': 'invalid'}}
|
||||
self.stubs.Set(compute.API, 'get_vnc_console',
|
||||
fake_get_vnc_console_invalid_type)
|
||||
req = webob.Request.blank('/v2/fake/servers/1/action')
|
||||
req.method = "POST"
|
||||
req.body = json.dumps(body)
|
||||
req.headers["content-type"] = "application/json"
|
||||
|
||||
res = req.get_response(fakes.wsgi_app())
|
||||
self.assertEqual(res.status_int, 400)
|
@ -157,6 +157,7 @@ class ExtensionControllerTest(ExtensionTestCase):
|
||||
"AdminActions",
|
||||
"Cloudpipe",
|
||||
"Console_output",
|
||||
"Consoles",
|
||||
"Createserverext",
|
||||
"DeferredDelete",
|
||||
"DiskConfig",
|
||||
|
@ -696,15 +696,40 @@ class ComputeTestCase(BaseTestCase):
|
||||
self.assert_(set(['token', 'host', 'port']).issubset(console.keys()))
|
||||
self.compute.terminate_instance(self.context, instance['uuid'])
|
||||
|
||||
def test_vnc_console(self):
|
||||
def test_novnc_vnc_console(self):
|
||||
"""Make sure we can a vnc console for an instance."""
|
||||
instance = self._create_fake_instance()
|
||||
self.compute.run_instance(self.context, instance['uuid'])
|
||||
|
||||
console = self.compute.get_vnc_console(self.context, instance['uuid'])
|
||||
console = self.compute.get_vnc_console(self.context,
|
||||
instance['uuid'],
|
||||
'novnc')
|
||||
self.assert_(console)
|
||||
self.compute.terminate_instance(self.context, instance['uuid'])
|
||||
|
||||
def test_xvpvnc_vnc_console(self):
|
||||
"""Make sure we can a vnc console for an instance."""
|
||||
instance = self._create_fake_instance()
|
||||
self.compute.run_instance(self.context, instance['uuid'])
|
||||
|
||||
console = self.compute.get_vnc_console(self.context,
|
||||
instance['uuid'],
|
||||
'xvpvnc')
|
||||
self.assert_(console)
|
||||
self.compute.terminate_instance(self.context, instance['uuid'])
|
||||
|
||||
def test_invalid_vnc_console_type(self):
|
||||
"""Make sure we can a vnc console for an instance."""
|
||||
instance = self._create_fake_instance()
|
||||
self.compute.run_instance(self.context, instance['uuid'])
|
||||
|
||||
self.assertRaises(exception.ConsoleTypeInvalid,
|
||||
self.compute.get_vnc_console,
|
||||
self.context,
|
||||
instance['uuid'],
|
||||
'invalid')
|
||||
self.compute.terminate_instance(self.context, instance['uuid'])
|
||||
|
||||
def test_diagnostics(self):
|
||||
"""Make sure we can get diagnostics for an instance."""
|
||||
instance = self._create_fake_instance()
|
||||
@ -2831,16 +2856,20 @@ class ComputeAPITestCase(BaseTestCase):
|
||||
def test_vnc_console(self):
|
||||
"""Make sure we can a vnc console for an instance."""
|
||||
def vnc_rpc_call_wrapper(*args, **kwargs):
|
||||
return {'token': 'asdf', 'host': '0.0.0.0', 'port': 8080}
|
||||
return {'token': 'asdf', 'host': '0.0.0.0',
|
||||
'port': 8080, 'access_url': None,
|
||||
'internal_access_path': None}
|
||||
|
||||
self.stubs.Set(rpc, 'call', vnc_rpc_call_wrapper)
|
||||
|
||||
instance = self._create_fake_instance()
|
||||
console = self.compute_api.get_vnc_console(self.context, instance)
|
||||
console = self.compute_api.get_vnc_console(self.context,
|
||||
instance,
|
||||
'novnc')
|
||||
self.compute_api.delete(self.context, instance)
|
||||
|
||||
def test_ajax_console(self):
|
||||
"""Make sure we can a vnc console for an instance."""
|
||||
"""Make sure we can an ajax console for an instance."""
|
||||
def ajax_rpc_call_wrapper(*args, **kwargs):
|
||||
return {'token': 'asdf', 'host': '0.0.0.0', 'port': 8080}
|
||||
|
||||
|
59
nova/tests/test_consoleauth.py
Normal file
59
nova/tests/test_consoleauth.py
Normal file
@ -0,0 +1,59 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""
|
||||
Tests for Consoleauth Code.
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from nova import context
|
||||
from nova import db
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import test
|
||||
from nova import utils
|
||||
from nova.consoleauth import manager
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
LOG = logging.getLogger('nova.tests.consoleauth')
|
||||
|
||||
|
||||
class ConsoleauthTestCase(test.TestCase):
|
||||
"""Test Case for consoleauth."""
|
||||
|
||||
def setUp(self):
|
||||
super(ConsoleauthTestCase, self).setUp()
|
||||
self.manager = utils.import_object(FLAGS.consoleauth_manager)
|
||||
self.context = context.get_admin_context()
|
||||
self.old_ttl = FLAGS.console_token_ttl
|
||||
|
||||
def tearDown(self):
|
||||
super(ConsoleauthTestCase, self).tearDown()
|
||||
FLAGS.console_token_ttl = self.old_ttl
|
||||
|
||||
def test_tokens_expire(self):
|
||||
"""Test that tokens expire correctly."""
|
||||
token = 'mytok'
|
||||
FLAGS.console_token_ttl = 1
|
||||
self.manager.authorize_console(self.context, token, 'novnc',
|
||||
'127.0.0.1', 'host', '')
|
||||
self.assertTrue(self.manager.check_token(self.context, token))
|
||||
time.sleep(1.1)
|
||||
self.assertFalse(self.manager.check_token(self.context, token))
|
@ -294,7 +294,7 @@ class _VirtDriverTestCase(test.TestCase):
|
||||
def test_get_vnc_console(self):
|
||||
instance_ref, network_info = self._get_running_instance()
|
||||
vnc_console = self.connection.get_vnc_console(instance_ref)
|
||||
self.assertIn('token', vnc_console)
|
||||
self.assertIn('internal_access_path', vnc_console)
|
||||
self.assertIn('host', vnc_console)
|
||||
self.assertIn('port', vnc_console)
|
||||
|
||||
|
@ -195,6 +195,10 @@ class ComputeDriver(object):
|
||||
# TODO(Vek): Need to pass context in for access to auth_token
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_vnc_console(self, instance):
|
||||
# TODO(Vek): Need to pass context in for access to auth_token
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_diagnostics(self, instance):
|
||||
"""Return data about VM diagnostics"""
|
||||
# TODO(Vek): Need to pass context in for access to auth_token
|
||||
|
@ -225,7 +225,7 @@ class FakeConnection(driver.ComputeDriver):
|
||||
'port': 6969}
|
||||
|
||||
def get_vnc_console(self, instance):
|
||||
return {'token': 'FAKETOKEN',
|
||||
return {'internal_access_path': 'FAKE',
|
||||
'host': 'fakevncconsole.com',
|
||||
'port': 6969}
|
||||
|
||||
|
@ -781,10 +781,9 @@ class LibvirtConnection(driver.ComputeDriver):
|
||||
return graphic.getAttribute('port')
|
||||
|
||||
port = get_vnc_port_for_instance(instance['name'])
|
||||
token = str(uuid.uuid4())
|
||||
host = instance['host']
|
||||
|
||||
return {'token': token, 'host': host, 'port': port}
|
||||
return {'host': host, 'port': port, 'internal_access_path': None}
|
||||
|
||||
@staticmethod
|
||||
def _cache_image(fn, target, fname, cow=False, *args, **kwargs):
|
||||
|
@ -59,6 +59,9 @@ flags.DEFINE_integer('xenapi_running_timeout', 60,
|
||||
flags.DEFINE_string('xenapi_vif_driver',
|
||||
'nova.virt.xenapi.vif.XenAPIBridgeDriver',
|
||||
'The XenAPI VIF driver using XenServer Network APIs.')
|
||||
flags.DEFINE_string('dom0_address',
|
||||
'169.254.0.1',
|
||||
'Ip address of dom0. Override for multi-host vnc.')
|
||||
flags.DEFINE_bool('xenapi_generate_swap',
|
||||
False,
|
||||
'Whether to generate swap (False means fetching it'
|
||||
@ -1381,6 +1384,17 @@ class VMOps(object):
|
||||
# TODO: implement this!
|
||||
return 'http://fakeajaxconsole/fake_url'
|
||||
|
||||
def get_vnc_console(self, instance):
|
||||
"""Return connection info for a vnc console."""
|
||||
vm_ref = self._get_vm_opaque_ref(instance)
|
||||
session_id = self._session.get_session_id()
|
||||
path = "/console?ref=%s&session_id=%s"\
|
||||
% (str(vm_ref), session_id)
|
||||
|
||||
# NOTE: XS5.6sp2+ use http over port 80 for xenapi com
|
||||
return {'host': FLAGS.dom0_address, 'port': 80,
|
||||
'internal_access_path': path}
|
||||
|
||||
def host_power_action(self, host, action):
|
||||
"""Reboots or shuts down the host."""
|
||||
args = {"action": json.dumps(action)}
|
||||
|
@ -338,6 +338,10 @@ class XenAPIConnection(driver.ComputeDriver):
|
||||
"""Return link to instance's ajax console"""
|
||||
return self._vmops.get_ajax_console(instance)
|
||||
|
||||
def get_vnc_console(self, instance):
|
||||
"""Return link to instance's ajax console"""
|
||||
return self._vmops.get_vnc_console(instance)
|
||||
|
||||
@staticmethod
|
||||
def get_host_ip_addr():
|
||||
xs_url = urlparse.urlparse(FLAGS.xenapi_connection_url)
|
||||
@ -493,6 +497,11 @@ class XenAPISession(object):
|
||||
"""Stubout point. This can be replaced with a mock xenapi module."""
|
||||
return __import__('XenAPI')
|
||||
|
||||
def get_session_id(self):
|
||||
"""Return a string session_id. Used for vnc consoles."""
|
||||
with self._get_session() as session:
|
||||
return str(session._session)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _get_session(self):
|
||||
"""Return exclusive session for scope of with statement"""
|
||||
|
@ -22,13 +22,15 @@ from nova import flags
|
||||
|
||||
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('vncproxy_topic', 'vncproxy',
|
||||
'the topic vnc proxy nodes listen on')
|
||||
flags.DEFINE_string('vncproxy_url',
|
||||
'http://127.0.0.1:6080',
|
||||
flags.DEFINE_string('novncproxy_base_url',
|
||||
'http://127.0.0.1:6080/vnc_auto.html',
|
||||
'location of vnc console proxy, \
|
||||
in the form "http://127.0.0.1:6080"')
|
||||
flags.DEFINE_string('vncserver_host', '0.0.0.0',
|
||||
in the form "http://127.0.0.1:6080/vnc_auto.html"')
|
||||
flags.DEFINE_string('xvpvncproxy_base_url',
|
||||
'http://127.0.0.1:6081/console',
|
||||
'location of nova xvp vnc console proxy, \
|
||||
in the form "http://127.0.0.1:6081/console"')
|
||||
flags.DEFINE_string('vncserver_host', '127.0.0.1',
|
||||
'the host interface on which vnc server should listen')
|
||||
flags.DEFINE_bool('vnc_enabled', True,
|
||||
'enable vnc related features')
|
||||
|
135
nova/vnc/auth.py
135
nova/vnc/auth.py
@ -1,135 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2010 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Auth Components for VNC Console."""
|
||||
|
||||
import time
|
||||
import urlparse
|
||||
import webob
|
||||
|
||||
from nova import context
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import manager
|
||||
from nova import rpc
|
||||
from nova import utils
|
||||
from nova import vnc
|
||||
|
||||
|
||||
LOG = logging.getLogger('nova.vncproxy')
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
|
||||
class VNCNovaAuthMiddleware(object):
|
||||
"""Implementation of Middleware to Handle Nova Auth."""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.token_cache = {}
|
||||
utils.LoopingCall(self.delete_expired_cache_items).start(1)
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
token = req.params.get('token')
|
||||
|
||||
if not token:
|
||||
referrer = req.environ.get('HTTP_REFERER')
|
||||
auth_params = urlparse.parse_qs(urlparse.urlparse(referrer).query)
|
||||
if 'token' in auth_params:
|
||||
token = auth_params['token'][0]
|
||||
|
||||
connection_info = self.get_token_info(token)
|
||||
if not connection_info:
|
||||
LOG.audit(_("Unauthorized Access: (%s)"), req.environ)
|
||||
return webob.exc.HTTPForbidden(detail='Unauthorized')
|
||||
|
||||
if req.path == vnc.proxy.WS_ENDPOINT:
|
||||
req.environ['vnc_host'] = connection_info['host']
|
||||
req.environ['vnc_port'] = int(connection_info['port'])
|
||||
|
||||
return req.get_response(self.app)
|
||||
|
||||
def get_token_info(self, token):
|
||||
if token in self.token_cache:
|
||||
return self.token_cache[token]
|
||||
|
||||
rval = rpc.call(context.get_admin_context(),
|
||||
FLAGS.vncproxy_topic,
|
||||
{"method": "check_token", "args": {'token': token}})
|
||||
if rval:
|
||||
self.token_cache[token] = rval
|
||||
return rval
|
||||
|
||||
def delete_expired_cache_items(self):
|
||||
now = time.time()
|
||||
to_delete = []
|
||||
for k, v in self.token_cache.items():
|
||||
if now - v['last_activity_at'] > FLAGS.vnc_token_ttl:
|
||||
to_delete.append(k)
|
||||
|
||||
for k in to_delete:
|
||||
del self.token_cache[k]
|
||||
|
||||
|
||||
class LoggingMiddleware(object):
|
||||
"""Middleware for basic vnc-specific request logging."""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
if req.path == vnc.proxy.WS_ENDPOINT:
|
||||
LOG.info(_("Received Websocket Request: %s"), req.url)
|
||||
else:
|
||||
LOG.info(_("Received Request: %s"), req.url)
|
||||
|
||||
return req.get_response(self.app)
|
||||
|
||||
|
||||
class VNCProxyAuthManager(manager.Manager):
|
||||
"""Manages token based authentication."""
|
||||
|
||||
def __init__(self, scheduler_driver=None, *args, **kwargs):
|
||||
super(VNCProxyAuthManager, self).__init__(*args, **kwargs)
|
||||
self.tokens = {}
|
||||
utils.LoopingCall(self._delete_expired_tokens).start(1)
|
||||
|
||||
def authorize_vnc_console(self, context, token, host, port):
|
||||
self.tokens[token] = {'host': host,
|
||||
'port': port,
|
||||
'last_activity_at': time.time()}
|
||||
token_dict = self.tokens[token]
|
||||
LOG.audit(_("Received Token: %(token)s, %(token_dict)s)"), locals())
|
||||
|
||||
def check_token(self, context, token):
|
||||
token_valid = token in self.tokens
|
||||
LOG.audit(_("Checking Token: %(token)s, %(token_valid)s)"), locals())
|
||||
if token_valid:
|
||||
return self.tokens[token]
|
||||
|
||||
def _delete_expired_tokens(self):
|
||||
now = time.time()
|
||||
to_delete = []
|
||||
for k, v in self.tokens.items():
|
||||
if now - v['last_activity_at'] > FLAGS.vnc_token_ttl:
|
||||
to_delete.append(k)
|
||||
|
||||
for k in to_delete:
|
||||
LOG.audit(_("Deleting Expired Token: %s)"), k)
|
||||
del self.tokens[k]
|
@ -1,130 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2010 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Eventlet WSGI Services to proxy VNC. No nova deps."""
|
||||
|
||||
import base64
|
||||
import os
|
||||
|
||||
import eventlet
|
||||
from eventlet import websocket
|
||||
|
||||
import webob
|
||||
|
||||
|
||||
WS_ENDPOINT = '/data'
|
||||
|
||||
|
||||
class WebsocketVNCProxy(object):
|
||||
"""Class to proxy from websocket to vnc server."""
|
||||
|
||||
def __init__(self, wwwroot):
|
||||
self.wwwroot = wwwroot
|
||||
self.whitelist = {}
|
||||
for root, dirs, files in os.walk(wwwroot):
|
||||
hidden_dirs = []
|
||||
for d in dirs:
|
||||
if d.startswith('.'):
|
||||
hidden_dirs.append(d)
|
||||
for d in hidden_dirs:
|
||||
dirs.remove(d)
|
||||
for name in files:
|
||||
if not str(name).startswith('.'):
|
||||
filename = os.path.join(root, name)
|
||||
self.whitelist[filename] = True
|
||||
|
||||
def get_whitelist(self):
|
||||
return self.whitelist.keys()
|
||||
|
||||
def sock2ws(self, source, dest):
|
||||
try:
|
||||
while True:
|
||||
d = source.recv(32384)
|
||||
if d == '':
|
||||
break
|
||||
d = base64.b64encode(d)
|
||||
dest.send(d)
|
||||
except Exception:
|
||||
source.close()
|
||||
dest.close()
|
||||
|
||||
def ws2sock(self, source, dest):
|
||||
try:
|
||||
while True:
|
||||
d = source.wait()
|
||||
if d is None:
|
||||
break
|
||||
d = base64.b64decode(d)
|
||||
dest.sendall(d)
|
||||
except Exception:
|
||||
source.close()
|
||||
dest.close()
|
||||
|
||||
def proxy_connection(self, environ, start_response):
|
||||
@websocket.WebSocketWSGI
|
||||
def _handle(client):
|
||||
server = eventlet.connect((client.environ['vnc_host'],
|
||||
client.environ['vnc_port']))
|
||||
t1 = eventlet.spawn(self.ws2sock, client, server)
|
||||
t2 = eventlet.spawn(self.sock2ws, server, client)
|
||||
t1.wait()
|
||||
t2.wait()
|
||||
_handle(environ, start_response)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
req = webob.Request(environ)
|
||||
if req.path == WS_ENDPOINT:
|
||||
return self.proxy_connection(environ, start_response)
|
||||
else:
|
||||
if req.path == '/':
|
||||
fname = '/vnc_auto.html'
|
||||
else:
|
||||
fname = req.path
|
||||
|
||||
fname = (self.wwwroot + fname).replace('//', '/')
|
||||
if not fname in self.whitelist:
|
||||
start_response('404 Not Found',
|
||||
[('content-type', 'text/html')])
|
||||
return "Not Found"
|
||||
|
||||
base, ext = os.path.splitext(fname)
|
||||
if ext == '.js':
|
||||
mimetype = 'application/javascript'
|
||||
elif ext == '.css':
|
||||
mimetype = 'text/css'
|
||||
elif ext in ['.svg', '.jpg', '.png', '.gif']:
|
||||
mimetype = 'image'
|
||||
else:
|
||||
mimetype = 'text/html'
|
||||
|
||||
start_response('200 OK', [('content-type', mimetype)])
|
||||
return open(os.path.join(fname)).read()
|
||||
|
||||
|
||||
class DebugMiddleware(object):
|
||||
"""Debug middleware. Skip auth, get vnc connect info from query string."""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
if req.path == WS_ENDPOINT:
|
||||
req.environ['vnc_host'] = req.params.get('host')
|
||||
req.environ['vnc_port'] = int(req.params.get('port'))
|
||||
return req.get_response(self.app)
|
@ -1,100 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2010 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Auth Components for VNC Console."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import version
|
||||
from nova import wsgi
|
||||
from nova.vnc import auth
|
||||
from nova.vnc import proxy
|
||||
|
||||
|
||||
LOG = logging.getLogger('nova.vncproxy')
|
||||
FLAGS = flags.FLAGS
|
||||
flags.DEFINE_string('vncproxy_wwwroot', '/var/lib/nova/noVNC/',
|
||||
'Full path to noVNC directory')
|
||||
flags.DEFINE_boolean('vnc_debug', False,
|
||||
'Enable debugging features, like token bypassing')
|
||||
flags.DEFINE_integer('vncproxy_port', 6080,
|
||||
'Port that the VNC proxy should bind to')
|
||||
flags.DEFINE_string('vncproxy_host', '0.0.0.0',
|
||||
'Address that the VNC proxy should bind to')
|
||||
flags.DEFINE_integer('vncproxy_flash_socket_policy_port', 843,
|
||||
'Port that the socket policy listener should bind to')
|
||||
flags.DEFINE_string('vncproxy_flash_socket_policy_host', '0.0.0.0',
|
||||
'Address that the socket policy listener should bind to')
|
||||
flags.DEFINE_integer('vnc_token_ttl', 300,
|
||||
'How many seconds before deleting tokens')
|
||||
flags.DEFINE_string('vncproxy_manager', 'nova.vnc.auth.VNCProxyAuthManager',
|
||||
'Manager for vncproxy auth')
|
||||
|
||||
|
||||
def get_wsgi_server():
|
||||
LOG.audit(_("Starting nova-vncproxy node (version %s)"),
|
||||
version.version_string_with_vcs())
|
||||
|
||||
if not (os.path.exists(FLAGS.vncproxy_wwwroot) and
|
||||
os.path.exists(FLAGS.vncproxy_wwwroot + '/vnc_auto.html')):
|
||||
LOG.info(_("Missing vncproxy_wwwroot (version %s)"),
|
||||
FLAGS.vncproxy_wwwroot)
|
||||
LOG.info(_("You need a slightly modified version of noVNC "
|
||||
"to work with the nova-vnc-proxy"))
|
||||
LOG.info(_("Check out the most recent nova noVNC code: %s"),
|
||||
"git://github.com/sleepsonthefloor/noVNC.git")
|
||||
LOG.info(_("And drop it in %s"), FLAGS.vncproxy_wwwroot)
|
||||
sys.exit(1)
|
||||
|
||||
app = proxy.WebsocketVNCProxy(FLAGS.vncproxy_wwwroot)
|
||||
|
||||
LOG.audit(_("Allowing access to the following files: %s"),
|
||||
app.get_whitelist())
|
||||
|
||||
with_logging = auth.LoggingMiddleware(app)
|
||||
|
||||
if FLAGS.vnc_debug:
|
||||
with_auth = proxy.DebugMiddleware(with_logging)
|
||||
else:
|
||||
with_auth = auth.VNCNovaAuthMiddleware(with_logging)
|
||||
|
||||
wsgi_server = wsgi.Server("VNC Proxy",
|
||||
with_auth,
|
||||
host=FLAGS.vncproxy_host,
|
||||
port=FLAGS.vncproxy_port)
|
||||
wsgi_server.start_tcp(handle_flash_socket_policy,
|
||||
host=FLAGS.vncproxy_flash_socket_policy_host,
|
||||
port=FLAGS.vncproxy_flash_socket_policy_port)
|
||||
return wsgi_server
|
||||
|
||||
|
||||
def handle_flash_socket_policy(socket):
|
||||
LOG.info(_("Received connection on flash socket policy port"))
|
||||
|
||||
fd = socket.makefile('rw')
|
||||
expected_command = "<policy-file-request/>"
|
||||
if expected_command in fd.read(len(expected_command) + 1):
|
||||
LOG.info(_("Received valid flash socket policy request"))
|
||||
fd.write('<?xml version="1.0"?><cross-domain-policy><allow-'
|
||||
'access-from domain="*" to-ports="%d" /></cross-'
|
||||
'domain-policy>' % (FLAGS.vncproxy_port))
|
||||
fd.flush()
|
||||
socket.close()
|
181
nova/vnc/xvp_proxy.py
Normal file
181
nova/vnc/xvp_proxy.py
Normal file
@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 Openstack, LLC.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Eventlet WSGI Services to proxy VNC for XCP protocol."""
|
||||
|
||||
import base64
|
||||
import os
|
||||
import socket
|
||||
import webob
|
||||
|
||||
import eventlet
|
||||
import eventlet.green
|
||||
import eventlet.greenio
|
||||
import eventlet.wsgi
|
||||
|
||||
from nova import context
|
||||
from nova import flags
|
||||
from nova import log as logging
|
||||
from nova import rpc
|
||||
from nova import version
|
||||
from nova import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger('nova.xvpvncproxy')
|
||||
FLAGS = flags.FLAGS
|
||||
|
||||
flags.DECLARE('consoleauth_topic', 'nova.consoleauth')
|
||||
flags.DEFINE_integer('xvpvncproxy_port', 6081,
|
||||
'Port that the XCP VNC proxy should bind to')
|
||||
flags.DEFINE_string('xvpvncproxy_host', '0.0.0.0',
|
||||
'Address that the XCP VNC proxy should bind to')
|
||||
|
||||
|
||||
class XCPVNCProxy(object):
|
||||
"""Class to use the xvp auth protocol to proxy instance vnc consoles."""
|
||||
|
||||
def one_way_proxy(self, source, dest):
|
||||
"""Proxy tcp connection from source to dest."""
|
||||
while True:
|
||||
try:
|
||||
d = source.recv(32384)
|
||||
except Exception as e:
|
||||
d = None
|
||||
|
||||
# If recv fails, send a write shutdown the other direction
|
||||
if d is None or len(d) == 0:
|
||||
dest.shutdown(socket.SHUT_WR)
|
||||
break
|
||||
# If send fails, terminate proxy in both directions
|
||||
try:
|
||||
# sendall raises an exception on write error, unlike send
|
||||
dest.sendall(d)
|
||||
except Exception as e:
|
||||
source.close()
|
||||
dest.close()
|
||||
break
|
||||
|
||||
def handshake(self, req, connect_info, sockets):
|
||||
"""Execute hypervisor-specific vnc auth handshaking (if needed)."""
|
||||
host = connect_info['host']
|
||||
port = int(connect_info['port'])
|
||||
|
||||
server = eventlet.connect((host, port))
|
||||
|
||||
# Handshake as necessary
|
||||
if connect_info.get('internal_access_path'):
|
||||
server.sendall("CONNECT %s HTTP/1.1\r\n\r\n" %
|
||||
connect_info['internal_access_path'])
|
||||
|
||||
data = ""
|
||||
while True:
|
||||
b = server.recv(1)
|
||||
if b:
|
||||
data += b
|
||||
if data.find("\r\n\r\n") != -1:
|
||||
if not data.split("\r\n")[0].find("200"):
|
||||
LOG.audit(_("Error in handshake: %s"), data)
|
||||
return
|
||||
break
|
||||
|
||||
if not b or len(data) > 4096:
|
||||
LOG.audit(_("Error in handshake: %s"), data)
|
||||
return
|
||||
|
||||
client = req.environ['eventlet.input'].get_socket()
|
||||
client.sendall("HTTP/1.1 200 OK\r\n\r\n")
|
||||
socketsserver = None
|
||||
sockets['client'] = client
|
||||
sockets['server'] = server
|
||||
|
||||
def proxy_connection(self, req, connect_info):
|
||||
"""Spawn bi-directional vnc proxy."""
|
||||
sockets = {}
|
||||
t0 = eventlet.spawn(self.handshake, req, connect_info, sockets)
|
||||
t0.wait()
|
||||
|
||||
if not sockets.get('client') or not sockets.get('server'):
|
||||
LOG.audit(_("Invalid request: %s"), req)
|
||||
start_response('400 Invalid Request',
|
||||
[('content-type', 'text/html')])
|
||||
return "Invalid Request"
|
||||
|
||||
client = sockets['client']
|
||||
server = sockets['server']
|
||||
|
||||
t1 = eventlet.spawn(self.one_way_proxy, client, server)
|
||||
t2 = eventlet.spawn(self.one_way_proxy, server, client)
|
||||
t1.wait()
|
||||
t2.wait()
|
||||
|
||||
# Make sure our sockets are closed
|
||||
server.close()
|
||||
client.close()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
try:
|
||||
req = webob.Request(environ)
|
||||
LOG.audit(_("Request: %s"), req)
|
||||
token = req.params.get('token')
|
||||
if not token:
|
||||
LOG.audit(_("Request made with missing token: %s"), req)
|
||||
start_response('400 Invalid Request',
|
||||
[('content-type', 'text/html')])
|
||||
return "Invalid Request"
|
||||
|
||||
ctxt = context.get_admin_context()
|
||||
connect_info = rpc.call(ctxt, FLAGS.consoleauth_topic,
|
||||
{'method': 'check_token',
|
||||
'args': {'token': token}})
|
||||
|
||||
if not connect_info:
|
||||
LOG.audit(_("Request made with invalid token: %s"), req)
|
||||
start_response('401 Not Authorized',
|
||||
[('content-type', 'text/html')])
|
||||
return "Not Authorized"
|
||||
|
||||
self.proxy_connection(req, connect_info)
|
||||
except Exception as e:
|
||||
LOG.audit(_("Unexpected error: %s"), e)
|
||||
|
||||
|
||||
class SafeHttpProtocol(eventlet.wsgi.HttpProtocol):
|
||||
"""HttpProtocol wrapper to suppress IOErrors.
|
||||
|
||||
The proxy code above always shuts down client connections, so we catch
|
||||
the IOError that raises when the SocketServer tries to flush the
|
||||
connection.
|
||||
"""
|
||||
def finish(self):
|
||||
try:
|
||||
eventlet.green.BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
|
||||
except IOError:
|
||||
pass
|
||||
eventlet.greenio.shutdown_safe(self.connection)
|
||||
self.connection.close()
|
||||
|
||||
|
||||
def get_wsgi_server():
|
||||
LOG.audit(_("Starting nova-xvpvncproxy node (version %s)"),
|
||||
version.version_string_with_vcs())
|
||||
|
||||
return wsgi.Server("XCP VNC Proxy",
|
||||
XCPVNCProxy(),
|
||||
protocol=SafeHttpProtocol,
|
||||
host=FLAGS.xvpvncproxy_host,
|
||||
port=FLAGS.xvpvncproxy_port)
|
@ -44,7 +44,8 @@ class Server(object):
|
||||
|
||||
default_pool_size = 1000
|
||||
|
||||
def __init__(self, name, app, host=None, port=None, pool_size=None):
|
||||
def __init__(self, name, app, host=None, port=None, pool_size=None,
|
||||
protocol=eventlet.wsgi.HttpProtocol):
|
||||
"""Initialize, but do not start, a WSGI server.
|
||||
|
||||
:param name: Pretty name for logging.
|
||||
@ -62,6 +63,7 @@ class Server(object):
|
||||
self._server = None
|
||||
self._tcp_server = None
|
||||
self._socket = None
|
||||
self._protocol = protocol
|
||||
self._pool = eventlet.GreenPool(pool_size or self.default_pool_size)
|
||||
self._logger = logging.getLogger("eventlet.wsgi.server")
|
||||
self._wsgi_logger = logging.WritableLogger(self._logger)
|
||||
@ -74,6 +76,7 @@ class Server(object):
|
||||
"""
|
||||
eventlet.wsgi.server(self._socket,
|
||||
self.app,
|
||||
protocol=self._protocol,
|
||||
custom_pool=self._pool,
|
||||
log=self._wsgi_logger)
|
||||
|
||||
|
3
setup.py
3
setup.py
@ -91,6 +91,7 @@ setup(name='nova',
|
||||
'bin/nova-api-os-volume',
|
||||
'bin/nova-compute',
|
||||
'bin/nova-console',
|
||||
'bin/nova-consoleauth',
|
||||
'bin/nova-dhcpbridge',
|
||||
'bin/nova-direct-api',
|
||||
'bin/nova-logspool',
|
||||
@ -100,9 +101,9 @@ setup(name='nova',
|
||||
'bin/nova-rootwrap',
|
||||
'bin/nova-scheduler',
|
||||
'bin/nova-spoolsentry',
|
||||
'bin/nova-vncproxy',
|
||||
'bin/nova-volume',
|
||||
'bin/nova-vsa',
|
||||
'bin/nova-xvpvncproxy',
|
||||
'bin/stack',
|
||||
'tools/nova-debug'],
|
||||
py_modules=[])
|
||||
|
Loading…
Reference in New Issue
Block a user