diff options
28 files changed, 783 insertions, 466 deletions
diff --git a/bin/nova-all b/bin/nova-all index 497195bda..9c9e2bbaa 100755 --- a/bin/nova-all +++ b/bin/nova-all @@ -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)) diff --git a/bin/nova-consoleauth b/bin/nova-consoleauth new file mode 100755 index 000000000..325a399d7 --- /dev/null +++ b/bin/nova-consoleauth @@ -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() diff --git a/bin/nova-vncproxy b/bin/nova-xvpvncproxy index 9b44a95ea..a17d0cbb3 100755 --- a/bin/nova-vncproxy +++ b/bin/nova-xvpvncproxy @@ -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() diff --git a/doc/source/runnova/vncconsole.rst b/doc/source/runnova/vncconsole.rst index 8bef4eb37..cf69b610b 100644 --- a/doc/source/runnova/vncconsole.rst +++ b/doc/source/runnova/vncconsole.rst @@ -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 +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. -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 +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. -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. +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: -By default, nova-vncproxy binds 0.0.0.0:6080. This can be configured with: + # 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] -* :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: +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:`--vncproxy_flash_socket_policy_port=[port]` -* :option:`--vncproxy_flash_socket_policy_host=[host]` +* :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:: -Enabling VNC Consoles in Nova ------------------------------ -At the moment, VNC support is supported only when using libvirt. To enable VNC -Console, configure the following flags: + git clone https://github.com/cloudbuilders/nova-xvpvncviewer + cd nova-xvpvncviewer + make -* :option:`--vnc_console_proxy_url=http://[proxy_host]:[proxy_port]` - - proxy_port defaults to 6080. This url must point to nova-vncproxy -* :option:`--vnc_enabled=[True|False]` - defaults to True. If this flag is - not set your instances will launch without vnc support. +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. -Getting an instance's VNC Console ---------------------------------- -You can access an instance's VNC Console url in the following methods: +By default, nova-novncproxy binds 0.0.0.0:6080. This can be configured with: -* 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 +* :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 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. +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. +* :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. diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index b082d0598..2869b4fe1 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -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 = [] diff --git a/nova/api/openstack/compute/contrib/consoles.py b/nova/api/openstack/compute/contrib/consoles.py new file mode 100644 index 000000000..46e3559ff --- /dev/null +++ b/nova/api/openstack/compute/contrib/consoles.py @@ -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] diff --git a/nova/compute/api.py b/nova/compute/api.py index 23bb4de82..012217584 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -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']}}) - - # 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')} + 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}) + + 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): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index c0ad9e626..999143153 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -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 diff --git a/nova/consoleauth/__init__.py b/nova/consoleauth/__init__.py new file mode 100644 index 000000000..9d578b77a --- /dev/null +++ b/nova/consoleauth/__init__.py @@ -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') diff --git a/nova/consoleauth/manager.py b/nova/consoleauth/manager.py new file mode 100644 index 000000000..8f86b4b8c --- /dev/null +++ b/nova/consoleauth/manager.py @@ -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] diff --git a/nova/exception.py b/nova/exception.py index 23bcf46f4..777d64515 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -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.") diff --git a/nova/tests/api/openstack/compute/contrib/test_consoles.py b/nova/tests/api/openstack/compute/contrib/test_consoles.py new file mode 100644 index 000000000..0ed177a33 --- /dev/null +++ b/nova/tests/api/openstack/compute/contrib/test_consoles.py @@ -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) diff --git a/nova/tests/api/openstack/compute/test_extensions.py b/nova/tests/api/openstack/compute/test_extensions.py index 4e41caa69..02356bc44 100644 --- a/nova/tests/api/openstack/compute/test_extensions.py +++ b/nova/tests/api/openstack/compute/test_extensions.py @@ -157,6 +157,7 @@ class ExtensionControllerTest(ExtensionTestCase): "AdminActions", "Cloudpipe", "Console_output", + "Consoles", "Createserverext", "DeferredDelete", "DiskConfig", diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index 06eaf8ef5..17cf46ceb 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -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} diff --git a/nova/tests/test_consoleauth.py b/nova/tests/test_consoleauth.py new file mode 100644 index 000000000..41aefa7fd --- /dev/null +++ b/nova/tests/test_consoleauth.py @@ -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)) diff --git a/nova/tests/test_virt_drivers.py b/nova/tests/test_virt_drivers.py index 9c17b3b0a..adf8f8eb8 100644 --- a/nova/tests/test_virt_drivers.py +++ b/nova/tests/test_virt_drivers.py @@ -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) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index e03793561..7a9347542 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -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 diff --git a/nova/virt/fake.py b/nova/virt/fake.py index 3311834c2..f8f535eb4 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -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} diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 85c48e495..4e5c86ea4 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -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): diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 99f5ca650..e92581213 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -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)} diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index e672a3cb5..bfce96423 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -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""" diff --git a/nova/vnc/__init__.py b/nova/vnc/__init__.py index 859bfd65f..bfaf0b391 100644 --- a/nova/vnc/__init__.py +++ b/nova/vnc/__init__.py @@ -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') diff --git a/nova/vnc/auth.py b/nova/vnc/auth.py deleted file mode 100644 index b96dc595e..000000000 --- a/nova/vnc/auth.py +++ /dev/null @@ -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] diff --git a/nova/vnc/proxy.py b/nova/vnc/proxy.py deleted file mode 100644 index 376db40c1..000000000 --- a/nova/vnc/proxy.py +++ /dev/null @@ -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) diff --git a/nova/vnc/server.py b/nova/vnc/server.py deleted file mode 100644 index c6eb7020f..000000000 --- a/nova/vnc/server.py +++ /dev/null @@ -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() diff --git a/nova/vnc/xvp_proxy.py b/nova/vnc/xvp_proxy.py new file mode 100644 index 000000000..fa1845726 --- /dev/null +++ b/nova/vnc/xvp_proxy.py @@ -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) diff --git a/nova/wsgi.py b/nova/wsgi.py index b94065b78..e2f17ea78 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -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) @@ -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=[]) |