Merge lp:~mdragon/nova/xs-console into lp:~hudson-openstack/nova/trunk
- xs-console
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Soren Hansen |
Approved revision: | 509 |
Merged at revision: | 543 |
Proposed branch: | lp:~mdragon/nova/xs-console |
Merge into: | lp:~hudson-openstack/nova/trunk |
Diff against target: |
1213 lines (+990/-3) 21 files modified
Authors (+1/-0) bin/nova-console (+44/-0) nova/api/openstack/__init__.py (+6/-0) nova/api/openstack/consoles.py (+96/-0) nova/compute/manager.py (+17/-0) nova/console/__init__.py (+13/-0) nova/console/api.py (+75/-0) nova/console/fake.py (+58/-0) nova/console/manager.py (+127/-0) nova/console/xvp.conf.template (+16/-0) nova/console/xvp.py (+194/-0) nova/db/api.py (+54/-0) nova/db/sqlalchemy/api.py (+108/-0) nova/db/sqlalchemy/models.py (+26/-1) nova/flags.py (+4/-0) nova/tests/test_console.py (+129/-0) nova/tests/test_virt.py (+1/-1) nova/virt/fake.py (+5/-0) nova/virt/hyperv.py (+1/-1) nova/virt/libvirt_conn.py (+8/-0) nova/virt/xenapi_conn.py (+7/-0) |
To merge this branch: | bzr merge lp:~mdragon/nova/xs-console |
Related bugs: | |
Related blueprints: |
XenServer Console
(High)
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Trey Morris (community) | Approve | ||
Matt Dietz (community) | Approve | ||
Vish Ishaya (community) | Needs Fixing | ||
Sandy Walsh (community) | Approve | ||
Cory Wright (community) | Approve | ||
Review via email: mp+45324@code.launchpad.net |
Commit message
Description of the change
Implementation of xs-console blueprint (adds support for console proxies like xvp)
If you spin up the nova-console service, you should be able to see the xvp.conf being edited, and the xvp daemon started/stopped if you exercise the openstack console api (consoles sub-resource on servers)
- 505. By Monsyne Dragon
-
pep8 fix
Sandy Walsh (sandy-walsh) wrote : | # |
This is a just a source code review. I haven't tried to run the branch tests yet:
Overall, awesome work! Very impressive first commit!
nova/console/
574 ... should there be debug code in here? Can't this be stubbed in the tests via stubout?
I think ConsolePool should be ConsoleProxyPool. It wasn't clear to me what a "console" was (since I always just viewed a console as an endpoint). A ConsoleProxy, however, makes more sense that it's a running entity.
I must be missing something, but I don't see where the ConsoleProxyManager is used? Other than via the tests. Yet, this is the only way to get a ConsoleProxy instantiated, afaik. Do you have any examples of the curl or python-cloudserver calls to add/remove a console?
I assume the proxies run on the console application server?
I have not reviewed the breadth of the tests yet. I'm not marking it Approved or Needs Fixing until you've had a chance to give feedback.
Monsyne Dragon (mdragon) wrote : | # |
nova/console/
(I think it got that name because I was wavering between console and consoleproxy as the name for the service)
re: diff line 574: this is code used for unittests to eliminate the rpc call. THis is that same as how compute.manager uses the stub_network flag for tests.
Really, the 'console' is an entry in the config for the running xvp console proxy daemon.
the 'ConsolePool' represents a group of consoles that all live in the save (in this case xenserver) host.
the xvp daemon is the actual proxy (for multiple consoles)
ConsoleProxyManager is the main manager for the nova-console worker.
Ultimately the calls to add/remove consoles are made through the openstack api. (console is a sub-resource of server)
Sandy Walsh (sandy-walsh) wrote : | # |
Change based on conversation with vishy: I assume the proxies run on the api servers?
Monsyne Dragon (mdragon) wrote : | # |
> Change based on conversation with vishy: I assume the proxies run on the api
> servers?
No, the proxy runs on a console proxy server
Sandy Walsh (sandy-walsh) wrote : | # |
Based on discussion with Dragon in IRC (thx again D):
1. The xcp daemon runs on the nova-console server and it assumed to have two NIC's: 1 public and 1 private. Clients obviously come in on the public one.
2. I think the flags for testing can be removed and stubout used. Dragon will have a look, but this works currently.
3. The Service fires up the Manager. API doesn't need to specify which manager.
4. The common pattern in Nova is:
* Managers are Factories of Drivers.
* Drivers are abstract Facades on something.
So calling driver.py proxy.py might add more confusion.
Approved code wise. Haven't had it running yet.
Vish Ishaya (vishvananda) wrote : | # |
This should be a quick change, but the standard way we're doing apis in the other components is to name the class API and then import it in __init__.py. Your way is fine too, I just think that we should be consistent across components.
- 506. By Monsyne Dragon
-
change API classname to match the way other API's are done.
Monsyne Dragon (mdragon) wrote : | # |
> This should be a quick change, but the standard way we're doing apis in the
> other components is to name the class API and then import it in __init__.py.
> Your way is fine too, I just think that we should be consistent across
> components.
Ok, makes sense. I have pushed this change to the branch.
- 507. By Monsyne Dragon
-
re-merged in trunk to correct conflict
Matt Dietz (cerberus) wrote : | # |
I'm not sure I follow the utility of having the driver.ConsoleProxy class. It looks to be an abstract class of sorts, except in every subclassing you re-implement every method. I would understand the utility if it seemed a lot of the functionality could be passed down, but as is it seems to be an unnecessary contract. Am I missing something?
Otherwise seems ok to me.
- 508. By Monsyne Dragon
-
remove uneeded superclass
- 509. By Monsyne Dragon
-
whups, fix accidental change to nova-combined
Monsyne Dragon (mdragon) wrote : | # |
> I'm not sure I follow the utility of having the driver.ConsoleProxy class. It
> looks to be an abstract class of sorts, except in every subclassing you re-
> implement every method. I would understand the utility if it seemed a lot of
> the functionality could be passed down, but as is it seems to be an
> unnecessary contract. Am I missing something?
Nope. I was thinking there would be more common code, but not so. I have removed the driver.ConsoleProxy class. (leave that to future refactorings if need be, once we actually have more than 1 real console driver class.)
Trey Morris (tr3buchet) wrote : | # |
long read! Looks good to me, except for a few places you used '\' for line breaks where you didn't have to from lines 833 to 908. However if it "needs fixing" is up to you.
Preview Diff
1 | === modified file 'Authors' |
2 | --- Authors 2011-01-09 19:13:19 +0000 |
3 | +++ Authors 2011-01-10 21:04:17 +0000 |
4 | @@ -26,6 +26,7 @@ |
5 | Ken Pepple <ken.pepple@gmail.com> |
6 | Matt Dietz <matt.dietz@rackspace.com> |
7 | Michael Gundlach <michael.gundlach@rackspace.com> |
8 | +Monsyne Dragon <mdragon@rackspace.com> |
9 | Monty Taylor <mordred@inaugust.com> |
10 | Paul Voccio <paul@openstack.org> |
11 | Rick Clark <rick@openstack.org> |
12 | |
13 | === added file 'bin/nova-console' |
14 | --- bin/nova-console 1970-01-01 00:00:00 +0000 |
15 | +++ bin/nova-console 2011-01-10 21:04:17 +0000 |
16 | @@ -0,0 +1,44 @@ |
17 | +#!/usr/bin/env python |
18 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
19 | + |
20 | +# Copyright (c) 2010 Openstack, LLC. |
21 | +# All Rights Reserved. |
22 | +# |
23 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
24 | +# not use this file except in compliance with the License. You may obtain |
25 | +# a copy of the License at |
26 | +# |
27 | +# http://www.apache.org/licenses/LICENSE-2.0 |
28 | +# |
29 | +# Unless required by applicable law or agreed to in writing, software |
30 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
31 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
32 | +# License for the specific language governing permissions and limitations |
33 | +# under the License. |
34 | + |
35 | +"""Starter script for Nova Console Proxy.""" |
36 | + |
37 | +import eventlet |
38 | +eventlet.monkey_patch() |
39 | + |
40 | +import gettext |
41 | +import os |
42 | +import sys |
43 | + |
44 | +# If ../nova/__init__.py exists, add ../ to Python search path, so that |
45 | +# it will override what happens to be installed in /usr/(local/)lib/python... |
46 | +possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]), |
47 | + os.pardir, |
48 | + os.pardir)) |
49 | +if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')): |
50 | + sys.path.insert(0, possible_topdir) |
51 | + |
52 | +gettext.install('nova', unicode=1) |
53 | + |
54 | +from nova import service |
55 | +from nova import utils |
56 | + |
57 | +if __name__ == '__main__': |
58 | + utils.default_flagfile() |
59 | + service.serve() |
60 | + service.wait() |
61 | |
62 | === modified file 'nova/api/openstack/__init__.py' |
63 | --- nova/api/openstack/__init__.py 2011-01-07 14:46:17 +0000 |
64 | +++ nova/api/openstack/__init__.py 2011-01-10 21:04:17 +0000 |
65 | @@ -31,6 +31,7 @@ |
66 | from nova import wsgi |
67 | from nova.api.openstack import faults |
68 | from nova.api.openstack import backup_schedules |
69 | +from nova.api.openstack import consoles |
70 | from nova.api.openstack import flavors |
71 | from nova.api.openstack import images |
72 | from nova.api.openstack import servers |
73 | @@ -100,6 +101,11 @@ |
74 | parent_resource=dict(member_name='server', |
75 | collection_name='servers')) |
76 | |
77 | + mapper.resource("console", "consoles", |
78 | + controller=consoles.Controller(), |
79 | + parent_resource=dict(member_name='server', |
80 | + collection_name='servers')) |
81 | + |
82 | mapper.resource("image", "images", controller=images.Controller(), |
83 | collection={'detail': 'GET'}) |
84 | mapper.resource("flavor", "flavors", controller=flavors.Controller(), |
85 | |
86 | === added file 'nova/api/openstack/consoles.py' |
87 | --- nova/api/openstack/consoles.py 1970-01-01 00:00:00 +0000 |
88 | +++ nova/api/openstack/consoles.py 2011-01-10 21:04:17 +0000 |
89 | @@ -0,0 +1,96 @@ |
90 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
91 | + |
92 | +# Copyright 2010 OpenStack LLC. |
93 | +# All Rights Reserved. |
94 | +# |
95 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
96 | +# not use this file except in compliance with the License. You may obtain |
97 | +# a copy of the License at |
98 | +# |
99 | +# http://www.apache.org/licenses/LICENSE-2.0 |
100 | +# |
101 | +# Unless required by applicable law or agreed to in writing, software |
102 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
103 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
104 | +# License for the specific language governing permissions and limitations |
105 | +# under the License. |
106 | + |
107 | +from webob import exc |
108 | + |
109 | +from nova import console |
110 | +from nova import exception |
111 | +from nova import wsgi |
112 | +from nova.api.openstack import faults |
113 | + |
114 | + |
115 | +def _translate_keys(cons): |
116 | + """Coerces a console instance into proper dictionary format """ |
117 | + pool = cons['pool'] |
118 | + info = {'id': cons['id'], |
119 | + 'console_type': pool['console_type']} |
120 | + return dict(console=info) |
121 | + |
122 | + |
123 | +def _translate_detail_keys(cons): |
124 | + """Coerces a console instance into proper dictionary format with |
125 | + correctly mapped attributes """ |
126 | + pool = cons['pool'] |
127 | + info = {'id': cons['id'], |
128 | + 'console_type': pool['console_type'], |
129 | + 'password': cons['password'], |
130 | + 'port': cons['port'], |
131 | + 'host': pool['public_hostname']} |
132 | + return dict(console=info) |
133 | + |
134 | + |
135 | +class Controller(wsgi.Controller): |
136 | + """The Consoles Controller for the Openstack API""" |
137 | + |
138 | + _serialization_metadata = { |
139 | + 'application/xml': { |
140 | + 'attributes': { |
141 | + 'console': []}}} |
142 | + |
143 | + def __init__(self): |
144 | + self.console_api = console.API() |
145 | + super(Controller, self).__init__() |
146 | + |
147 | + def index(self, req, server_id): |
148 | + """Returns a list of consoles for this instance""" |
149 | + consoles = self.console_api.get_consoles( |
150 | + req.environ['nova.context'], |
151 | + int(server_id)) |
152 | + return dict(consoles=[_translate_keys(console) |
153 | + for console in consoles]) |
154 | + |
155 | + def create(self, req, server_id): |
156 | + """Creates a new console""" |
157 | + #info = self._deserialize(req.body, req) |
158 | + self.console_api.create_console( |
159 | + req.environ['nova.context'], |
160 | + int(server_id)) |
161 | + |
162 | + def show(self, req, server_id, id): |
163 | + """Shows in-depth information on a specific console""" |
164 | + try: |
165 | + console = self.console_api.get_console( |
166 | + req.environ['nova.context'], |
167 | + int(server_id), |
168 | + int(id)) |
169 | + except exception.NotFound: |
170 | + return faults.Fault(exc.HTTPNotFound()) |
171 | + return _translate_detail_keys(console) |
172 | + |
173 | + def update(self, req, server_id, id): |
174 | + """You can't update a console""" |
175 | + raise faults.Fault(exc.HTTPNotImplemented()) |
176 | + |
177 | + def delete(self, req, server_id, id): |
178 | + """Deletes a console""" |
179 | + try: |
180 | + self.console_api.delete_console(req.environ['nova.context'], |
181 | + int(server_id), |
182 | + int(id)) |
183 | + except exception.NotFound: |
184 | + return faults.Fault(exc.HTTPNotFound()) |
185 | + return exc.HTTPAccepted() |
186 | |
187 | === modified file 'nova/compute/manager.py' |
188 | --- nova/compute/manager.py 2011-01-08 14:35:50 +0000 |
189 | +++ nova/compute/manager.py 2011-01-10 21:04:17 +0000 |
190 | @@ -35,6 +35,8 @@ |
191 | """ |
192 | |
193 | import datetime |
194 | +import logging |
195 | +import socket |
196 | import functools |
197 | |
198 | from nova import exception |
199 | @@ -52,6 +54,9 @@ |
200 | 'Driver to use for controlling virtualization') |
201 | flags.DEFINE_string('stub_network', False, |
202 | 'Stub network related code') |
203 | +flags.DEFINE_string('console_host', socket.gethostname(), |
204 | + 'Console proxy host to use to connect to instances on' |
205 | + 'this host.') |
206 | |
207 | LOG = logging.getLogger('nova.compute.manager') |
208 | |
209 | @@ -122,6 +127,15 @@ |
210 | state = power_state.NOSTATE |
211 | self.db.instance_set_state(context, instance_id, state) |
212 | |
213 | + def get_console_topic(self, context, **_kwargs): |
214 | + """Retrieves the console host for a project on this host |
215 | + Currently this is just set in the flags for each compute |
216 | + host.""" |
217 | + #TODO(mdragon): perhaps make this variable by console_type? |
218 | + return self.db.queue_get_for(context, |
219 | + FLAGS.console_topic, |
220 | + FLAGS.console_host) |
221 | + |
222 | def get_network_topic(self, context, **_kwargs): |
223 | """Retrieves the network host for a project on this host""" |
224 | # TODO(vish): This method should be memoized. This will make |
225 | @@ -136,6 +150,9 @@ |
226 | FLAGS.network_topic, |
227 | host) |
228 | |
229 | + def get_console_pool_info(self, context, console_type): |
230 | + return self.driver.get_console_pool_info(console_type) |
231 | + |
232 | @exception.wrap_exception |
233 | def refresh_security_group_rules(self, context, |
234 | security_group_id, **_kwargs): |
235 | |
236 | === added directory 'nova/console' |
237 | === added file 'nova/console/__init__.py' |
238 | --- nova/console/__init__.py 1970-01-01 00:00:00 +0000 |
239 | +++ nova/console/__init__.py 2011-01-10 21:04:17 +0000 |
240 | @@ -0,0 +1,13 @@ |
241 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
242 | + |
243 | +""" |
244 | +:mod:`nova.console` -- Console Prxy to set up VM console access (i.e. with xvp) |
245 | +===================================================== |
246 | + |
247 | +.. automodule:: nova.console |
248 | + :platform: Unix |
249 | + :synopsis: Wrapper around console proxies such as xvp to set up |
250 | + multitenant VM console access |
251 | +.. moduleauthor:: Monsyne Dragon <mdragon@rackspace.com> |
252 | +""" |
253 | +from nova.console.api import API |
254 | |
255 | === added file 'nova/console/api.py' |
256 | --- nova/console/api.py 1970-01-01 00:00:00 +0000 |
257 | +++ nova/console/api.py 2011-01-10 21:04:17 +0000 |
258 | @@ -0,0 +1,75 @@ |
259 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
260 | + |
261 | +# Copyright (c) 2010 Openstack, LLC. |
262 | +# All Rights Reserved. |
263 | +# |
264 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
265 | +# not use this file except in compliance with the License. You may obtain |
266 | +# a copy of the License at |
267 | +# |
268 | +# http://www.apache.org/licenses/LICENSE-2.0 |
269 | +# |
270 | +# Unless required by applicable law or agreed to in writing, software |
271 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
272 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
273 | +# License for the specific language governing permissions and limitations |
274 | +# under the License. |
275 | + |
276 | +""" |
277 | +Handles ConsoleProxy API requests |
278 | +""" |
279 | + |
280 | +from nova import exception |
281 | +from nova.db import base |
282 | + |
283 | + |
284 | +from nova import flags |
285 | +from nova import rpc |
286 | + |
287 | + |
288 | +FLAGS = flags.FLAGS |
289 | + |
290 | + |
291 | +class API(base.Base): |
292 | + """API for spining up or down console proxy connections""" |
293 | + |
294 | + def __init__(self, **kwargs): |
295 | + super(API, self).__init__(**kwargs) |
296 | + |
297 | + def get_consoles(self, context, instance_id): |
298 | + return self.db.console_get_all_by_instance(context, instance_id) |
299 | + |
300 | + def get_console(self, context, instance_id, console_id): |
301 | + return self.db.console_get(context, console_id, instance_id) |
302 | + |
303 | + def delete_console(self, context, instance_id, console_id): |
304 | + console = self.db.console_get(context, |
305 | + console_id, |
306 | + instance_id) |
307 | + pool = console['pool'] |
308 | + rpc.cast(context, |
309 | + self.db.queue_get_for(context, |
310 | + FLAGS.console_topic, |
311 | + pool['host']), |
312 | + {"method": "remove_console", |
313 | + "args": {"console_id": console['id']}}) |
314 | + |
315 | + def create_console(self, context, instance_id): |
316 | + instance = self.db.instance_get(context, instance_id) |
317 | + #NOTE(mdragon): If we wanted to return this the console info |
318 | + # here, as we would need to do a call. |
319 | + # They can just do an index later to fetch |
320 | + # console info. I am not sure which is better |
321 | + # here. |
322 | + rpc.cast(context, |
323 | + self._get_console_topic(context, instance['host']), |
324 | + {"method": "add_console", |
325 | + "args": {"instance_id": instance_id}}) |
326 | + |
327 | + def _get_console_topic(self, context, instance_host): |
328 | + topic = self.db.queue_get_for(context, |
329 | + FLAGS.compute_topic, |
330 | + instance_host) |
331 | + return rpc.call(context, |
332 | + topic, |
333 | + {"method": "get_console_topic", "args": {'fake': 1}}) |
334 | |
335 | === added file 'nova/console/fake.py' |
336 | --- nova/console/fake.py 1970-01-01 00:00:00 +0000 |
337 | +++ nova/console/fake.py 2011-01-10 21:04:17 +0000 |
338 | @@ -0,0 +1,58 @@ |
339 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
340 | + |
341 | +# Copyright (c) 2010 Openstack, LLC. |
342 | +# All Rights Reserved. |
343 | +# |
344 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
345 | +# not use this file except in compliance with the License. You may obtain |
346 | +# a copy of the License at |
347 | +# |
348 | +# http://www.apache.org/licenses/LICENSE-2.0 |
349 | +# |
350 | +# Unless required by applicable law or agreed to in writing, software |
351 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
352 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
353 | +# License for the specific language governing permissions and limitations |
354 | +# under the License. |
355 | + |
356 | +""" |
357 | +Fake ConsoleProxy driver for tests. |
358 | +""" |
359 | + |
360 | +from nova import exception |
361 | + |
362 | + |
363 | +class FakeConsoleProxy(object): |
364 | + """Fake ConsoleProxy driver.""" |
365 | + |
366 | + @property |
367 | + def console_type(self): |
368 | + return "fake" |
369 | + |
370 | + def setup_console(self, context, console): |
371 | + """Sets up actual proxies""" |
372 | + pass |
373 | + |
374 | + def teardown_console(self, context, console): |
375 | + """Tears down actual proxies""" |
376 | + pass |
377 | + |
378 | + def init_host(self): |
379 | + """Start up any config'ed consoles on start""" |
380 | + pass |
381 | + |
382 | + def generate_password(self, length=8): |
383 | + """Returns random console password""" |
384 | + return "fakepass" |
385 | + |
386 | + def get_port(self, context): |
387 | + """get available port for consoles that need one""" |
388 | + return 5999 |
389 | + |
390 | + def fix_pool_password(self, password): |
391 | + """Trim password to length, and any other massaging""" |
392 | + return password |
393 | + |
394 | + def fix_console_password(self, password): |
395 | + """Trim password to length, and any other massaging""" |
396 | + return password |
397 | |
398 | === added file 'nova/console/manager.py' |
399 | --- nova/console/manager.py 1970-01-01 00:00:00 +0000 |
400 | +++ nova/console/manager.py 2011-01-10 21:04:17 +0000 |
401 | @@ -0,0 +1,127 @@ |
402 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
403 | + |
404 | +# Copyright (c) 2010 Openstack, LLC. |
405 | +# All Rights Reserved. |
406 | +# |
407 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
408 | +# not use this file except in compliance with the License. You may obtain |
409 | +# a copy of the License at |
410 | +# |
411 | +# http://www.apache.org/licenses/LICENSE-2.0 |
412 | +# |
413 | +# Unless required by applicable law or agreed to in writing, software |
414 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
415 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
416 | +# License for the specific language governing permissions and limitations |
417 | +# under the License. |
418 | + |
419 | +""" |
420 | +Console Proxy Service |
421 | +""" |
422 | + |
423 | +import functools |
424 | +import logging |
425 | +import socket |
426 | + |
427 | +from nova import exception |
428 | +from nova import flags |
429 | +from nova import manager |
430 | +from nova import rpc |
431 | +from nova import utils |
432 | + |
433 | +FLAGS = flags.FLAGS |
434 | +flags.DEFINE_string('console_driver', |
435 | + 'nova.console.xvp.XVPConsoleProxy', |
436 | + 'Driver to use for the console proxy') |
437 | +flags.DEFINE_boolean('stub_compute', False, |
438 | + 'Stub calls to compute worker for tests') |
439 | +flags.DEFINE_string('console_public_hostname', |
440 | + socket.gethostname(), |
441 | + 'Publicly visable name for this console host') |
442 | + |
443 | + |
444 | +class ConsoleProxyManager(manager.Manager): |
445 | + |
446 | + """ Sets up and tears down any proxy connections needed for accessing |
447 | + instance consoles securely""" |
448 | + |
449 | + def __init__(self, console_driver=None, *args, **kwargs): |
450 | + if not console_driver: |
451 | + console_driver = FLAGS.console_driver |
452 | + self.driver = utils.import_object(console_driver) |
453 | + super(ConsoleProxyManager, self).__init__(*args, **kwargs) |
454 | + self.driver.host = self.host |
455 | + |
456 | + def init_host(self): |
457 | + self.driver.init_host() |
458 | + |
459 | + @exception.wrap_exception |
460 | + def add_console(self, context, instance_id, password=None, |
461 | + port=None, **kwargs): |
462 | + instance = self.db.instance_get(context, instance_id) |
463 | + host = instance['host'] |
464 | + name = instance['name'] |
465 | + pool = self.get_pool_for_instance_host(context, host) |
466 | + try: |
467 | + console = self.db.console_get_by_pool_instance(context, |
468 | + pool['id'], |
469 | + instance_id) |
470 | + except exception.NotFound: |
471 | + logging.debug("Adding console") |
472 | + if not password: |
473 | + password = self.driver.generate_password() |
474 | + if not port: |
475 | + port = self.driver.get_port(context) |
476 | + console_data = {'instance_name': name, |
477 | + 'instance_id': instance_id, |
478 | + 'password': password, |
479 | + 'pool_id': pool['id']} |
480 | + if port: |
481 | + console_data['port'] = port |
482 | + console = self.db.console_create(context, console_data) |
483 | + self.driver.setup_console(context, console) |
484 | + return console['id'] |
485 | + |
486 | + @exception.wrap_exception |
487 | + def remove_console(self, context, console_id, **_kwargs): |
488 | + try: |
489 | + console = self.db.console_get(context, console_id) |
490 | + except exception.NotFound: |
491 | + logging.debug(_('Tried to remove non-existant console ' |
492 | + '%(console_id)s.') % |
493 | + {'console_id': console_id}) |
494 | + return |
495 | + self.db.console_delete(context, console_id) |
496 | + self.driver.teardown_console(context, console) |
497 | + |
498 | + def get_pool_for_instance_host(self, context, instance_host): |
499 | + context = context.elevated() |
500 | + console_type = self.driver.console_type |
501 | + try: |
502 | + pool = self.db.console_pool_get_by_host_type(context, |
503 | + instance_host, |
504 | + self.host, |
505 | + console_type) |
506 | + except exception.NotFound: |
507 | + #NOTE(mdragon): Right now, the only place this info exists is the |
508 | + # compute worker's flagfile, at least for |
509 | + # xenserver. Thus we ned to ask. |
510 | + if FLAGS.stub_compute: |
511 | + pool_info = {'address': '127.0.0.1', |
512 | + 'username': 'test', |
513 | + 'password': '1234pass'} |
514 | + else: |
515 | + pool_info = rpc.call(context, |
516 | + self.db.queue_get_for(context, |
517 | + FLAGS.compute_topic, |
518 | + instance_host), |
519 | + {"method": "get_console_pool_info", |
520 | + "args": {"console_type": console_type}}) |
521 | + pool_info['password'] = self.driver.fix_pool_password( |
522 | + pool_info['password']) |
523 | + pool_info['host'] = self.host |
524 | + pool_info['public_hostname'] = FLAGS.console_public_hostname |
525 | + pool_info['console_type'] = self.driver.console_type |
526 | + pool_info['compute_host'] = instance_host |
527 | + pool = self.db.console_pool_create(context, pool_info) |
528 | + return pool |
529 | |
530 | === added file 'nova/console/xvp.conf.template' |
531 | --- nova/console/xvp.conf.template 1970-01-01 00:00:00 +0000 |
532 | +++ nova/console/xvp.conf.template 2011-01-10 21:04:17 +0000 |
533 | @@ -0,0 +1,16 @@ |
534 | +# One time password use with time window |
535 | +OTP ALLOW IPCHECK HTTP 60 |
536 | +#if $multiplex_port |
537 | +MULTIPLEX $multiplex_port |
538 | +#end if |
539 | + |
540 | +#for $pool in $pools |
541 | +POOL $pool.address |
542 | + DOMAIN $pool.address |
543 | + MANAGER root $pool.password |
544 | + HOST $pool.address |
545 | + VM - dummy 0123456789ABCDEF |
546 | + #for $console in $pool.consoles |
547 | + VM #if $multiplex_port then '-' else $console.port # $console.instance_name $pass_encode($console.password) |
548 | + #end for |
549 | +#end for |
550 | |
551 | === added file 'nova/console/xvp.py' |
552 | --- nova/console/xvp.py 1970-01-01 00:00:00 +0000 |
553 | +++ nova/console/xvp.py 2011-01-10 21:04:17 +0000 |
554 | @@ -0,0 +1,194 @@ |
555 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
556 | + |
557 | +# Copyright (c) 2010 Openstack, LLC. |
558 | +# All Rights Reserved. |
559 | +# |
560 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
561 | +# not use this file except in compliance with the License. You may obtain |
562 | +# a copy of the License at |
563 | +# |
564 | +# http://www.apache.org/licenses/LICENSE-2.0 |
565 | +# |
566 | +# Unless required by applicable law or agreed to in writing, software |
567 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
568 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
569 | +# License for the specific language governing permissions and limitations |
570 | +# under the License. |
571 | + |
572 | +""" |
573 | +XVP (Xenserver VNC Proxy) driver. |
574 | +""" |
575 | + |
576 | +import fcntl |
577 | +import logging |
578 | +import os |
579 | +import signal |
580 | +import subprocess |
581 | + |
582 | +from Cheetah.Template import Template |
583 | + |
584 | +from nova import context |
585 | +from nova import db |
586 | +from nova import exception |
587 | +from nova import flags |
588 | +from nova import utils |
589 | + |
590 | +flags.DEFINE_string('console_xvp_conf_template', |
591 | + utils.abspath('console/xvp.conf.template'), |
592 | + 'XVP conf template') |
593 | +flags.DEFINE_string('console_xvp_conf', |
594 | + '/etc/xvp.conf', |
595 | + 'generated XVP conf file') |
596 | +flags.DEFINE_string('console_xvp_pid', |
597 | + '/var/run/xvp.pid', |
598 | + 'XVP master process pid file') |
599 | +flags.DEFINE_string('console_xvp_log', |
600 | + '/var/log/xvp.log', |
601 | + 'XVP log file') |
602 | +flags.DEFINE_integer('console_xvp_multiplex_port', |
603 | + 5900, |
604 | + "port for XVP to multiplex VNC connections on") |
605 | +FLAGS = flags.FLAGS |
606 | + |
607 | + |
608 | +class XVPConsoleProxy(object): |
609 | + """Sets up XVP config, and manages xvp daemon""" |
610 | + |
611 | + def __init__(self): |
612 | + self.xvpconf_template = open(FLAGS.console_xvp_conf_template).read() |
613 | + self.host = FLAGS.host # default, set by manager. |
614 | + super(XVPConsoleProxy, self).__init__() |
615 | + |
616 | + @property |
617 | + def console_type(self): |
618 | + return "vnc+xvp" |
619 | + |
620 | + def get_port(self, context): |
621 | + """get available port for consoles that need one""" |
622 | + #TODO(mdragon): implement port selection for non multiplex ports, |
623 | + # we are not using that, but someone else may want |
624 | + # it. |
625 | + return FLAGS.console_xvp_multiplex_port |
626 | + |
627 | + def setup_console(self, context, console): |
628 | + """Sets up actual proxies""" |
629 | + self._rebuild_xvp_conf(context.elevated()) |
630 | + |
631 | + def teardown_console(self, context, console): |
632 | + """Tears down actual proxies""" |
633 | + self._rebuild_xvp_conf(context.elevated()) |
634 | + |
635 | + def init_host(self): |
636 | + """Start up any config'ed consoles on start""" |
637 | + ctxt = context.get_admin_context() |
638 | + self._rebuild_xvp_conf(ctxt) |
639 | + |
640 | + def fix_pool_password(self, password): |
641 | + """Trim password to length, and encode""" |
642 | + return self._xvp_encrypt(password, is_pool_password=True) |
643 | + |
644 | + def fix_console_password(self, password): |
645 | + """Trim password to length, and encode""" |
646 | + return self._xvp_encrypt(password) |
647 | + |
648 | + def generate_password(self, length=8): |
649 | + """Returns random console password""" |
650 | + return os.urandom(length * 2).encode('base64')[:length] |
651 | + |
652 | + def _rebuild_xvp_conf(self, context): |
653 | + logging.debug("Rebuilding xvp conf") |
654 | + pools = [pool for pool in |
655 | + db.console_pool_get_all_by_host_type(context, self.host, |
656 | + self.console_type) |
657 | + if pool['consoles']] |
658 | + if not pools: |
659 | + logging.debug("No console pools!") |
660 | + self._xvp_stop() |
661 | + return |
662 | + conf_data = {'multiplex_port': FLAGS.console_xvp_multiplex_port, |
663 | + 'pools': pools, |
664 | + 'pass_encode': self.fix_console_password} |
665 | + config = str(Template(self.xvpconf_template, searchList=[conf_data])) |
666 | + self._write_conf(config) |
667 | + self._xvp_restart() |
668 | + |
669 | + def _write_conf(self, config): |
670 | + logging.debug('Re-wrote %s' % FLAGS.console_xvp_conf) |
671 | + with open(FLAGS.console_xvp_conf, 'w') as cfile: |
672 | + cfile.write(config) |
673 | + |
674 | + def _xvp_stop(self): |
675 | + logging.debug("Stopping xvp") |
676 | + pid = self._xvp_pid() |
677 | + if not pid: |
678 | + return |
679 | + try: |
680 | + os.kill(pid, signal.SIGTERM) |
681 | + except OSError: |
682 | + #if it's already not running, no problem. |
683 | + pass |
684 | + |
685 | + def _xvp_start(self): |
686 | + if self._xvp_check_running(): |
687 | + return |
688 | + logging.debug("Starting xvp") |
689 | + try: |
690 | + utils.execute('xvp -p %s -c %s -l %s' % |
691 | + (FLAGS.console_xvp_pid, |
692 | + FLAGS.console_xvp_conf, |
693 | + FLAGS.console_xvp_log)) |
694 | + except exception.ProcessExecutionError, err: |
695 | + logging.error("Error starting xvp: %s" % err) |
696 | + |
697 | + def _xvp_restart(self): |
698 | + logging.debug("Restarting xvp") |
699 | + if not self._xvp_check_running(): |
700 | + logging.debug("xvp not running...") |
701 | + self._xvp_start() |
702 | + else: |
703 | + pid = self._xvp_pid() |
704 | + os.kill(pid, signal.SIGUSR1) |
705 | + |
706 | + def _xvp_pid(self): |
707 | + try: |
708 | + with open(FLAGS.console_xvp_pid, 'r') as pidfile: |
709 | + pid = int(pidfile.read()) |
710 | + except IOError: |
711 | + return None |
712 | + except ValueError: |
713 | + return None |
714 | + return pid |
715 | + |
716 | + def _xvp_check_running(self): |
717 | + pid = self._xvp_pid() |
718 | + if not pid: |
719 | + return False |
720 | + try: |
721 | + os.kill(pid, 0) |
722 | + except OSError: |
723 | + return False |
724 | + return True |
725 | + |
726 | + def _xvp_encrypt(self, password, is_pool_password=False): |
727 | + """Call xvp to obfuscate passwords for config file. |
728 | + |
729 | + Args: |
730 | + - password: the password to encode, max 8 char for vm passwords, |
731 | + and 16 chars for pool passwords. passwords will |
732 | + be trimmed to max len before encoding. |
733 | + - is_pool_password: True if this this is the XenServer api password |
734 | + False if it's a VM console password |
735 | + (xvp uses different keys and max lengths for pool passwords) |
736 | + |
737 | + Note that xvp's obfuscation should not be considered 'real' encryption. |
738 | + It simply DES encrypts the passwords with static keys plainly viewable |
739 | + in the xvp source code.""" |
740 | + maxlen = 8 |
741 | + flag = '-e' |
742 | + if is_pool_password: |
743 | + maxlen = 16 |
744 | + flag = '-x' |
745 | + #xvp will blow up on passwords that are too long (mdragon) |
746 | + password = password[:maxlen] |
747 | + out, err = utils.execute('xvp %s' % flag, process_input=password) |
748 | + return out.strip() |
749 | |
750 | === modified file 'nova/db/api.py' |
751 | --- nova/db/api.py 2011-01-10 15:57:13 +0000 |
752 | +++ nova/db/api.py 2011-01-10 21:04:17 +0000 |
753 | @@ -906,3 +906,57 @@ |
754 | |
755 | """ |
756 | return IMPL.host_get_networks(context, host) |
757 | + |
758 | + |
759 | +################## |
760 | + |
761 | + |
762 | +def console_pool_create(context, values): |
763 | + """Create console pool.""" |
764 | + return IMPL.console_pool_create(context, values) |
765 | + |
766 | + |
767 | +def console_pool_get(context, pool_id): |
768 | + """Get a console pool.""" |
769 | + return IMPL.console_pool_get(context, pool_id) |
770 | + |
771 | + |
772 | +def console_pool_get_by_host_type(context, compute_host, proxy_host, |
773 | + console_type): |
774 | + """Fetch a console pool for a given proxy host, compute host, and type.""" |
775 | + return IMPL.console_pool_get_by_host_type(context, |
776 | + compute_host, |
777 | + proxy_host, |
778 | + console_type) |
779 | + |
780 | + |
781 | +def console_pool_get_all_by_host_type(context, host, console_type): |
782 | + """Fetch all pools for given proxy host and type.""" |
783 | + return IMPL.console_pool_get_all_by_host_type(context, |
784 | + host, |
785 | + console_type) |
786 | + |
787 | + |
788 | +def console_create(context, values): |
789 | + """Create a console.""" |
790 | + return IMPL.console_create(context, values) |
791 | + |
792 | + |
793 | +def console_delete(context, console_id): |
794 | + """Delete a console.""" |
795 | + return IMPL.console_delete(context, console_id) |
796 | + |
797 | + |
798 | +def console_get_by_pool_instance(context, pool_id, instance_id): |
799 | + """Get console entry for a given instance and pool.""" |
800 | + return IMPL.console_get_by_pool_instance(context, pool_id, instance_id) |
801 | + |
802 | + |
803 | +def console_get_all_by_instance(context, instance_id): |
804 | + """Get consoles for a given instance.""" |
805 | + return IMPL.console_get_all_by_instance(context, instance_id) |
806 | + |
807 | + |
808 | +def console_get(context, console_id, instance_id=None): |
809 | + """Get a specific console (possibly on a given instance).""" |
810 | + return IMPL.console_get(context, console_id, instance_id) |
811 | |
812 | === modified file 'nova/db/sqlalchemy/api.py' |
813 | --- nova/db/sqlalchemy/api.py 2011-01-10 15:57:13 +0000 |
814 | +++ nova/db/sqlalchemy/api.py 2011-01-10 21:04:17 +0000 |
815 | @@ -1863,3 +1863,111 @@ |
816 | filter_by(deleted=False).\ |
817 | filter_by(host=host).\ |
818 | all() |
819 | + |
820 | + |
821 | +################## |
822 | + |
823 | + |
824 | +def console_pool_create(context, values): |
825 | + pool = models.ConsolePool() |
826 | + pool.update(values) |
827 | + pool.save() |
828 | + return pool |
829 | + |
830 | + |
831 | +def console_pool_get(context, pool_id): |
832 | + session = get_session() |
833 | + result = session.query(models.ConsolePool).\ |
834 | + filter_by(deleted=False).\ |
835 | + filter_by(id=pool_id).\ |
836 | + first() |
837 | + if not result: |
838 | + raise exception.NotFound(_("No console pool with id %(pool_id)s") % |
839 | + {'pool_id': pool_id}) |
840 | + |
841 | + return result |
842 | + |
843 | + |
844 | +def console_pool_get_by_host_type(context, compute_host, host, |
845 | + console_type): |
846 | + session = get_session() |
847 | + result = session.query(models.ConsolePool).\ |
848 | + filter_by(host=host).\ |
849 | + filter_by(console_type=console_type).\ |
850 | + filter_by(compute_host=compute_host).\ |
851 | + filter_by(deleted=False).\ |
852 | + options(joinedload('consoles')).\ |
853 | + first() |
854 | + if not result: |
855 | + raise exception.NotFound(_('No console pool of type %(type)s ' |
856 | + 'for compute host %(compute_host)s ' |
857 | + 'on proxy host %(host)s') % |
858 | + {'type': console_type, |
859 | + 'compute_host': compute_host, |
860 | + 'host': host}) |
861 | + return result |
862 | + |
863 | + |
864 | +def console_pool_get_all_by_host_type(context, host, console_type): |
865 | + session = get_session() |
866 | + return session.query(models.ConsolePool).\ |
867 | + filter_by(host=host).\ |
868 | + filter_by(console_type=console_type).\ |
869 | + filter_by(deleted=False).\ |
870 | + options(joinedload('consoles')).\ |
871 | + all() |
872 | + |
873 | + |
874 | +def console_create(context, values): |
875 | + console = models.Console() |
876 | + console.update(values) |
877 | + console.save() |
878 | + return console |
879 | + |
880 | + |
881 | +def console_delete(context, console_id): |
882 | + session = get_session() |
883 | + with session.begin(): |
884 | + # consoles are meant to be transient. (mdragon) |
885 | + session.execute('delete from consoles ' |
886 | + 'where id=:id', {'id': console_id}) |
887 | + |
888 | + |
889 | +def console_get_by_pool_instance(context, pool_id, instance_id): |
890 | + session = get_session() |
891 | + result = session.query(models.Console).\ |
892 | + filter_by(pool_id=pool_id).\ |
893 | + filter_by(instance_id=instance_id).\ |
894 | + options(joinedload('pool')).\ |
895 | + first() |
896 | + if not result: |
897 | + raise exception.NotFound(_('No console for instance %(instance_id)s ' |
898 | + 'in pool %(pool_id)s') % |
899 | + {'instance_id': instance_id, |
900 | + 'pool_id': pool_id}) |
901 | + return result |
902 | + |
903 | + |
904 | +def console_get_all_by_instance(context, instance_id): |
905 | + session = get_session() |
906 | + results = session.query(models.Console).\ |
907 | + filter_by(instance_id=instance_id).\ |
908 | + options(joinedload('pool')).\ |
909 | + all() |
910 | + return results |
911 | + |
912 | + |
913 | +def console_get(context, console_id, instance_id=None): |
914 | + session = get_session() |
915 | + query = session.query(models.Console).\ |
916 | + filter_by(id=console_id) |
917 | + if instance_id: |
918 | + query = query.filter_by(instance_id=instance_id) |
919 | + result = query.options(joinedload('pool')).first() |
920 | + if not result: |
921 | + idesc = (_("on instance %s") % instance_id) if instance_id else "" |
922 | + raise exception.NotFound(_("No console with id %(console_id)s" |
923 | + " %(instance)s") % |
924 | + {'instance': idesc, |
925 | + 'console_id': console_id}) |
926 | + return result |
927 | |
928 | === modified file 'nova/db/sqlalchemy/models.py' |
929 | --- nova/db/sqlalchemy/models.py 2011-01-06 22:35:48 +0000 |
930 | +++ nova/db/sqlalchemy/models.py 2011-01-10 21:04:17 +0000 |
931 | @@ -540,6 +540,31 @@ |
932 | host = Column(String(255)) # , ForeignKey('hosts.id')) |
933 | |
934 | |
935 | +class ConsolePool(BASE, NovaBase): |
936 | + """Represents pool of consoles on the same physical node.""" |
937 | + __tablename__ = 'console_pools' |
938 | + id = Column(Integer, primary_key=True) |
939 | + address = Column(String(255)) |
940 | + username = Column(String(255)) |
941 | + password = Column(String(255)) |
942 | + console_type = Column(String(255)) |
943 | + public_hostname = Column(String(255)) |
944 | + host = Column(String(255)) |
945 | + compute_host = Column(String(255)) |
946 | + |
947 | + |
948 | +class Console(BASE, NovaBase): |
949 | + """Represents a console session for an instance.""" |
950 | + __tablename__ = 'consoles' |
951 | + id = Column(Integer, primary_key=True) |
952 | + instance_name = Column(String(255)) |
953 | + instance_id = Column(Integer) |
954 | + password = Column(String(255)) |
955 | + port = Column(Integer, nullable=True) |
956 | + pool_id = Column(Integer, ForeignKey('console_pools.id')) |
957 | + pool = relationship(ConsolePool, backref=backref('consoles')) |
958 | + |
959 | + |
960 | def register_models(): |
961 | """Register Models and create metadata. |
962 | |
963 | @@ -552,7 +577,7 @@ |
964 | Volume, ExportDevice, IscsiTarget, FixedIp, FloatingIp, |
965 | Network, SecurityGroup, SecurityGroupIngressRule, |
966 | SecurityGroupInstanceAssociation, AuthToken, User, |
967 | - Project, Certificate) # , Image, Host |
968 | + Project, Certificate, ConsolePool, Console) # , Image, Host |
969 | engine = create_engine(FLAGS.sql_connection, echo=False) |
970 | for model in models: |
971 | model.metadata.create_all(engine) |
972 | |
973 | === modified file 'nova/flags.py' |
974 | --- nova/flags.py 2011-01-04 05:23:35 +0000 |
975 | +++ nova/flags.py 2011-01-10 21:04:17 +0000 |
976 | @@ -216,6 +216,8 @@ |
977 | DEFINE_string('s3_host', '127.0.0.1', 's3 host (for infrastructure)') |
978 | DEFINE_string('s3_dmz', '127.0.0.1', 's3 dmz ip (for instances)') |
979 | DEFINE_string('compute_topic', 'compute', 'the topic compute nodes listen on') |
980 | +DEFINE_string('console_topic', 'console', |
981 | + 'the topic console proxy nodes listen on') |
982 | DEFINE_string('scheduler_topic', 'scheduler', |
983 | 'the topic scheduler nodes listen on') |
984 | DEFINE_string('volume_topic', 'volume', 'the topic volume nodes listen on') |
985 | @@ -269,6 +271,8 @@ |
986 | |
987 | DEFINE_string('compute_manager', 'nova.compute.manager.ComputeManager', |
988 | 'Manager for compute') |
989 | +DEFINE_string('console_manager', 'nova.console.manager.ConsoleProxyManager', |
990 | + 'Manager for console proxy') |
991 | DEFINE_string('network_manager', 'nova.network.manager.VlanManager', |
992 | 'Manager for network') |
993 | DEFINE_string('volume_manager', 'nova.volume.manager.VolumeManager', |
994 | |
995 | === added file 'nova/tests/test_console.py' |
996 | --- nova/tests/test_console.py 1970-01-01 00:00:00 +0000 |
997 | +++ nova/tests/test_console.py 2011-01-10 21:04:17 +0000 |
998 | @@ -0,0 +1,129 @@ |
999 | +# vim: tabstop=4 shiftwidth=4 softtabstop=4 |
1000 | + |
1001 | +# Copyright (c) 2010 Openstack, LLC. |
1002 | +# Administrator of the National Aeronautics and Space Administration. |
1003 | +# All Rights Reserved. |
1004 | +# |
1005 | +# Licensed under the Apache License, Version 2.0 (the "License"); you may |
1006 | +# not use this file except in compliance with the License. You may obtain |
1007 | +# a copy of the License at |
1008 | +# |
1009 | +# http://www.apache.org/licenses/LICENSE-2.0 |
1010 | +# |
1011 | +# Unless required by applicable law or agreed to in writing, software |
1012 | +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
1013 | +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
1014 | +# License for the specific language governing permissions and limitations |
1015 | +# under the License. |
1016 | + |
1017 | +""" |
1018 | +Tests For Console proxy. |
1019 | +""" |
1020 | + |
1021 | +import datetime |
1022 | +import logging |
1023 | + |
1024 | +from nova import context |
1025 | +from nova import db |
1026 | +from nova import exception |
1027 | +from nova import flags |
1028 | +from nova import test |
1029 | +from nova import utils |
1030 | +from nova.auth import manager |
1031 | +from nova.console import manager as console_manager |
1032 | + |
1033 | +FLAGS = flags.FLAGS |
1034 | + |
1035 | + |
1036 | +class ConsoleTestCase(test.TestCase): |
1037 | + """Test case for console proxy""" |
1038 | + def setUp(self): |
1039 | + logging.getLogger().setLevel(logging.DEBUG) |
1040 | + super(ConsoleTestCase, self).setUp() |
1041 | + self.flags(console_driver='nova.console.fake.FakeConsoleProxy', |
1042 | + stub_compute=True) |
1043 | + self.console = utils.import_object(FLAGS.console_manager) |
1044 | + self.manager = manager.AuthManager() |
1045 | + self.user = self.manager.create_user('fake', 'fake', 'fake') |
1046 | + self.project = self.manager.create_project('fake', 'fake', 'fake') |
1047 | + self.context = context.get_admin_context() |
1048 | + self.host = 'test_compute_host' |
1049 | + |
1050 | + def tearDown(self): |
1051 | + self.manager.delete_user(self.user) |
1052 | + self.manager.delete_project(self.project) |
1053 | + super(ConsoleTestCase, self).tearDown() |
1054 | + |
1055 | + def _create_instance(self): |
1056 | + """Create a test instance""" |
1057 | + inst = {} |
1058 | + #inst['host'] = self.host |
1059 | + #inst['name'] = 'instance-1234' |
1060 | + inst['image_id'] = 'ami-test' |
1061 | + inst['reservation_id'] = 'r-fakeres' |
1062 | + inst['launch_time'] = '10' |
1063 | + inst['user_id'] = self.user.id |
1064 | + inst['project_id'] = self.project.id |
1065 | + inst['instance_type'] = 'm1.tiny' |
1066 | + inst['mac_address'] = utils.generate_mac() |
1067 | + inst['ami_launch_index'] = 0 |
1068 | + return db.instance_create(self.context, inst)['id'] |
1069 | + |
1070 | + def test_get_pool_for_instance_host(self): |
1071 | + pool = self.console.get_pool_for_instance_host(self.context, self.host) |
1072 | + self.assertEqual(pool['compute_host'], self.host) |
1073 | + |
1074 | + def test_get_pool_creates_new_pool_if_needed(self): |
1075 | + self.assertRaises(exception.NotFound, |
1076 | + db.console_pool_get_by_host_type, |
1077 | + self.context, |
1078 | + self.host, |
1079 | + self.console.host, |
1080 | + self.console.driver.console_type) |
1081 | + pool = self.console.get_pool_for_instance_host(self.context, |
1082 | + self.host) |
1083 | + pool2 = db.console_pool_get_by_host_type(self.context, |
1084 | + self.host, |
1085 | + self.console.host, |
1086 | + self.console.driver.console_type) |
1087 | + self.assertEqual(pool['id'], pool2['id']) |
1088 | + |
1089 | + def test_get_pool_does_not_create_new_pool_if_exists(self): |
1090 | + pool_info = {'address': '127.0.0.1', |
1091 | + 'username': 'test', |
1092 | + 'password': '1234pass', |
1093 | + 'host': self.console.host, |
1094 | + 'console_type': self.console.driver.console_type, |
1095 | + 'compute_host': 'sometesthostname'} |
1096 | + new_pool = db.console_pool_create(self.context, pool_info) |
1097 | + pool = self.console.get_pool_for_instance_host(self.context, |
1098 | + 'sometesthostname') |
1099 | + self.assertEqual(pool['id'], new_pool['id']) |
1100 | + |
1101 | + def test_add_console(self): |
1102 | + instance_id = self._create_instance() |
1103 | + self.console.add_console(self.context, instance_id) |
1104 | + instance = db.instance_get(self.context, instance_id) |
1105 | + pool = db.console_pool_get_by_host_type(self.context, |
1106 | + instance['host'], |
1107 | + self.console.host, |
1108 | + self.console.driver.console_type) |
1109 | + |
1110 | + console_instances = [con['instance_id'] for con in pool.consoles] |
1111 | + self.assert_(instance_id in console_instances) |
1112 | + |
1113 | + def test_add_console_does_not_duplicate(self): |
1114 | + instance_id = self._create_instance() |
1115 | + cons1 = self.console.add_console(self.context, instance_id) |
1116 | + cons2 = self.console.add_console(self.context, instance_id) |
1117 | + self.assertEqual(cons1, cons2) |
1118 | + |
1119 | + def test_remove_console(self): |
1120 | + instance_id = self._create_instance() |
1121 | + console_id = self.console.add_console(self.context, instance_id) |
1122 | + self.console.remove_console(self.context, console_id) |
1123 | + |
1124 | + self.assertRaises(exception.NotFound, |
1125 | + db.console_get, |
1126 | + self.context, |
1127 | + console_id) |
1128 | |
1129 | === modified file 'nova/tests/test_virt.py' |
1130 | --- nova/tests/test_virt.py 2011-01-10 10:32:17 +0000 |
1131 | +++ nova/tests/test_virt.py 2011-01-10 21:04:17 +0000 |
1132 | @@ -249,7 +249,7 @@ |
1133 | '-A FORWARD -o virbr0 -j REJECT --reject-with icmp-port-unreachable ', |
1134 | '-A FORWARD -i virbr0 -j REJECT --reject-with icmp-port-unreachable ', |
1135 | 'COMMIT', |
1136 | - '# Completed on Mon Dec 6 11:54:13 2010' |
1137 | + '# Completed on Mon Dec 6 11:54:13 2010', |
1138 | ] |
1139 | |
1140 | def test_static_filters(self): |
1141 | |
1142 | === modified file 'nova/virt/fake.py' |
1143 | --- nova/virt/fake.py 2010-12-30 21:23:14 +0000 |
1144 | +++ nova/virt/fake.py 2011-01-10 21:04:17 +0000 |
1145 | @@ -289,6 +289,11 @@ |
1146 | def get_console_output(self, instance): |
1147 | return 'FAKE CONSOLE OUTPUT' |
1148 | |
1149 | + def get_console_pool_info(self, console_type): |
1150 | + return {'address': '127.0.0.1', |
1151 | + 'username': 'fakeuser', |
1152 | + 'password': 'fakepassword'} |
1153 | + |
1154 | |
1155 | class FakeInstance(object): |
1156 | |
1157 | |
1158 | === modified file 'nova/virt/hyperv.py' |
1159 | --- nova/virt/hyperv.py 2011-01-07 14:46:17 +0000 |
1160 | +++ nova/virt/hyperv.py 2011-01-10 21:04:17 +0000 |
1161 | @@ -92,7 +92,7 @@ |
1162 | 'Reboot': 10, |
1163 | 'Reset': 11, |
1164 | 'Paused': 32768, |
1165 | - 'Suspended': 32769 |
1166 | + 'Suspended': 32769, |
1167 | } |
1168 | |
1169 | |
1170 | |
1171 | === modified file 'nova/virt/libvirt_conn.py' |
1172 | --- nova/virt/libvirt_conn.py 2011-01-08 14:35:50 +0000 |
1173 | +++ nova/virt/libvirt_conn.py 2011-01-10 21:04:17 +0000 |
1174 | @@ -707,6 +707,14 @@ |
1175 | domain = self._conn.lookupByName(instance_name) |
1176 | return domain.interfaceStats(interface) |
1177 | |
1178 | + def get_console_pool_info(self, console_type): |
1179 | + #TODO(mdragon): console proxy should be implemented for libvirt, |
1180 | + # in case someone wants to use it with kvm or |
1181 | + # such. For now return fake data. |
1182 | + return {'address': '127.0.0.1', |
1183 | + 'username': 'fakeuser', |
1184 | + 'password': 'fakepassword'} |
1185 | + |
1186 | def refresh_security_group_rules(self, security_group_id): |
1187 | self.firewall_driver.refresh_security_group_rules(security_group_id) |
1188 | |
1189 | |
1190 | === modified file 'nova/virt/xenapi_conn.py' |
1191 | --- nova/virt/xenapi_conn.py 2011-01-07 14:46:17 +0000 |
1192 | +++ nova/virt/xenapi_conn.py 2011-01-10 21:04:17 +0000 |
1193 | @@ -52,6 +52,7 @@ |
1194 | """ |
1195 | |
1196 | import sys |
1197 | +import urlparse |
1198 | import xmlrpclib |
1199 | |
1200 | from eventlet import event |
1201 | @@ -190,6 +191,12 @@ |
1202 | """Detach volume storage to VM instance""" |
1203 | return self._volumeops.detach_volume(instance_name, mountpoint) |
1204 | |
1205 | + def get_console_pool_info(self, console_type): |
1206 | + xs_url = urlparse.urlparse(FLAGS.xenapi_connection_url) |
1207 | + return {'address': xs_url.netloc, |
1208 | + 'username': FLAGS.xenapi_connection_username, |
1209 | + 'password': FLAGS.xenapi_connection_password} |
1210 | + |
1211 | |
1212 | class XenAPISession(object): |
1213 | """The session to invoke XenAPI SDK calls""" |
lgtm. Maybe update this at some point with some docstrings for the ConsoleAPI methods. otherwise everything looks good.