Merge lp:~sleepsonthefloor/nova/vnc_console into lp:~hudson-openstack/nova/trunk
- vnc_console
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Vish Ishaya (community) | Approve | ||
termie (community) | Approve | ||
Thierry Carrez (community) | ffe | Approve | |
Review via email:
|
Commit message
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://
* 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

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 ?

Thierry Carrez (ttx) : | # |

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

Jesse Andrews (anotherjesse) wrote : | # |
Ugh. Typing on tablet. Sorry for gobblygook.
The user can test networking from within the instance

termie (termie) wrote : | # |
All my comments are taking place on https:/
anthony: please reply to my questions and comments on the github page so we can see how that works :)

Thierry Carrez (ttx) wrote : | # |
You should add the new script to setup.py.

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

Jay Pipes (jaypipes) wrote : | # |
++ on Thierry's comments about closed-door development.

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?

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.

termie (termie) wrote : | # |
per reviews on the github pull request page, i think this is ready now

Vish Ishaya (vishvananda) wrote : | # |
my concerns have been addressed. Lgtm

OpenStack Infra (hudson-openstack) wrote : | # |
The attempt to merge lp:~sleepsonthefloor/nova/vnc_console into lp:nova failed. Below is the output from the failed tests.
AccountsTest
test_
test_
test_
test_
AdminAPITest
test_
test_
APITest
test_
Test
test_
test_
test_bad_token OK
test_
test_
test_no_user OK
test_
TestFunctional
test_
test_
TestLimiter
test_
LimiterTest
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
ActionExtensionTest
test_
test_
test_
ExtensionContro
test_
test_index OK
ExtensionManage
test_
ResourceExtensi
test_
test_
test_
ResponseExtensi
test_
test_
TestFaults
test_
test_raise ...
Preview Diff
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']) |
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.