summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xbin/nova-all8
-rwxr-xr-xbin/nova-consoleauth48
-rwxr-xr-xbin/nova-xvpvncproxy (renamed from bin/nova-vncproxy)9
-rw-r--r--doc/source/runnova/vncconsole.rst135
-rw-r--r--nova/api/ec2/cloud.py9
-rw-r--r--nova/api/openstack/compute/contrib/consoles.py79
-rw-r--r--nova/compute/api.py36
-rw-r--r--nova/compute/manager.py23
-rw-r--r--nova/consoleauth/__init__.py26
-rw-r--r--nova/consoleauth/manager.py74
-rw-r--r--nova/exception.py4
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_consoles.py97
-rw-r--r--nova/tests/api/openstack/compute/test_extensions.py1
-rw-r--r--nova/tests/test_compute.py39
-rw-r--r--nova/tests/test_consoleauth.py59
-rw-r--r--nova/tests/test_virt_drivers.py2
-rw-r--r--nova/virt/driver.py4
-rw-r--r--nova/virt/fake.py2
-rw-r--r--nova/virt/libvirt/connection.py3
-rw-r--r--nova/virt/xenapi/vmops.py14
-rw-r--r--nova/virt/xenapi_conn.py9
-rw-r--r--nova/vnc/__init__.py14
-rw-r--r--nova/vnc/auth.py135
-rw-r--r--nova/vnc/proxy.py130
-rw-r--r--nova/vnc/server.py100
-rw-r--r--nova/vnc/xvp_proxy.py181
-rw-r--r--nova/wsgi.py5
-rw-r--r--setup.py3
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)
diff --git a/setup.py b/setup.py
index f7aec9a36..b63bfde39 100644
--- a/setup.py
+++ b/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=[])