Implements blueprint vnc-console-cleanup

* Creates a unified way to access vnc consoles for xenserver and libvirt
 * Now supports both java and websocket clients
 * Removes nova-vncproxy - a replacement version of this (nova-novncproxy) can be found as described in vncconsole.rst
 * Adds nova-xvpvncproxy, which supports a java vnc client
 * Adds api extension to access java and novnc access_urls
 * Fixes proxy server to close/shutdown sockets more cleanly
 * Address style feedback
 * Use new-style extension format
 * Fix setup.py
 * utils.gen_uuid must be wrapped like str(utils.gen_uuid()) or it can't be serialized

Change-Id: I5e42e2f160e8e3476269bd64b0e8aa77e66c918c
This commit is contained in:
Anthony Young 2011-12-22 21:39:21 +00:00
parent 5987ed97ff
commit 8d010cacb5
28 changed files with 792 additions and 475 deletions

View File

@ -44,7 +44,6 @@ from nova import flags
from nova import log as logging
from nova import service
from nova import utils
from nova.vnc import server
from nova.objectstore import s3server
@ -60,17 +59,12 @@ if __name__ == '__main__':
servers.append(service.WSGIService(api))
except (Exception, SystemExit):
logging.exception(_('Failed to load %s') % '%s-api' % api)
# nova-vncproxy
try:
servers.append(server.get_wsgi_server())
except (Exception, SystemExit):
logging.exception(_('Failed to load %s') % 'vncproxy-wsgi')
# nova-objectstore
try:
servers.append(s3server.get_wsgi_server())
except (Exception, SystemExit):
logging.exception(_('Failed to load %s') % 'objectstore-wsgi')
for binary in ['nova-vncproxy', 'nova-compute', 'nova-volume',
for binary in ['nova-xvpvncproxy', 'nova-compute', 'nova-volume',
'nova-network', 'nova-scheduler', 'nova-vsa']:
try:
servers.append(service.Service.create(binary=binary))

48
bin/nova-consoleauth Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 Openstack, LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""VNC Console Proxy Server."""
import eventlet
eventlet.monkey_patch()
import os
import sys
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir,
os.pardir))
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
sys.path.insert(0, possible_topdir)
from nova import flags
from nova import log as logging
from nova import service
from nova import utils
from nova.consoleauth import manager
if __name__ == "__main__":
utils.default_flagfile()
flags.FLAGS(sys.argv)
logging.setup()
server = service.Service.create(binary='nova-consoleauth')
service.serve(server)
service.wait()

View File

@ -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()

View File

@ -15,68 +15,117 @@
License for the specific language governing permissions and limitations
under the License.
Getting Started with the VNC Proxy
==================================
Overview
========
The VNC Proxy is an OpenStack component that allows users of Nova to access
their instances through a websocket enabled browser (like Google Chrome).
their instances through vnc clients. In essex and beyond, there is support
for for both libvirt and XenServer using both java and websocket cleints.
A VNC Connection works like so:
In general, a VNC console Connection works like so:
* User connects over an api and gets a url like http://ip:port/?token=xyz
* User pastes url in browser
* Browser connects to VNC Proxy though a websocket enabled client like noVNC
* VNC Proxy authorizes users token, maps the token to a host and port of an
* User connects to api and gets an access_url like http://ip:port/?token=xyz
* User pastes url in browser or as client parameter
* Browser/Client connects to proxy
* Proxy authorizes users token, maps the token to a host and port of an
instance's VNC server
* VNC Proxy initiates connection to VNC server, and continues proxying until
* Proxy initiates connection to VNC server, and continues proxying until
the session ends
Configuring the VNC Proxy
-------------------------
nova-vncproxy requires a websocket enabled html client to work properly. At
this time, the only tested client is a slightly modified fork of noVNC, which
you can at find http://github.com/openstack/noVNC.git
.. todo:: add instruction for installing from package
noVNC must be in the location specified by --vncproxy_wwwroot, which defaults
to /var/lib/nova/noVNC. nova-vncproxy will fail to launch until this code
is properly installed.
By default, nova-vncproxy binds 0.0.0.0:6080. This can be configured with:
* :option:`--vncproxy_port=[port]`
* :option:`--vncproxy_host=[host]`
It also binds a separate Flash socket policy listener on 0.0.0.0:843. This
can be configured with:
* :option:`--vncproxy_flash_socket_policy_port=[port]`
* :option:`--vncproxy_flash_socket_policy_host=[host]`
Note that in general, the vnc proxy performs multiple functions:
* Bridges between public network (where clients live) and private network
(where vncservers live)
* Mediates token authentication
* Transparently deals with hypervisor-specific connection details to provide
a uniform client experience.
Enabling VNC Consoles in Nova
-----------------------------
At the moment, VNC support is supported only when using libvirt. To enable VNC
Console, configure the following flags:
About nova-consoleauth
----------------------
Both client proxies leverage a shared service to manage token auth called
nova-consoleauth. This service must be running in order for for either proxy
to work. Many proxies of either type can be run against a single
nova-consoleauth service in a cluster configuration.
* :option:`--vnc_console_proxy_url=http://[proxy_host]:[proxy_port]` -
proxy_port defaults to 6080. This url must point to nova-vncproxy
Getting an Access Url
---------------------
Nova provides the ability to create access_urls through the os-consoles extension.
Support for accessing this url is provided by novaclient:
# FIXME (sleepsonthefloor) update this branch name once client code merges
git clone https://github.com/cloudbuilders/python-novaclient
git checkout vnc_redux
. openrc # or whatever you use to load standard nova creds
nova get-vnc-console [server_id] [xvpvnc|novnc]
Accessing VNC Consoles with a Java client
-----------------------------------------
To enable support for the OpenStack java vnc client in nova, nova provides the
nova-xvpvncproxy service, which you should run to enable this feature.
* :option:`--xvpvncproxy_baseurl=[base url for client connections]` -
this is the public base url to which clients will connect. "?token=abc"
will be added to this url for the purposes of auth.
* :option:`--xvpvncproxy_port=[port]` - port to bind (defaults to 6081)
* :option:`--xvpvncproxy_host=[host]` - host to bind (defaults to 0.0.0.0)
As a client, you will need a special Java client, which is
a version of TightVNC slightly modified to support our token auth::
git clone https://github.com/cloudbuilders/nova-xvpvncviewer
cd nova-xvpvncviewer
make
Then, to create a session, first request an access url using python-novaclient
and then run the client like so::
# Retrieve access url
nova get-vnc-console [server_id] xvpvnc
# Run client
java -jar VncViewer.jar [access_url]
nova-vncproxy replaced with nova-novncproxy
-------------------------------------------
The previous vnc proxy, nova-vncproxy, has been removed from the nova source
tree and replaced with an improved server that can be found externally at
http://github.com/cloudbuilders/noVNC.git (in a branch called vnc_redux while
this patch is in review).
To use this nova-novncproxy:
git clone http://github.com/cloudbuilders/noVNC.git
git checkout vnc_redux
utils/nova-novncproxy --flagfile=[path to flagfile]
The --flagfile param should point to your nova config that includes the rabbit
server address and credentials.
By default, nova-novncproxy binds 0.0.0.0:6080. This can be configured with:
* :option:`--novncproxy_baseurl=[base url for client connections]` -
this is the public base url to which clients will connect. "?token=abc"
will be added to this url for the purposes of auth.
* :option:`--novncproxy_port=[port]`
* :option:`--novncproxy_host=[host]`
Accessing a vnc console through a web browser
---------------------------------------------
Retrieving an access_url for a web browser is similar to the flow for
the java client:
# Retrieve access url
nova get-vnc-console [server_id] novnc
# Then, paste the url into your web browser
Support for a streamlined flow via dashboard will land in essex.
Important Options
-----------------
* :option:`--vnc_enabled=[True|False]` - defaults to True. If this flag is
not set your instances will launch without vnc support.
Getting an instance's VNC Console
---------------------------------
You can access an instance's VNC Console url in the following methods:
* Using the direct api:
eg: '``stack --user=admin --project=admin compute get_vnc_console instance_id=1``'
* Support for Dashboard, and the Openstack API will be forthcoming
Accessing VNC Consoles without a web browser
--------------------------------------------
At the moment, VNC Consoles are only supported through the web browser, but
more general VNC support is in the works.
* :option:`--vncserver_host=[instance vncserver host]` - defaults to 127.0.0.1
This is the address that vncservers will bind, and should be overridden in
production deployments as a private address. Applies to libvirt only.

View File

@ -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 = []

View File

@ -0,0 +1,79 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License
import webob
from nova import compute
from nova import exception
from nova import log as logging
from nova.api.openstack import extensions
from nova.api.openstack import wsgi
LOG = logging.getLogger('nova.api.openstack.compute.contrib.console')
class ConsolesController(wsgi.Controller):
def __init__(self, *args, **kwargs):
self.compute_api = compute.API()
super(ConsolesController, self).__init__(*args, **kwargs)
@wsgi.action('os-getVNCConsole')
def get_vnc_console(self, req, id, body):
"""Get text console output."""
context = req.environ['nova.context']
console_type = body['os-getVNCConsole'].get('type')
if not console_type:
raise webob.exc.HTTPBadRequest(_('Missing type specification'))
try:
instance = self.compute_api.routing_get(context, id)
except exception.NotFound:
raise webob.exc.HTTPNotFound(_('Instance not found'))
try:
output = self.compute_api.get_vnc_console(context,
instance,
console_type)
except exception.ConsoleTypeInvalid, e:
raise webob.exc.HTTPBadRequest(_('Invalid type specification'))
except exception.ApiError, e:
raise webob.exc.HTTPBadRequest(explanation=e.message)
except exception.NotAuthorized, e:
raise webob.exc.HTTPUnauthorized()
return {'console': {'type': console_type, 'url': output['url']}}
def get_actions(self):
"""Return the actions the extension adds, as required by contract."""
actions = [extensions.ActionExtension("servers", "os-getVNCConsole",
self.get_vnc_console)]
return actions
class Consoles(extensions.ExtensionDescriptor):
"""Interactive Console support."""
name = "Consoles"
alias = "os-consoles"
namespace = "http://docs.openstack.org/compute/ext/os-consoles/api/v2"
updated = "2011-12-23T00:00:00+00:00"
def get_controller_extensions(self):
controller = ConsolesController()
extension = extensions.ControllerExtension(self, 'servers', controller)
return [extension]

View File

@ -50,7 +50,7 @@ LOG = logging.getLogger('nova.compute.api')
FLAGS = flags.FLAGS
flags.DECLARE('enable_zone_routing', 'nova.scheduler.api')
flags.DECLARE('vncproxy_topic', 'nova.vnc')
flags.DECLARE('consoleauth_topic', 'nova.consoleauth')
flags.DEFINE_integer('find_host_timeout', 30,
'Timeout after NN seconds when looking for a host.')
@ -1571,23 +1571,23 @@ class API(base.Base):
output['token'])}
@wrap_check_policy
def get_vnc_console(self, context, instance):
"""Get a url to a VNC Console."""
output = self._call_compute_message('get_vnc_console',
context,
instance)
rpc.call(context, '%s' % FLAGS.vncproxy_topic,
{'method': 'authorize_vnc_console',
'args': {'token': output['token'],
'host': output['host'],
'port': output['port']}})
def get_vnc_console(self, context, instance, console_type):
"""Get a url to an instance Console."""
connect_info = self._call_compute_message('get_vnc_console',
context,
instance,
params={"console_type": console_type})
# hostignore and portignore are compatibility params for noVNC
return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % (
FLAGS.vncproxy_url,
output['token'],
'hostignore',
'portignore')}
rpc.call(context, '%s' % FLAGS.consoleauth_topic,
{'method': 'authorize_console',
'args': {'token': connect_info['token'],
'console_type': console_type,
'host': connect_info['host'],
'port': connect_info['port'],
'internal_access_path':\
connect_info['internal_access_path']}})
return {'url': connect_info['access_url']}
@wrap_check_policy
def get_console_output(self, context, instance, tail_length=None):

View File

@ -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

View File

@ -0,0 +1,26 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 Openstack, LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Module to authenticate Consoles."""
from nova import flags
FLAGS = flags.FLAGS
flags.DEFINE_string('consoleauth_topic', 'consoleauth',
'the topic console auth proxy nodes listen on')

View File

@ -0,0 +1,74 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 Openstack, LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Auth Components for Consoles."""
import os
import sys
import time
from nova import flags
from nova import log as logging
from nova import manager
from nova import utils
LOG = logging.getLogger('nova.consoleauth')
FLAGS = flags.FLAGS
flags.DEFINE_integer('console_token_ttl', 600,
'How many seconds before deleting tokens')
flags.DEFINE_string('consoleauth_manager',
'nova.consoleauth.manager.ConsoleAuthManager',
'Manager for console auth')
class ConsoleAuthManager(manager.Manager):
"""Manages token based authentication."""
def __init__(self, scheduler_driver=None, *args, **kwargs):
super(ConsoleAuthManager, self).__init__(*args, **kwargs)
self.tokens = {}
utils.LoopingCall(self._delete_expired_tokens).start(1)
def _delete_expired_tokens(self):
now = time.time()
to_delete = []
for k, v in self.tokens.items():
if now - v['last_activity_at'] > FLAGS.console_token_ttl:
to_delete.append(k)
for k in to_delete:
LOG.audit(_("Deleting Expired Token: (%s)"), k)
del self.tokens[k]
def authorize_console(self, context, token, console_type, host, port,
internal_access_path):
self.tokens[token] = {'token': token,
'console_type': console_type,
'host': host,
'port': port,
'internal_access_path': internal_access_path,
'last_activity_at': time.time()}
token_dict = self.tokens[token]
LOG.audit(_("Received Token: %(token)s, %(token_dict)s)"), locals())
def check_token(self, context, token):
token_valid = token in self.tokens
LOG.audit(_("Checking Token: %(token)s, %(token_valid)s)"), locals())
if token_valid:
return self.tokens[token]

View File

@ -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.")

View File

@ -0,0 +1,97 @@
# Copyright 2012 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import webob
from nova import compute
from nova import exception
from nova import test
from nova.tests.api.openstack import fakes
def fake_get_vnc_console(self, _context, _instance, _console_type):
return {'url': 'http://fake'}
def fake_get_vnc_console_invalid_type(self, _context,
_instance, _console_type):
raise exception.ConsoleTypeInvalid()
def fake_get(self, context, instance_uuid):
return {'uuid': instance_uuid}
def fake_get_not_found(self, context, instance_uuid):
raise exception.NotFound()
class ConsolesExtensionTest(test.TestCase):
def setUp(self):
super(ConsolesExtensionTest, self).setUp()
self.stubs.Set(compute.API, 'get_vnc_console',
fake_get_vnc_console)
self.stubs.Set(compute.API, 'get', fake_get)
def test_get_vnc_console(self):
body = {'os-getVNCConsole': {'type': 'novnc'}}
req = webob.Request.blank('/v2/fake/servers/1/action')
req.method = "POST"
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
output = json.loads(res.body)
self.assertEqual(res.status_int, 200)
self.assertEqual(output,
{u'console': {u'url': u'http://fake', u'type': u'novnc'}})
def test_get_vnc_console_no_type(self):
self.stubs.Set(compute.API, 'get', fake_get)
body = {'os-getVNCConsole': {}}
req = webob.Request.blank('/v2/fake/servers/1/action')
req.method = "POST"
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 400)
def test_get_vnc_console_no_instance(self):
self.stubs.Set(compute.API, 'get', fake_get_not_found)
body = {'os-getVNCConsole': {'type': 'novnc'}}
req = webob.Request.blank('/v2/fake/servers/1/action')
req.method = "POST"
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 404)
def test_get_vnc_console_invalid_type(self):
self.stubs.Set(compute.API, 'get', fake_get)
body = {'os-getVNCConsole': {'type': 'invalid'}}
self.stubs.Set(compute.API, 'get_vnc_console',
fake_get_vnc_console_invalid_type)
req = webob.Request.blank('/v2/fake/servers/1/action')
req.method = "POST"
req.body = json.dumps(body)
req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 400)

View File

@ -157,6 +157,7 @@ class ExtensionControllerTest(ExtensionTestCase):
"AdminActions",
"Cloudpipe",
"Console_output",
"Consoles",
"Createserverext",
"DeferredDelete",
"DiskConfig",

View File

@ -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}

View File

@ -0,0 +1,59 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2012 OpenStack LLC.
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Tests for Consoleauth Code.
"""
import time
from nova import context
from nova import db
from nova import flags
from nova import log as logging
from nova import test
from nova import utils
from nova.consoleauth import manager
FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.tests.consoleauth')
class ConsoleauthTestCase(test.TestCase):
"""Test Case for consoleauth."""
def setUp(self):
super(ConsoleauthTestCase, self).setUp()
self.manager = utils.import_object(FLAGS.consoleauth_manager)
self.context = context.get_admin_context()
self.old_ttl = FLAGS.console_token_ttl
def tearDown(self):
super(ConsoleauthTestCase, self).tearDown()
FLAGS.console_token_ttl = self.old_ttl
def test_tokens_expire(self):
"""Test that tokens expire correctly."""
token = 'mytok'
FLAGS.console_token_ttl = 1
self.manager.authorize_console(self.context, token, 'novnc',
'127.0.0.1', 'host', '')
self.assertTrue(self.manager.check_token(self.context, token))
time.sleep(1.1)
self.assertFalse(self.manager.check_token(self.context, token))

View File

@ -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)

View File

@ -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

View File

@ -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}

View File

@ -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):

View File

@ -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)}

View File

@ -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"""

View File

@ -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')

View File

@ -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]

View File

@ -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)

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2010 Openstack, LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Auth Components for VNC Console."""
import os
import sys
from nova import flags
from nova import log as logging
from nova import version
from nova import wsgi
from nova.vnc import auth
from nova.vnc import proxy
LOG = logging.getLogger('nova.vncproxy')
FLAGS = flags.FLAGS
flags.DEFINE_string('vncproxy_wwwroot', '/var/lib/nova/noVNC/',
'Full path to noVNC directory')
flags.DEFINE_boolean('vnc_debug', False,
'Enable debugging features, like token bypassing')
flags.DEFINE_integer('vncproxy_port', 6080,
'Port that the VNC proxy should bind to')
flags.DEFINE_string('vncproxy_host', '0.0.0.0',
'Address that the VNC proxy should bind to')
flags.DEFINE_integer('vncproxy_flash_socket_policy_port', 843,
'Port that the socket policy listener should bind to')
flags.DEFINE_string('vncproxy_flash_socket_policy_host', '0.0.0.0',
'Address that the socket policy listener should bind to')
flags.DEFINE_integer('vnc_token_ttl', 300,
'How many seconds before deleting tokens')
flags.DEFINE_string('vncproxy_manager', 'nova.vnc.auth.VNCProxyAuthManager',
'Manager for vncproxy auth')
def get_wsgi_server():
LOG.audit(_("Starting nova-vncproxy node (version %s)"),
version.version_string_with_vcs())
if not (os.path.exists(FLAGS.vncproxy_wwwroot) and
os.path.exists(FLAGS.vncproxy_wwwroot + '/vnc_auto.html')):
LOG.info(_("Missing vncproxy_wwwroot (version %s)"),
FLAGS.vncproxy_wwwroot)
LOG.info(_("You need a slightly modified version of noVNC "
"to work with the nova-vnc-proxy"))
LOG.info(_("Check out the most recent nova noVNC code: %s"),
"git://github.com/sleepsonthefloor/noVNC.git")
LOG.info(_("And drop it in %s"), FLAGS.vncproxy_wwwroot)
sys.exit(1)
app = proxy.WebsocketVNCProxy(FLAGS.vncproxy_wwwroot)
LOG.audit(_("Allowing access to the following files: %s"),
app.get_whitelist())
with_logging = auth.LoggingMiddleware(app)
if FLAGS.vnc_debug:
with_auth = proxy.DebugMiddleware(with_logging)
else:
with_auth = auth.VNCNovaAuthMiddleware(with_logging)
wsgi_server = wsgi.Server("VNC Proxy",
with_auth,
host=FLAGS.vncproxy_host,
port=FLAGS.vncproxy_port)
wsgi_server.start_tcp(handle_flash_socket_policy,
host=FLAGS.vncproxy_flash_socket_policy_host,
port=FLAGS.vncproxy_flash_socket_policy_port)
return wsgi_server
def handle_flash_socket_policy(socket):
LOG.info(_("Received connection on flash socket policy port"))
fd = socket.makefile('rw')
expected_command = "<policy-file-request/>"
if expected_command in fd.read(len(expected_command) + 1):
LOG.info(_("Received valid flash socket policy request"))
fd.write('<?xml version="1.0"?><cross-domain-policy><allow-'
'access-from domain="*" to-ports="%d" /></cross-'
'domain-policy>' % (FLAGS.vncproxy_port))
fd.flush()
socket.close()

181
nova/vnc/xvp_proxy.py Normal file
View File

@ -0,0 +1,181 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (c) 2012 Openstack, LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Eventlet WSGI Services to proxy VNC for XCP protocol."""
import base64
import os
import socket
import webob
import eventlet
import eventlet.green
import eventlet.greenio
import eventlet.wsgi
from nova import context
from nova import flags
from nova import log as logging
from nova import rpc
from nova import version
from nova import wsgi
LOG = logging.getLogger('nova.xvpvncproxy')
FLAGS = flags.FLAGS
flags.DECLARE('consoleauth_topic', 'nova.consoleauth')
flags.DEFINE_integer('xvpvncproxy_port', 6081,
'Port that the XCP VNC proxy should bind to')
flags.DEFINE_string('xvpvncproxy_host', '0.0.0.0',
'Address that the XCP VNC proxy should bind to')
class XCPVNCProxy(object):
"""Class to use the xvp auth protocol to proxy instance vnc consoles."""
def one_way_proxy(self, source, dest):
"""Proxy tcp connection from source to dest."""
while True:
try:
d = source.recv(32384)
except Exception as e:
d = None
# If recv fails, send a write shutdown the other direction
if d is None or len(d) == 0:
dest.shutdown(socket.SHUT_WR)
break
# If send fails, terminate proxy in both directions
try:
# sendall raises an exception on write error, unlike send
dest.sendall(d)
except Exception as e:
source.close()
dest.close()
break
def handshake(self, req, connect_info, sockets):
"""Execute hypervisor-specific vnc auth handshaking (if needed)."""
host = connect_info['host']
port = int(connect_info['port'])
server = eventlet.connect((host, port))
# Handshake as necessary
if connect_info.get('internal_access_path'):
server.sendall("CONNECT %s HTTP/1.1\r\n\r\n" %
connect_info['internal_access_path'])
data = ""
while True:
b = server.recv(1)
if b:
data += b
if data.find("\r\n\r\n") != -1:
if not data.split("\r\n")[0].find("200"):
LOG.audit(_("Error in handshake: %s"), data)
return
break
if not b or len(data) > 4096:
LOG.audit(_("Error in handshake: %s"), data)
return
client = req.environ['eventlet.input'].get_socket()
client.sendall("HTTP/1.1 200 OK\r\n\r\n")
socketsserver = None
sockets['client'] = client
sockets['server'] = server
def proxy_connection(self, req, connect_info):
"""Spawn bi-directional vnc proxy."""
sockets = {}
t0 = eventlet.spawn(self.handshake, req, connect_info, sockets)
t0.wait()
if not sockets.get('client') or not sockets.get('server'):
LOG.audit(_("Invalid request: %s"), req)
start_response('400 Invalid Request',
[('content-type', 'text/html')])
return "Invalid Request"
client = sockets['client']
server = sockets['server']
t1 = eventlet.spawn(self.one_way_proxy, client, server)
t2 = eventlet.spawn(self.one_way_proxy, server, client)
t1.wait()
t2.wait()
# Make sure our sockets are closed
server.close()
client.close()
def __call__(self, environ, start_response):
try:
req = webob.Request(environ)
LOG.audit(_("Request: %s"), req)
token = req.params.get('token')
if not token:
LOG.audit(_("Request made with missing token: %s"), req)
start_response('400 Invalid Request',
[('content-type', 'text/html')])
return "Invalid Request"
ctxt = context.get_admin_context()
connect_info = rpc.call(ctxt, FLAGS.consoleauth_topic,
{'method': 'check_token',
'args': {'token': token}})
if not connect_info:
LOG.audit(_("Request made with invalid token: %s"), req)
start_response('401 Not Authorized',
[('content-type', 'text/html')])
return "Not Authorized"
self.proxy_connection(req, connect_info)
except Exception as e:
LOG.audit(_("Unexpected error: %s"), e)
class SafeHttpProtocol(eventlet.wsgi.HttpProtocol):
"""HttpProtocol wrapper to suppress IOErrors.
The proxy code above always shuts down client connections, so we catch
the IOError that raises when the SocketServer tries to flush the
connection.
"""
def finish(self):
try:
eventlet.green.BaseHTTPServer.BaseHTTPRequestHandler.finish(self)
except IOError:
pass
eventlet.greenio.shutdown_safe(self.connection)
self.connection.close()
def get_wsgi_server():
LOG.audit(_("Starting nova-xvpvncproxy node (version %s)"),
version.version_string_with_vcs())
return wsgi.Server("XCP VNC Proxy",
XCPVNCProxy(),
protocol=SafeHttpProtocol,
host=FLAGS.xvpvncproxy_host,
port=FLAGS.xvpvncproxy_port)

View File

@ -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)

View File

@ -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=[])