Merge lp:~sleepsonthefloor/nova/vnc_console into lp:~hudson-openstack/nova/trunk

Proposed by Anthony Young
Status: Merged
Merged at revision: 914
Proposed branch: lp:~sleepsonthefloor/nova/vnc_console
Merge into: lp:~hudson-openstack/nova/trunk
Diff against target: 707 lines (+563/-2)
14 files modified
bin/nova-vncproxy (+101/-0)
doc/source/runnova/vncconsole.rst (+76/-0)
nova/api/ec2/cloud.py (+7/-0)
nova/api/openstack/servers.py (+11/-1)
nova/compute/api.py (+19/-0)
nova/compute/manager.py (+9/-0)
nova/tests/test_compute.py (+10/-0)
nova/virt/fake.py (+5/-0)
nova/virt/libvirt.xml.template (+3/-0)
nova/virt/libvirt_conn.py (+20/-1)
nova/vnc/__init__.py (+34/-0)
nova/vnc/auth.py (+136/-0)
nova/vnc/proxy.py (+131/-0)
setup.py (+1/-0)
To merge this branch: bzr merge lp:~sleepsonthefloor/nova/vnc_console
Reviewer Review Type Date Requested Status
Vish Ishaya (community) Approve
termie (community) Approve
Thierry Carrez (community) ffe Approve
Review via email: mp+54805@code.launchpad.net

Description of the change

The VNC Proxy is an OpenStack component that allows users of Nova to access
their instances through a websocket enabled browser (like Google Chrome).

A VNC 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
  instance's VNC server
* VNC Proxy initiates connection to VNC server, and continues proxying until
  the session ends

For more info see vncconsole.rst

To post a comment you must log in.
Revision history for this message
Vish Ishaya (vishvananda) wrote :

This is a really awesome feature, and since it doesn't actually remove or change any existing code, I'd propose a FF exception.

One small change:
nova-vnc-console should be added to setup.py so it gets installed and can be picked up by the packages.

image_sems shouldn't be around anymore, I guess it got missed in a merge.

review: Needs Fixing
Revision history for this message
Thierry Carrez (ttx) wrote :

This one is very late to the party, but relatively self-contained...

If you want this into Cactus rather than wait for Diablo, could you provide a quick benefit vs. risk of regression rationale, then request a specific FFe review from me ?

Revision history for this message
Thierry Carrez (ttx) :
review: Needs Information (ffe)
Revision history for this message
Jesse Andrews (anotherjesse) wrote :

This enables users and developers to interact with VMs even if instance networking is broken

This is useful for fixing issues and debugging networking.

When people report in irc and launchpad that they cannot connect to an instance we can ask they to check from when the instace

once vish's request is resolved I think we should merge as he comments it isn't invasive

Revision history for this message
Jesse Andrews (anotherjesse) wrote :

Ugh. Typing on tablet. Sorry for gobblygook.

The user can test networking from within the instance

Revision history for this message
termie (termie) wrote :

All my comments are taking place on https://github.com/termie/nova/pull/1, btw, in case anybody wants to test out the github reveiew stuff, it's still a little barebones but i've put in a feature request with one of their dudes and he claims they are working on it.

anthony: please reply to my questions and comments on the github page so we can see how that works :)

review: Needs Fixing
Revision history for this message
Thierry Carrez (ttx) wrote :

You should add the new script to setup.py.

review: Needs Fixing
Revision history for this message
Thierry Carrez (ttx) wrote :

Yes I agree it's a cool feature, well-documented and is relatively safe release-wise, so I'll grant the exception as long as you can get this merged soon (since I'd like it to see some testing).

That said, I really don't like the "behind closed doors" approach to open source where stuff is being developed without blueprint traces or branch links and proposed very late for merging. We promise openness, and that includes "Open development". But this is not the place to discuss this, I'll raise it on the ML (and probably at the design summit).

review: Approve (ffe)
Revision history for this message
Jay Pipes (jaypipes) wrote :

++ on Thierry's comments about closed-door development.

http://communitymgt.wikia.com/wiki/Water_cooler

Revision history for this message
Monsyne Dragon (mdragon) wrote :

Just a question:

This is pretty nice, and the websocket based vnc proxy is very useful. I will say, tho, that with this, we now have 3 separate console proxy subsystems:

1) the ajax text console system

2) the generic console proxy worker, (which currently only has a driver for XVP, (Xen's VNC proxy), but supports other drivers)

3) this websocket vnc proxy.

All of which are useful, but perhaps we could consolidate these at some point?

Revision history for this message
Anthony Young (sleepsonthefloor) wrote :

Agreed, it would be nice to consolidate some of the console code.

1 and 3 could be refactored to share code, as they have similar internal mechanics. They both are more or less simple http proxies with token auth, and don't require persistent state. I need to study what you've done with the generic console proxy and look for consolidation opportunities there.

It would be great if we could sync up in a few weeks at the design summit and work through some of this.

Revision history for this message
termie (termie) wrote :

per reviews on the github pull request page, i think this is ready now

review: Approve
Revision history for this message
Vish Ishaya (vishvananda) wrote :

my concerns have been addressed. Lgtm

review: Approve
Revision history for this message
OpenStack Infra (hudson-openstack) wrote :
Download full text (44.6 KiB)

The attempt to merge lp:~sleepsonthefloor/nova/vnc_console into lp:nova failed. Below is the output from the failed tests.

AccountsTest
    test_account_create OK
    test_account_delete OK
    test_account_update OK
    test_get_account OK
AdminAPITest
    test_admin_disabled OK
    test_admin_enabled OK
APITest
    test_exceptions_are_converted_to_faults OK
Test
    test_authorize_token OK
    test_authorize_user OK
    test_bad_token OK
    test_bad_user_bad_key OK
    test_bad_user_good_key OK
    test_no_user OK
    test_token_expiry OK
TestFunctional
    test_token_doesnotexist OK
    test_token_expiry OK
TestLimiter
    test_authorize_token OK
LimiterTest
    test_limiter_custom_max_limit OK
    test_limiter_limit_and_offset OK
    test_limiter_limit_medium OK
    test_limiter_limit_over_max OK
    test_limiter_limit_zero OK
    test_limiter_negative_limit OK
    test_limiter_negative_offset OK
    test_limiter_nothing OK
    test_limiter_offset_bad OK
    test_limiter_offset_blank OK
    test_limiter_offset_medium OK
    test_limiter_offset_over_max OK
    test_limiter_offset_zero OK
ActionExtensionTest
    test_extended_action OK
    test_invalid_action OK
    test_invalid_action_body OK
ExtensionControllerTest
    test_get_by_alias OK
    test_index OK
ExtensionManagerTest
    test_get_resources OK
ResourceExtensionTest
    test_get_resources OK
    test_get_resources_with_controller OK
    test_no_extension_present OK
ResponseExtensionTest
    test_get_resources_with_mgr OK
    test_get_resources_with_stub_mgr OK
TestFaults
    test_fault_parts OK
    test_raise ...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'bin/nova-vncproxy'
2--- bin/nova-vncproxy 1970-01-01 00:00:00 +0000
3+++ bin/nova-vncproxy 2011-03-29 22:38:29 +0000
4@@ -0,0 +1,101 @@
5+#!/usr/bin/env python
6+# vim: tabstop=4 shiftwidth=4 softtabstop=4
7+
8+# Copyright (c) 2010 Openstack, LLC.
9+# All Rights Reserved.
10+#
11+# Licensed under the Apache License, Version 2.0 (the "License");
12+# you may not use this file except in compliance with the License.
13+# You may obtain a copy of the License at
14+#
15+# http://www.apache.org/licenses/LICENSE-2.0
16+#
17+# Unless required by applicable law or agreed to in writing, software
18+# distributed under the License is distributed on an "AS IS" BASIS,
19+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20+# See the License for the specific language governing permissions and
21+# limitations under the License.
22+
23+"""VNC Console Proxy Server."""
24+
25+import eventlet
26+import gettext
27+import os
28+import sys
29+
30+possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
31+ os.pardir,
32+ os.pardir))
33+if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
34+ sys.path.insert(0, possible_topdir)
35+
36+gettext.install('nova', unicode=1)
37+
38+from nova import flags
39+from nova import log as logging
40+from nova import service
41+from nova import utils
42+from nova import wsgi
43+from nova import version
44+from nova.vnc import auth
45+from nova.vnc import proxy
46+
47+
48+LOG = logging.getLogger('nova.vnc-proxy')
49+
50+
51+FLAGS = flags.FLAGS
52+flags.DEFINE_string('vncproxy_wwwroot', '/var/lib/nova/noVNC/',
53+ 'Full path to noVNC directory')
54+flags.DEFINE_boolean('vnc_debug', False,
55+ 'Enable debugging features, like token bypassing')
56+flags.DEFINE_integer('vncproxy_port', 6080,
57+ 'Port that the VNC proxy should bind to')
58+flags.DEFINE_string('vncproxy_host', '0.0.0.0',
59+ 'Address that the VNC proxy should bind to')
60+flags.DEFINE_integer('vnc_token_ttl', 300,
61+ 'How many seconds before deleting tokens')
62+flags.DEFINE_string('vncproxy_manager', 'nova.vnc.auth.VNCProxyAuthManager',
63+ 'Manager for vncproxy auth')
64+
65+flags.DEFINE_flag(flags.HelpFlag())
66+flags.DEFINE_flag(flags.HelpshortFlag())
67+flags.DEFINE_flag(flags.HelpXMLFlag())
68+
69+
70+if __name__ == "__main__":
71+ utils.default_flagfile()
72+ FLAGS(sys.argv)
73+ logging.setup()
74+
75+ LOG.audit(_("Starting nova-vnc-proxy node (version %s)"),
76+ version.version_string_with_vcs())
77+
78+ if not (os.path.exists(FLAGS.vncproxy_wwwroot) and
79+ os.path.exists(FLAGS.vncproxy_wwwroot + '/vnc_auto.html')):
80+ LOG.info(_("Missing vncproxy_wwwroot (version %s)"),
81+ FLAGS.vncproxy_wwwroot)
82+ LOG.info(_("You need a slightly modified version of noVNC "
83+ "to work with the nova-vnc-proxy"))
84+ LOG.info(_("Check out the most recent nova noVNC code: %s"),
85+ "git://github.com/sleepsonthefloor/noVNC.git")
86+ LOG.info(_("And drop it in %s"), FLAGS.vncproxy_wwwroot)
87+ exit(1)
88+
89+ app = proxy.WebsocketVNCProxy(FLAGS.vncproxy_wwwroot)
90+
91+ LOG.audit(_("Allowing access to the following files: %s"),
92+ app.get_whitelist())
93+
94+ with_logging = auth.LoggingMiddleware(app)
95+
96+ if FLAGS.vnc_debug:
97+ with_auth = proxy.DebugMiddleware(with_logging)
98+ else:
99+ with_auth = auth.VNCNovaAuthMiddleware(with_logging)
100+
101+ service.serve()
102+
103+ server = wsgi.Server()
104+ server.start(with_auth, FLAGS.vncproxy_port, host=FLAGS.vncproxy_host)
105+ server.wait()
106
107=== added file 'doc/source/runnova/vncconsole.rst'
108--- doc/source/runnova/vncconsole.rst 1970-01-01 00:00:00 +0000
109+++ doc/source/runnova/vncconsole.rst 2011-03-29 22:38:29 +0000
110@@ -0,0 +1,76 @@
111+..
112+ Copyright 2010-2011 United States Government as represented by the
113+ Administrator of the National Aeronautics and Space Administration.
114+ All Rights Reserved.
115+
116+ Licensed under the Apache License, Version 2.0 (the "License"); you may
117+ not use this file except in compliance with the License. You may obtain
118+ a copy of the License at
119+
120+ http://www.apache.org/licenses/LICENSE-2.0
121+
122+ Unless required by applicable law or agreed to in writing, software
123+ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
124+ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
125+ License for the specific language governing permissions and limitations
126+ under the License.
127+
128+Getting Started with the VNC Proxy
129+==================================
130+
131+The VNC Proxy is an OpenStack component that allows users of Nova to access
132+their instances through a websocket enabled browser (like Google Chrome).
133+
134+A VNC Connection works like so:
135+
136+* User connects over an api and gets a url like http://ip:port/?token=xyz
137+* User pastes url in browser
138+* Browser connects to VNC Proxy though a websocket enabled client like noVNC
139+* VNC Proxy authorizes users token, maps the token to a host and port of an
140+ instance's VNC server
141+* VNC Proxy initiates connection to VNC server, and continues proxying until
142+ the session ends
143+
144+
145+Configuring the VNC Proxy
146+-------------------------
147+nova-vnc-proxy requires a websocket enabled html client to work properly. At
148+this time, the only tested client is a slightly modified fork of noVNC, which
149+you can at find http://github.com/openstack/noVNC.git
150+
151+.. todo:: add instruction for installing from package
152+
153+noVNC must be in the location specified by --vncproxy_wwwroot, which defaults
154+to /var/lib/nova/noVNC. nova-vnc-proxy will fail to launch until this code
155+is properly installed.
156+
157+By default, nova-vnc-proxy binds 0.0.0.0:6080. This can be configured with:
158+
159+* --vncproxy_port=[port]
160+* --vncproxy_host=[host]
161+
162+
163+Enabling VNC Consoles in Nova
164+-----------------------------
165+At the moment, VNC support is supported only when using libvirt. To enable VNC
166+Console, configure the following flags:
167+
168+* --vnc_console_proxy_url=http://[proxy_host]:[proxy_port] - proxy_port
169+ defaults to 6080. This url must point to nova-vnc-proxy
170+* --vnc_enabled=[True|False] - defaults to True. If this flag is not set your
171+ instances will launch without vnc support.
172+
173+
174+Getting an instance's VNC Console
175+---------------------------------
176+You can access an instance's VNC Console url in the following methods:
177+
178+* Using the direct api:
179+ eg: 'stack --user=admin --project=admin compute get_vnc_console instance_id=1'
180+* Support for Dashboard, and the Openstack API will be forthcoming
181+
182+
183+Accessing VNC Consoles without a web browser
184+--------------------------------------------
185+At the moment, VNC Consoles are only supported through the web browser, but
186+more general VNC support is in the works.
187
188=== modified file 'nova/api/ec2/cloud.py'
189--- nova/api/ec2/cloud.py 2011-03-28 18:56:19 +0000
190+++ nova/api/ec2/cloud.py 2011-03-29 22:38:29 +0000
191@@ -536,6 +536,13 @@
192 return self.compute_api.get_ajax_console(context,
193 instance_id=instance_id)
194
195+ def get_vnc_console(self, context, instance_id, **kwargs):
196+ """Returns vnc browser url. Used by OS dashboard."""
197+ ec2_id = instance_id
198+ instance_id = ec2utils.ec2_id_to_id(ec2_id)
199+ return self.compute_api.get_vnc_console(context,
200+ instance_id=instance_id)
201+
202 def describe_volumes(self, context, volume_id=None, **kwargs):
203 if volume_id:
204 volumes = []
205
206=== modified file 'nova/api/openstack/servers.py'
207--- nova/api/openstack/servers.py 2011-03-28 21:23:14 +0000
208+++ nova/api/openstack/servers.py 2011-03-29 22:38:29 +0000
209@@ -477,7 +477,7 @@
210
211 @scheduler_api.redirect_handler
212 def get_ajax_console(self, req, id):
213- """ Returns a url to an instance's ajaxterm console. """
214+ """Returns a url to an instance's ajaxterm console."""
215 try:
216 self.compute_api.get_ajax_console(req.environ['nova.context'],
217 int(id))
218@@ -486,6 +486,16 @@
219 return exc.HTTPAccepted()
220
221 @scheduler_api.redirect_handler
222+ def get_vnc_console(self, req, id):
223+ """Returns a url to an instance's ajaxterm console."""
224+ try:
225+ self.compute_api.get_vnc_console(req.environ['nova.context'],
226+ int(id))
227+ except exception.NotFound:
228+ return faults.Fault(exc.HTTPNotFound())
229+ return exc.HTTPAccepted()
230+
231+ @scheduler_api.redirect_handler
232 def diagnostics(self, req, id):
233 """Permit Admins to retrieve server diagnostics."""
234 ctxt = req.environ["nova.context"]
235
236=== modified file 'nova/compute/api.py'
237--- nova/compute/api.py 2011-03-24 22:47:36 +0000
238+++ nova/compute/api.py 2011-03-29 22:38:29 +0000
239@@ -608,6 +608,25 @@
240 return {'url': '%s/?token=%s' % (FLAGS.ajax_console_proxy_url,
241 output['token'])}
242
243+ def get_vnc_console(self, context, instance_id):
244+ """Get a url to a VNC Console."""
245+ instance = self.get(context, instance_id)
246+ output = self._call_compute_message('get_vnc_console',
247+ context,
248+ instance_id)
249+ rpc.call(context, '%s' % FLAGS.vncproxy_topic,
250+ {'method': 'authorize_vnc_console',
251+ 'args': {'token': output['token'],
252+ 'host': output['host'],
253+ 'port': output['port']}})
254+
255+ # hostignore and portignore are compatability params for noVNC
256+ return {'url': '%s/vnc_auto.html?token=%s&host=%s&port=%s' % (
257+ FLAGS.vncproxy_url,
258+ output['token'],
259+ 'hostignore',
260+ 'portignore')}
261+
262 def get_console_output(self, context, instance_id):
263 """Get console output for an an instance"""
264 return self._call_compute_message('get_console_output',
265
266=== modified file 'nova/compute/manager.py'
267--- nova/compute/manager.py 2011-03-28 17:15:59 +0000
268+++ nova/compute/manager.py 2011-03-29 22:38:29 +0000
269@@ -723,6 +723,15 @@
270
271 return self.driver.get_ajax_console(instance_ref)
272
273+ @exception.wrap_exception
274+ def get_vnc_console(self, context, instance_id):
275+ """Return connection information for an vnc console."""
276+ context = context.elevated()
277+ LOG.debug(_("instance %s: getting vnc console"), instance_id)
278+ instance_ref = self.db.instance_get(context, instance_id)
279+
280+ return self.driver.get_vnc_console(instance_ref)
281+
282 @checks_instance_lock
283 def attach_volume(self, context, instance_id, volume_id, mountpoint):
284 """Attach a volume to an instance."""
285
286=== modified file 'nova/tests/test_compute.py'
287--- nova/tests/test_compute.py 2011-03-24 10:01:22 +0000
288+++ nova/tests/test_compute.py 2011-03-29 22:38:29 +0000
289@@ -286,6 +286,16 @@
290
291 console = self.compute.get_ajax_console(self.context,
292 instance_id)
293+ self.assert_(set(['token', 'host', 'port']).issubset(console.keys()))
294+ self.compute.terminate_instance(self.context, instance_id)
295+
296+ def test_vnc_console(self):
297+ """Make sure we can a vnc console for an instance."""
298+ instance_id = self._create_instance()
299+ self.compute.run_instance(self.context, instance_id)
300+
301+ console = self.compute.get_vnc_console(self.context,
302+ instance_id)
303 self.assert_(console)
304 self.compute.terminate_instance(self.context, instance_id)
305
306
307=== modified file 'nova/virt/fake.py'
308--- nova/virt/fake.py 2011-03-24 17:53:54 +0000
309+++ nova/virt/fake.py 2011-03-29 22:38:29 +0000
310@@ -375,6 +375,11 @@
311 'host': 'fakeajaxconsole.com',
312 'port': 6969}
313
314+ def get_vnc_console(self, instance):
315+ return {'token': 'FAKETOKEN',
316+ 'host': 'fakevncconsole.com',
317+ 'port': 6969}
318+
319 def get_console_pool_info(self, console_type):
320 return {'address': '127.0.0.1',
321 'username': 'fakeuser',
322
323=== modified file 'nova/virt/libvirt.xml.template'
324--- nova/virt/libvirt.xml.template 2011-03-28 19:57:18 +0000
325+++ nova/virt/libvirt.xml.template 2011-03-29 22:38:29 +0000
326@@ -115,5 +115,8 @@
327 <target port='0'/>
328 </serial>
329
330+#if $getVar('vncserver_host', False)
331+ <graphics type='vnc' port='-1' autoport='yes' keymap='en-us' listen='${vncserver_host}'/>
332+#end if
333 </devices>
334 </domain>
335
336=== modified file 'nova/virt/libvirt_conn.py'
337--- nova/virt/libvirt_conn.py 2011-03-29 20:14:29 +0000
338+++ nova/virt/libvirt_conn.py 2011-03-29 22:38:29 +0000
339@@ -60,6 +60,7 @@
340 from nova import log as logging
341 #from nova import test
342 from nova import utils
343+from nova import vnc
344 from nova.auth import manager
345 from nova.compute import instance_types
346 from nova.compute import power_state
347@@ -675,7 +676,23 @@
348 subprocess.Popen(cmd, shell=True)
349 return {'token': token, 'host': host, 'port': port}
350
351- _image_sems = {}
352+ @exception.wrap_exception
353+ def get_vnc_console(self, instance):
354+ def get_vnc_port_for_instance(instance_name):
355+ virt_dom = self._conn.lookupByName(instance_name)
356+ xml = virt_dom.XMLDesc(0)
357+ # TODO: use etree instead of minidom
358+ dom = minidom.parseString(xml)
359+
360+ for graphic in dom.getElementsByTagName('graphics'):
361+ if graphic.getAttribute('type') == 'vnc':
362+ return graphic.getAttribute('port')
363+
364+ port = get_vnc_port_for_instance(instance['name'])
365+ token = str(uuid.uuid4())
366+ host = instance['host']
367+
368+ return {'token': token, 'host': host, 'port': port}
369
370 @staticmethod
371 def _cache_image(fn, target, fname, cow=False, *args, **kwargs):
372@@ -949,6 +966,8 @@
373 'driver_type': driver_type,
374 'nics': nics}
375
376+ if FLAGS.vnc_enabled:
377+ xml_info['vncserver_host'] = FLAGS.vncserver_host
378 if not rescue:
379 if instance['kernel_id']:
380 xml_info['kernel'] = xml_info['basepath'] + "/kernel"
381
382=== added directory 'nova/vnc'
383=== added file 'nova/vnc/__init__.py'
384--- nova/vnc/__init__.py 1970-01-01 00:00:00 +0000
385+++ nova/vnc/__init__.py 2011-03-29 22:38:29 +0000
386@@ -0,0 +1,34 @@
387+#!/usr/bin/env python
388+# vim: tabstop=4 shiftwidth=4 softtabstop=4
389+
390+# Copyright (c) 2010 Openstack, LLC.
391+# All Rights Reserved.
392+#
393+# Licensed under the Apache License, Version 2.0 (the "License");
394+# you may not use this file except in compliance with the License.
395+# You may obtain a copy of the License at
396+#
397+# http://www.apache.org/licenses/LICENSE-2.0
398+#
399+# Unless required by applicable law or agreed to in writing, software
400+# distributed under the License is distributed on an "AS IS" BASIS,
401+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
402+# See the License for the specific language governing permissions and
403+# limitations under the License.
404+
405+"""Module for VNC Proxying."""
406+
407+from nova import flags
408+
409+
410+FLAGS = flags.FLAGS
411+flags.DEFINE_string('vncproxy_topic', 'vncproxy',
412+ 'the topic vnc proxy nodes listen on')
413+flags.DEFINE_string('vncproxy_url',
414+ 'http://127.0.0.1:6080',
415+ 'location of vnc console proxy, \
416+ in the form "http://127.0.0.1:6080"')
417+flags.DEFINE_string('vncserver_host', '0.0.0.0',
418+ 'the host interface on which vnc server should listen')
419+flags.DEFINE_bool('vnc_enabled', True,
420+ 'enable vnc related features')
421
422=== added file 'nova/vnc/auth.py'
423--- nova/vnc/auth.py 1970-01-01 00:00:00 +0000
424+++ nova/vnc/auth.py 2011-03-29 22:38:29 +0000
425@@ -0,0 +1,136 @@
426+#!/usr/bin/env python
427+# vim: tabstop=4 shiftwidth=4 softtabstop=4
428+
429+# Copyright (c) 2010 Openstack, LLC.
430+# All Rights Reserved.
431+#
432+# Licensed under the Apache License, Version 2.0 (the "License");
433+# you may not use this file except in compliance with the License.
434+# You may obtain a copy of the License at
435+#
436+# http://www.apache.org/licenses/LICENSE-2.0
437+#
438+# Unless required by applicable law or agreed to in writing, software
439+# distributed under the License is distributed on an "AS IS" BASIS,
440+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
441+# See the License for the specific language governing permissions and
442+# limitations under the License.
443+
444+"""Auth Components for VNC Console."""
445+
446+import time
447+import urlparse
448+import webob
449+
450+from webob import Request
451+
452+from nova import context
453+from nova import flags
454+from nova import log as logging
455+from nova import manager
456+from nova import rpc
457+from nova import utils
458+from nova import wsgi
459+from nova import vnc
460+
461+
462+LOG = logging.getLogger('nova.vnc-proxy')
463+FLAGS = flags.FLAGS
464+
465+
466+class VNCNovaAuthMiddleware(object):
467+ """Implementation of Middleware to Handle Nova Auth."""
468+
469+ def __init__(self, app):
470+ self.app = app
471+ self.token_cache = {}
472+ utils.LoopingCall(self.delete_expired_cache_items).start(1)
473+
474+ @webob.dec.wsgify
475+ def __call__(self, req):
476+ token = req.params.get('token')
477+
478+ if not token:
479+ referrer = req.environ.get('HTTP_REFERER')
480+ auth_params = urlparse.parse_qs(urlparse.urlparse(referrer).query)
481+ if 'token' in auth_params:
482+ token = auth_params['token'][0]
483+
484+ connection_info = self.get_token_info(token)
485+ if not connection_info:
486+ LOG.audit(_("Unauthorized Access: (%s)"), req.environ)
487+ return webob.exc.HTTPForbidden(detail='Unauthorized')
488+
489+ if req.path == vnc.proxy.WS_ENDPOINT:
490+ req.environ['vnc_host'] = connection_info['host']
491+ req.environ['vnc_port'] = int(connection_info['port'])
492+
493+ return req.get_response(self.app)
494+
495+ def get_token_info(self, token):
496+ if token in self.token_cache:
497+ return self.token_cache[token]
498+
499+ rval = rpc.call(context.get_admin_context(),
500+ FLAGS.vncproxy_topic,
501+ {"method": "check_token", "args": {'token': token}})
502+ if rval:
503+ self.token_cache[token] = rval
504+ return rval
505+
506+ def delete_expired_cache_items(self):
507+ now = time.time()
508+ to_delete = []
509+ for k, v in self.token_cache.items():
510+ if now - v['last_activity_at'] > FLAGS.vnc_token_ttl:
511+ to_delete.append(k)
512+
513+ for k in to_delete:
514+ del self.token_cache[k]
515+
516+
517+class LoggingMiddleware(object):
518+ """Middleware for basic vnc-specific request logging."""
519+
520+ def __init__(self, app):
521+ self.app = app
522+
523+ @webob.dec.wsgify
524+ def __call__(self, req):
525+ if req.path == vnc.proxy.WS_ENDPOINT:
526+ LOG.info(_("Received Websocket Request: %s"), req.url)
527+ else:
528+ LOG.info(_("Received Request: %s"), req.url)
529+
530+ return req.get_response(self.app)
531+
532+
533+class VNCProxyAuthManager(manager.Manager):
534+ """Manages token based authentication."""
535+
536+ def __init__(self, scheduler_driver=None, *args, **kwargs):
537+ super(VNCProxyAuthManager, self).__init__(*args, **kwargs)
538+ self.tokens = {}
539+ utils.LoopingCall(self._delete_expired_tokens).start(1)
540+
541+ def authorize_vnc_console(self, context, token, host, port):
542+ self.tokens[token] = {'host': host,
543+ 'port': port,
544+ 'last_activity_at': time.time()}
545+ LOG.audit(_("Received Token: %s, %s)"), token, self.tokens[token])
546+
547+ def check_token(self, context, token):
548+ LOG.audit(_("Checking Token: %s, %s)"), token, (token in self.tokens))
549+ if token in self.tokens:
550+ return self.tokens[token]
551+
552+ def _delete_expired_tokens(self):
553+ now = time.time()
554+ to_delete = []
555+ for k, v in self.tokens.items():
556+ if now - v['last_activity_at'] > FLAGS.vnc_token_ttl:
557+ to_delete.append(k)
558+
559+ for k in to_delete:
560+ LOG.audit(_("Deleting Expired Token: %s)"), k)
561+ del self.tokens[k]
562
563=== added file 'nova/vnc/proxy.py'
564--- nova/vnc/proxy.py 1970-01-01 00:00:00 +0000
565+++ nova/vnc/proxy.py 2011-03-29 22:38:29 +0000
566@@ -0,0 +1,131 @@
567+#!/usr/bin/env python
568+# vim: tabstop=4 shiftwidth=4 softtabstop=4
569+
570+# Copyright (c) 2010 Openstack, LLC.
571+# All Rights Reserved.
572+#
573+# Licensed under the Apache License, Version 2.0 (the "License");
574+# you may not use this file except in compliance with the License.
575+# You may obtain a copy of the License at
576+#
577+# http://www.apache.org/licenses/LICENSE-2.0
578+#
579+# Unless required by applicable law or agreed to in writing, software
580+# distributed under the License is distributed on an "AS IS" BASIS,
581+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
582+# See the License for the specific language governing permissions and
583+# limitations under the License.
584+
585+"""Eventlet WSGI Services to proxy VNC. No nova deps."""
586+
587+import base64
588+import os
589+
590+import eventlet
591+from eventlet import wsgi
592+from eventlet import websocket
593+
594+import webob
595+
596+
597+WS_ENDPOINT = '/data'
598+
599+
600+class WebsocketVNCProxy(object):
601+ """Class to proxy from websocket to vnc server."""
602+
603+ def __init__(self, wwwroot):
604+ self.wwwroot = wwwroot
605+ self.whitelist = {}
606+ for root, dirs, files in os.walk(wwwroot):
607+ hidden_dirs = []
608+ for d in dirs:
609+ if d.startswith('.'):
610+ hidden_dirs.append(d)
611+ for d in hidden_dirs:
612+ dirs.remove(d)
613+ for name in files:
614+ if not str(name).startswith('.'):
615+ filename = os.path.join(root, name)
616+ self.whitelist[filename] = True
617+
618+ def get_whitelist(self):
619+ return self.whitelist.keys()
620+
621+ def sock2ws(self, source, dest):
622+ try:
623+ while True:
624+ d = source.recv(32384)
625+ if d == '':
626+ break
627+ d = base64.b64encode(d)
628+ dest.send(d)
629+ except:
630+ source.close()
631+ dest.close()
632+
633+ def ws2sock(self, source, dest):
634+ try:
635+ while True:
636+ d = source.wait()
637+ if d is None:
638+ break
639+ d = base64.b64decode(d)
640+ dest.sendall(d)
641+ except:
642+ source.close()
643+ dest.close()
644+
645+ def proxy_connection(self, environ, start_response):
646+ @websocket.WebSocketWSGI
647+ def _handle(client):
648+ server = eventlet.connect((client.environ['vnc_host'],
649+ client.environ['vnc_port']))
650+ t1 = eventlet.spawn(self.ws2sock, client, server)
651+ t2 = eventlet.spawn(self.sock2ws, server, client)
652+ t1.wait()
653+ t2.wait()
654+ _handle(environ, start_response)
655+
656+ def __call__(self, environ, start_response):
657+ req = webob.Request(environ)
658+ if req.path == WS_ENDPOINT:
659+ return self.proxy_connection(environ, start_response)
660+ else:
661+ if req.path == '/':
662+ fname = '/vnc_auto.html'
663+ else:
664+ fname = req.path
665+
666+ fname = (self.wwwroot + fname).replace('//', '/')
667+ if not fname in self.whitelist:
668+ start_response('404 Not Found',
669+ [('content-type', 'text/html')])
670+ return "Not Found"
671+
672+ base, ext = os.path.splitext(fname)
673+ if ext == '.js':
674+ mimetype = 'application/javascript'
675+ elif ext == '.css':
676+ mimetype = 'text/css'
677+ elif ext in ['.svg', '.jpg', '.png', '.gif']:
678+ mimetype = 'image'
679+ else:
680+ mimetype = 'text/html'
681+
682+ start_response('200 OK', [('content-type', mimetype)])
683+ return open(os.path.join(fname)).read()
684+
685+
686+class DebugMiddleware(object):
687+ """Debug middleware. Skip auth, get vnc connect info from query string."""
688+
689+ def __init__(self, app):
690+ self.app = app
691+
692+ @webob.dec.wsgify
693+ def __call__(self, req):
694+ if req.path == WS_ENDPOINT:
695+ req.environ['vnc_host'] = req.params.get('host')
696+ req.environ['vnc_port'] = int(req.params.get('port'))
697+ return req.get_response(self.app)
698
699=== modified file 'setup.py'
700--- setup.py 2011-02-22 16:37:12 +0000
701+++ setup.py 2011-03-29 22:38:29 +0000
702@@ -112,4 +112,5 @@
703 'bin/nova-spoolsentry',
704 'bin/stack',
705 'bin/nova-volume',
706+ 'bin/nova-vncproxy',
707 'tools/nova-debug'])