Merge lp:~frankban/juju-quickstart/quickstart-deploy into lp:juju-quickstart
- quickstart-deploy
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 5 |
Proposed branch: | lp:~frankban/juju-quickstart/quickstart-deploy |
Merge into: | lp:juju-quickstart |
Diff against target: |
625 lines (+497/-17) 10 files modified
.lbox (+1/-1) Makefile (+1/-1) quickstart/app.py (+46/-1) quickstart/juju.py (+108/-0) quickstart/manage.py (+3/-0) quickstart/tests/helpers.py (+14/-14) quickstart/tests/test_app.py (+84/-0) quickstart/tests/test_juju.py (+210/-0) quickstart/tests/test_utils.py (+20/-0) quickstart/utils.py (+10/-0) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/quickstart-deploy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+191679@code.launchpad.net |
Commit message
Description of the change
Deploy and expose the GUI.
Use a customized jujuclient.
object in order to be able deploy to a
specific machine and to log traffic.
Tests: make check
QA: make run -> no errors and a resulting
juju environment with the GUI exposed
and deploying in the bootstrap node.
Francesco Banconi (frankban) wrote : | # |
Gary Poster (gary) wrote : | # |
LGTM, with trivial. Thank you!
https:/
File quickstart/app.py (right):
https:/
quickstart/
from
Nice way to be incremental.
https:/
File quickstart/juju.py (right):
https:/
quickstart/
I'm guessing the watcher stops itself and closes when the context
manager hook is called?
https:/
quickstart/
If the previous comment is correct, it might be nice to add a comment to
that regard here. On first glance it seems odd that you have to call
start but you don't have to call stop.
Madison Scott-Clary (makyo) wrote : | # |
LGTM, thank you!
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Deploy and expose the GUI.
Use a customized jujuclient.
object in order to be able deploy to a
specific machine and to log traffic.
Tests: make check
QA: make run -> no errors and a resulting
juju environment with the GUI exposed
and deploying in the bootstrap node.
R=gary.poster, matthew.scott
CC=
https:/
https:/
File quickstart/juju.py (right):
https:/
quickstart/
On 2013/10/17 17:20:13, gary.poster wrote:
> I'm guessing the watcher stops itself and closes when the context
manager hook
> is called?
Yes indeed, added a comment.
https:/
quickstart/
On 2013/10/17 17:20:13, gary.poster wrote:
> If the previous comment is correct, it might be nice to add a comment
to that
> regard here. On first glance it seems odd that you have to call start
but you
> don't have to call stop.
Well, removed the start call. The watcher is not started by the context
manager's __start__, but its "next" method does the following:
if self.watcher_id is None:
So I hope removing the start() call makes the code less surprising.
Thanks for this suggestion.
Francesco Banconi (frankban) wrote : | # |
Thank you both for the reviews!
Preview Diff
1 | === modified file '.lbox' |
2 | --- .lbox 2013-10-14 11:43:06 +0000 |
3 | +++ .lbox 2013-10-17 16:57:02 +0000 |
4 | @@ -1,1 +1,1 @@ |
5 | -propose -cr -for lp:juju-gui/juju-quickstart |
6 | +propose -cr -for lp:juju-quickstart |
7 | |
8 | === modified file 'Makefile' |
9 | --- Makefile 2013-10-15 18:25:57 +0000 |
10 | +++ Makefile 2013-10-17 16:57:02 +0000 |
11 | @@ -63,7 +63,7 @@ |
12 | @$(VENV)/bin/flake8 --show-source --exclude=$(VENV) ./quickstart |
13 | |
14 | run: setup |
15 | - $(VENV)/bin/python ./juju-quickstart |
16 | + $(VENV)/bin/python ./juju-quickstart --debug |
17 | |
18 | source: |
19 | $(PYTHON) setup.py sdist |
20 | |
21 | === modified file 'quickstart/app.py' |
22 | --- quickstart/app.py 2013-10-16 15:59:23 +0000 |
23 | +++ quickstart/app.py 2013-10-17 16:57:02 +0000 |
24 | @@ -18,7 +18,12 @@ |
25 | |
26 | import json |
27 | |
28 | -from quickstart import utils |
29 | +import jujuclient |
30 | + |
31 | +from quickstart import ( |
32 | + juju, |
33 | + utils, |
34 | +) |
35 | |
36 | |
37 | class ProgramExit(Exception): |
38 | @@ -66,6 +71,7 @@ |
39 | |
40 | Use the Juju CLI in a subprocess in order to retrieve the API addresses. |
41 | Return the complete URL, e.g. "wss://api.example.com:17070". |
42 | + Raise a ProgramExit if any error occurs. |
43 | """ |
44 | retcode, output, error = utils.call( |
45 | 'juju', 'api-endpoints', '-e', env_name, '--format', 'json') |
46 | @@ -75,3 +81,42 @@ |
47 | # from the JSON output. |
48 | api_address = json.loads(output)[0] |
49 | return 'wss://{}'.format(api_address) |
50 | + |
51 | + |
52 | +def connect(api_url, admin_secret): |
53 | + """Connect to the Juju API server and log in using the given credentials. |
54 | + |
55 | + Return a connected and authenticated Juju Environment instance. |
56 | + Raise a ProgramExit if any error occurs while establishing the WebSocket |
57 | + connection or if the API returns an error response. |
58 | + """ |
59 | + try: |
60 | + env = juju.connect(api_url) |
61 | + except Exception as err: |
62 | + msg = 'unable to connect to the Juju API server on {}: {}' |
63 | + raise ProgramExit(msg.format(api_url, err)) |
64 | + try: |
65 | + env.login(admin_secret) |
66 | + except jujuclient.EnvError as err: |
67 | + msg = 'unable to log in to the Juju API server on {}: {}' |
68 | + raise ProgramExit(msg.format(api_url, err.message)) |
69 | + return env |
70 | + |
71 | + |
72 | +def deploy_gui(env, service_name): |
73 | + """Deploy and expose the given service, reusing the bootstrap node. |
74 | + |
75 | + Receive an authenticated Juju Environment instance, the name of the service |
76 | + and the corresponding charm URL. |
77 | + |
78 | + Raise a ProgramExit if the API server returns an error response. |
79 | + """ |
80 | + # XXX 2013-10-17 frankban: |
81 | + # Retrieve the URL of the last charm revision from |
82 | + # manage.jujucharms.com. |
83 | + charm_url = 'cs:precise/juju-gui-77' |
84 | + try: |
85 | + env.deploy(service_name, charm_url, to=0) |
86 | + env.expose(service_name) |
87 | + except jujuclient.EnvError as err: |
88 | + raise ProgramExit('bad API server response: {}'.format(err.message)) |
89 | |
90 | === added file 'quickstart/juju.py' |
91 | --- quickstart/juju.py 1970-01-01 00:00:00 +0000 |
92 | +++ quickstart/juju.py 2013-10-17 16:57:02 +0000 |
93 | @@ -0,0 +1,108 @@ |
94 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
95 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
96 | +# Copyright (C) 2013 Canonical Ltd. |
97 | +# |
98 | +# This program is free software: you can redistribute it and/or modify it under |
99 | +# the terms of the GNU Affero General Public License version 3, as published by |
100 | +# the Free Software Foundation. |
101 | +# |
102 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
103 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
104 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
105 | +# Affero General Public License for more details. |
106 | +# |
107 | +# You should have received a copy of the GNU Affero General Public License |
108 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
109 | + |
110 | +"""Juju Quickstart API client.""" |
111 | + |
112 | +import logging |
113 | + |
114 | +import jujuclient |
115 | +import websocket |
116 | + |
117 | +from quickstart import utils |
118 | + |
119 | + |
120 | +def connect(api_url): |
121 | + """Return an Environment instance connected to the given API URL.""" |
122 | + connection = WebSocketConnection() |
123 | + # See the websocket.create_connection function. |
124 | + connection.settimeout(websocket.default_timeout) |
125 | + connection.connect(api_url, origin=api_url) |
126 | + return Environment(api_url, conn=connection) |
127 | + |
128 | + |
129 | +class Environment(jujuclient.Environment): |
130 | + """A Juju bootstrapped environment. |
131 | + |
132 | + Instances of this class can be used to run API operations on a Juju |
133 | + environment. This is a subclass of the jujuclient.Environment object. |
134 | + """ |
135 | + |
136 | + def deploy( |
137 | + self, service_name, charm_url, num_units=1, config=None, |
138 | + constraints=None, to=None): |
139 | + """Deploy a charm. Local charms are not supported. |
140 | + |
141 | + This method is overridden to add the ability to deploy to a specific |
142 | + machine (i.e. support the ToMachineSpec API parameter). |
143 | + """ |
144 | + service_config = {} |
145 | + if config is not None: |
146 | + service_config = self._prepare_strparams(config) |
147 | + service_constraints = {} |
148 | + if constraints is not None: |
149 | + service_constraints = self._prepare_constraints(constraints) |
150 | + params = { |
151 | + 'ServiceName': service_name, |
152 | + 'CharmURL': charm_url, |
153 | + 'NumUnits': num_units, |
154 | + 'Config': service_config, |
155 | + 'Constraints': service_constraints, |
156 | + } |
157 | + if to is not None: |
158 | + params['ToMachineSpec'] = str(to) |
159 | + request = { |
160 | + 'Type': 'Client', |
161 | + 'Request': 'ServiceDeploy', |
162 | + 'Params': params, |
163 | + } |
164 | + return self._rpc(request) |
165 | + |
166 | + def watch_changes(self, processor): |
167 | + """Start watching the changes occurring in the Juju environment. |
168 | + |
169 | + For each changeset, call the given processor callable, and yield |
170 | + the values returned by the processor. |
171 | + """ |
172 | + with jujuclient.Watcher(self.conn) as watcher: |
173 | + watcher.start() |
174 | + for changeset in watcher: |
175 | + changes = processor(changeset) |
176 | + if changes: |
177 | + yield changes |
178 | + |
179 | + |
180 | +class WebSocketConnection(websocket.WebSocket): |
181 | + """A WebSocket client connection. |
182 | + |
183 | + This is a subclass of the websocket.WebSocket object. |
184 | + """ |
185 | + |
186 | + def send(self, message): |
187 | + """Send the given WebSocket message. |
188 | + |
189 | + Overridden to add logging. |
190 | + """ |
191 | + logging.debug('API message: --> {}'.format(utils.utf8(message))) |
192 | + return super(WebSocketConnection, self).send(message) |
193 | + |
194 | + def recv(self): |
195 | + """Receive a message from the WebSocket server. |
196 | + |
197 | + Overridden to add logging. |
198 | + """ |
199 | + message = super(WebSocketConnection, self).recv() |
200 | + logging.debug('API message: <-- {}'.format(utils.utf8(message))) |
201 | + return message |
202 | |
203 | === modified file 'quickstart/manage.py' |
204 | --- quickstart/manage.py 2013-10-16 15:23:08 +0000 |
205 | +++ quickstart/manage.py 2013-10-17 16:57:02 +0000 |
206 | @@ -140,3 +140,6 @@ |
207 | print('retrieving the Juju API address') |
208 | api_url = app.get_api_url(options.env_name) |
209 | print('connecting to {}'.format(api_url)) |
210 | + env = app.connect(api_url, options.admin_secret) |
211 | + print('deploying Juju GUI') |
212 | + app.deploy_gui(env, 'juju-gui') |
213 | |
214 | === modified file 'quickstart/tests/helpers.py' |
215 | --- quickstart/tests/helpers.py 2013-10-16 12:12:31 +0000 |
216 | +++ quickstart/tests/helpers.py 2013-10-17 16:57:02 +0000 |
217 | @@ -24,20 +24,6 @@ |
218 | import yaml |
219 | |
220 | |
221 | -class CallTestsMixin(object): |
222 | - """Easily use the quickstart.utils.call function.""" |
223 | - |
224 | - def patch_call(self, retcode, output='', error=''): |
225 | - """Patch the quickstart.utils.call function.""" |
226 | - mock_call = mock.Mock(return_value=(retcode, output, error)) |
227 | - return mock.patch('quickstart.utils.call', mock_call) |
228 | - |
229 | - def patch_multiple_calls(self, side_effect): |
230 | - """Patch multiple subsequent quickstart.utils.call calls.""" |
231 | - mock_call = mock.Mock(side_effect=side_effect) |
232 | - return mock.patch('quickstart.utils.call', mock_call) |
233 | - |
234 | - |
235 | @contextmanager |
236 | def assert_logs(messages, level='debug'): |
237 | """Ensure the given messages are logged using the given log level. |
238 | @@ -51,6 +37,20 @@ |
239 | mock_log.assert_has_calls(expected_calls) |
240 | |
241 | |
242 | +class CallTestsMixin(object): |
243 | + """Easily use the quickstart.utils.call function.""" |
244 | + |
245 | + def patch_call(self, retcode, output='', error=''): |
246 | + """Patch the quickstart.utils.call function.""" |
247 | + mock_call = mock.Mock(return_value=(retcode, output, error)) |
248 | + return mock.patch('quickstart.utils.call', mock_call) |
249 | + |
250 | + def patch_multiple_calls(self, side_effect): |
251 | + """Patch multiple subsequent quickstart.utils.call calls.""" |
252 | + mock_call = mock.Mock(side_effect=side_effect) |
253 | + return mock.patch('quickstart.utils.call', mock_call) |
254 | + |
255 | + |
256 | class EnvFileTestsMixin(object): |
257 | """Shared methods for testing a Juju environments file.""" |
258 | |
259 | |
260 | === modified file 'quickstart/tests/test_app.py' |
261 | --- quickstart/tests/test_app.py 2013-10-16 15:59:23 +0000 |
262 | +++ quickstart/tests/test_app.py 2013-10-17 16:57:02 +0000 |
263 | @@ -20,6 +20,7 @@ |
264 | import json |
265 | import unittest |
266 | |
267 | +import jujuclient |
268 | import mock |
269 | import yaml |
270 | |
271 | @@ -49,6 +50,10 @@ |
272 | expected = 'juju-quickstart: error: {}'.format(error) |
273 | self.assertEqual(expected, str(context_manager.exception)) |
274 | |
275 | + def make_env_error(self, message): |
276 | + """Create and return a jujuclient.EnvError with the given message.""" |
277 | + return jujuclient.EnvError({'Error': message}) |
278 | + |
279 | |
280 | class TestBootstrap( |
281 | helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
282 | @@ -181,3 +186,82 @@ |
283 | app.get_api_url(self.env_name) |
284 | mock_call.assert_called_once_with( |
285 | 'juju', 'api-endpoints', '-e', self.env_name, '--format', 'json') |
286 | + |
287 | + |
288 | +class TestConnect(ProgramExitTestsMixin, unittest.TestCase): |
289 | + |
290 | + admin_secret = 'Secret!' |
291 | + api_url = 'wss://api.example.com:17070' |
292 | + |
293 | + def test_connection_established(self): |
294 | + # The connection is done and the Environment instance is returned. |
295 | + with mock.patch('quickstart.juju.connect') as mock_connect: |
296 | + env = app.connect(self.api_url, self.admin_secret) |
297 | + mock_connect.assert_called_once_with(self.api_url) |
298 | + mock_env = mock_connect() |
299 | + mock_env.login.assert_called_once_with(self.admin_secret) |
300 | + self.assertEqual(mock_env, env) |
301 | + |
302 | + def test_connection_error(self): |
303 | + # A ProgramExit is raised if an error occurs in the connection. |
304 | + mock_connect = mock.Mock(side_effect=ValueError('bad wolf')) |
305 | + expected = 'unable to connect to the Juju API server on {}: bad wolf' |
306 | + with mock.patch('quickstart.juju.connect', mock_connect): |
307 | + with self.assert_program_exit(expected.format(self.api_url)): |
308 | + app.connect(self.api_url, self.admin_secret) |
309 | + mock_connect.assert_called_once_with(self.api_url) |
310 | + |
311 | + def test_authentication_error(self): |
312 | + # A ProgramExit is raised if an error occurs in the authentication. |
313 | + expected = 'unable to log in to the Juju API server on {}: bad wolf' |
314 | + with mock.patch('quickstart.juju.connect') as mock_connect: |
315 | + mock_login = mock_connect().login |
316 | + mock_login.side_effect = self.make_env_error('bad wolf') |
317 | + with self.assert_program_exit(expected.format(self.api_url)): |
318 | + app.connect(self.api_url, self.admin_secret) |
319 | + mock_connect.assert_called_with(self.api_url) |
320 | + mock_login.assert_called_once_with(self.admin_secret) |
321 | + |
322 | + def test_other_errors(self): |
323 | + # Any other errors occurred during the log in process are not trapped. |
324 | + error = ValueError('explode!') |
325 | + with mock.patch('quickstart.juju.connect') as mock_connect: |
326 | + mock_login = mock_connect().login |
327 | + mock_login.side_effect = error |
328 | + with self.assertRaises(ValueError) as context_manager: |
329 | + app.connect(self.api_url, self.admin_secret) |
330 | + self.assertIs(error, context_manager.exception) |
331 | + |
332 | + |
333 | +class TestDeployGui(ProgramExitTestsMixin, unittest.TestCase): |
334 | + |
335 | + def test_deployment(self): |
336 | + # The function correctly deploys and exposes the service. |
337 | + env = mock.Mock() |
338 | + app.deploy_gui(env, 'my-gui') |
339 | + env.assert_has_calls([ |
340 | + mock.call.deploy('my-gui', 'cs:precise/juju-gui-77', to=0), |
341 | + mock.call.expose('my-gui') |
342 | + ]) |
343 | + |
344 | + def test_api_error(self): |
345 | + # A ProgramExit is raised if an error occurs in one of the API calls. |
346 | + env = mock.Mock() |
347 | + env.deploy.side_effect = self.make_env_error('service already exists') |
348 | + expected = 'bad API server response: service already exists' |
349 | + with self.assert_program_exit(expected): |
350 | + app.deploy_gui(env, 'another-gui') |
351 | + env.deploy.assert_called_once_with( |
352 | + 'another-gui', 'cs:precise/juju-gui-77', to=0) |
353 | + |
354 | + def test_other_errors(self): |
355 | + # Any other errors occurred during the process are not trapped. |
356 | + error = ValueError('explode!') |
357 | + env = mock.Mock() |
358 | + env.expose.side_effect = error |
359 | + with self.assertRaises(ValueError) as context_manager: |
360 | + app.deploy_gui(env, 'juju-gui') |
361 | + env.deploy.assert_called_once_with( |
362 | + 'juju-gui', 'cs:precise/juju-gui-77', to=0) |
363 | + env.expose.assert_called_once_with('juju-gui') |
364 | + self.assertIs(error, context_manager.exception) |
365 | |
366 | === added file 'quickstart/tests/test_juju.py' |
367 | --- quickstart/tests/test_juju.py 1970-01-01 00:00:00 +0000 |
368 | +++ quickstart/tests/test_juju.py 2013-10-17 16:57:02 +0000 |
369 | @@ -0,0 +1,210 @@ |
370 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
371 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
372 | +# Copyright (C) 2013 Canonical Ltd. |
373 | +# |
374 | +# This program is free software: you can redistribute it and/or modify it under |
375 | +# the terms of the GNU Affero General Public License version 3, as published by |
376 | +# the Free Software Foundation. |
377 | +# |
378 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
379 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
380 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
381 | +# Affero General Public License for more details. |
382 | +# |
383 | +# You should have received a copy of the GNU Affero General Public License |
384 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
385 | + |
386 | +"""Tests for the Juju Quickstart API client.""" |
387 | + |
388 | +import unittest |
389 | + |
390 | +import mock |
391 | +import websocket |
392 | + |
393 | +from quickstart import juju |
394 | +from quickstart.tests import helpers |
395 | + |
396 | + |
397 | +class TestConnect(unittest.TestCase): |
398 | + |
399 | + api_url = 'wss://api.example.com:17070' |
400 | + |
401 | + @mock.patch('quickstart.juju.WebSocketConnection') |
402 | + def test_environment_connection(self, mock_conn): |
403 | + # A connected Environment instance is correctly returned. |
404 | + env = juju.connect(self.api_url) |
405 | + mock_conn.assert_called_once_with() |
406 | + conn = mock_conn() |
407 | + conn.assert_has_calls([ |
408 | + mock.call.settimeout(websocket.default_timeout), |
409 | + mock.call.connect(self.api_url, origin=self.api_url) |
410 | + ]) |
411 | + self.assertIsInstance(env, juju.Environment) |
412 | + self.assertEqual(self.api_url, env.endpoint) |
413 | + self.assertEqual(conn, env.conn) |
414 | + |
415 | + |
416 | +class TestEnvironment(unittest.TestCase): |
417 | + |
418 | + api_url = 'wss://api.example.com:17070' |
419 | + charm_url = 'cs:precise/juju-gui-77' |
420 | + service_name = 'juju-gui' |
421 | + |
422 | + def setUp(self): |
423 | + # Set up an Environment instance. |
424 | + api_url = self.api_url |
425 | + with mock.patch('websocket.create_connection') as mock_connect: |
426 | + self.env = juju.Environment(api_url) |
427 | + mock_connect.assert_called_once_with(api_url, origin=api_url) |
428 | + # Keep track of watcher changes in the changesets list. |
429 | + self.changesets = [] |
430 | + |
431 | + def make_deploy_request(self, **kwargs): |
432 | + """Create and return a deploy request. |
433 | + |
434 | + Use kwargs to add or override request parameters. |
435 | + """ |
436 | + params = { |
437 | + 'ServiceName': self.service_name, |
438 | + 'CharmURL': self.charm_url, |
439 | + 'NumUnits': 1, |
440 | + 'Config': {}, |
441 | + 'Constraints': {}, |
442 | + } |
443 | + params.update(kwargs) |
444 | + return { |
445 | + 'Type': 'Client', |
446 | + 'Request': 'ServiceDeploy', |
447 | + 'Params': params, |
448 | + } |
449 | + |
450 | + def processor(self, changeset): |
451 | + self.changesets.append(changeset) |
452 | + return changeset |
453 | + |
454 | + @mock.patch('quickstart.juju.Environment._rpc') |
455 | + def test_deploy(self, mock_rpc): |
456 | + # The deploy API call is properly generated. |
457 | + self.env.deploy(self.service_name, self.charm_url) |
458 | + mock_rpc.assert_called_once_with(self.make_deploy_request()) |
459 | + |
460 | + @mock.patch('quickstart.juju.Environment._rpc') |
461 | + def test_deploy_config(self, mock_rpc): |
462 | + # The deploy API call is properly generated when passing settings. |
463 | + self.env.deploy( |
464 | + self.service_name, self.charm_url, |
465 | + config={'key1': 'value1', 'key2': 42}) |
466 | + expected = self.make_deploy_request( |
467 | + Config={'key1': 'value1', 'key2': '42'}) |
468 | + mock_rpc.assert_called_once_with(expected) |
469 | + |
470 | + @mock.patch('quickstart.juju.Environment._rpc') |
471 | + def test_deploy_constraints(self, mock_rpc): |
472 | + # The deploy API call is properly generated when passing constraints. |
473 | + constraints = {'cpu-cores': 8, 'mem': 16} |
474 | + self.env.deploy( |
475 | + self.service_name, self.charm_url, constraints=constraints) |
476 | + expected = self.make_deploy_request(Constraints=constraints) |
477 | + mock_rpc.assert_called_once_with(expected) |
478 | + |
479 | + @mock.patch('quickstart.juju.Environment._rpc') |
480 | + def test_deploy_to(self, mock_rpc): |
481 | + # The deploy API call is properly generated when passing a machine |
482 | + # specification. |
483 | + self.env.deploy(self.service_name, self.charm_url, to=0) |
484 | + expected = self.make_deploy_request(ToMachineSpec='0') |
485 | + mock_rpc.assert_called_once_with(expected) |
486 | + |
487 | + @mock.patch('quickstart.juju.jujuclient.Watcher._rpc') |
488 | + def test_watch_changes(self, mock_rpc): |
489 | + # It is possible to watch for changes using a processor callable. |
490 | + changeset1 = ['change1', 'change2'] |
491 | + changeset2 = ['change3'] |
492 | + mock_rpc.side_effect = [ |
493 | + # Define the response to the watcher's start call. |
494 | + {'Response': {}, 'AllWatcherId': 42}, |
495 | + # Define two responses to the two subsequent next calls. |
496 | + {'Response': {}, 'Deltas': changeset1}, |
497 | + {'Response': {}, 'Deltas': changeset2}, |
498 | + # Define the response to the watcher's stop call. |
499 | + {'Response': {}}, |
500 | + ] |
501 | + watcher = self.env.watch_changes(self.processor) |
502 | + # The first set of changes is correctly returned. |
503 | + changeset = watcher.next() |
504 | + self.assertEqual(changeset1, changeset) |
505 | + # The second set of changes is correctly returned. |
506 | + changeset = watcher.next() |
507 | + self.assertEqual(changeset2, changeset) |
508 | + # All the changes have been processed. |
509 | + self.assertEqual([changeset1, changeset2], self.changesets) |
510 | + # Ensure the API has been used properly. |
511 | + mock_rpc.assert_has_calls([ |
512 | + mock.call({'Type': 'Client', 'Request': 'WatchAll', 'Params': {}}), |
513 | + mock.call({'Type': 'AllWatcher', 'Request': 'Next', 'Id': 42}), |
514 | + mock.call({'Type': 'AllWatcher', 'Request': 'Next', 'Id': 42}), |
515 | + ]) |
516 | + |
517 | + @mock.patch('quickstart.juju.jujuclient.Watcher._rpc') |
518 | + def test_watch_closed(self, mock_rpc): |
519 | + # A stop API call on the AllWatcher is performed when the watcher is |
520 | + # garbage collected. |
521 | + mock_rpc.side_effect = [ |
522 | + # Define the response to the watcher's start call. |
523 | + {'Response': {}, 'AllWatcherId': 42}, |
524 | + # Define a response to a next call. |
525 | + {'Response': {}, 'Deltas': ['change1', 'change2']}, |
526 | + # Define the response to the watcher's stop call. |
527 | + {'Response': {}}, |
528 | + ] |
529 | + watcher = self.env.watch_changes(self.processor) |
530 | + # The first set of changes is correctly returned. |
531 | + watcher.next() |
532 | + del watcher |
533 | + # Ensure the API has been used properly. |
534 | + mock_rpc.assert_has_calls([ |
535 | + mock.call({'Type': 'Client', 'Request': 'WatchAll', 'Params': {}}), |
536 | + mock.call({'Type': 'AllWatcher', 'Request': 'Next', 'Id': 42}), |
537 | + mock.call({'Type': 'AllWatcher', 'Request': 'Stop', 'Id': 42}), |
538 | + ]) |
539 | + |
540 | + |
541 | +class TestWebSocketConnection(unittest.TestCase): |
542 | + |
543 | + snowman = u'Here is a snowman\u00a1: \u2603' |
544 | + |
545 | + def setUp(self): |
546 | + with mock.patch('socket.socket') as mock_socket: |
547 | + self.conn = juju.WebSocketConnection() |
548 | + # Patch the socket.send() function used by the send method. |
549 | + self.mock_send = mock_socket().send |
550 | + # The recv method calls the recv_data one. |
551 | + self.conn.recv_data = self.mock_recv = mock.Mock() |
552 | + |
553 | + def test_send(self): |
554 | + # Outgoing messages are properly logged. |
555 | + with helpers.assert_logs(['API message: --> my message'], 'debug'): |
556 | + self.conn.send('my message') |
557 | + self.assertTrue(self.mock_send.called) |
558 | + |
559 | + def test_send_unicode(self): |
560 | + # Outgoing unicode messages are properly logged. |
561 | + expected = 'API message: --> {}'.format(self.snowman.encode('utf-8')) |
562 | + with helpers.assert_logs([expected], 'debug'): |
563 | + self.conn.send(self.snowman) |
564 | + self.assertTrue(self.mock_send.called) |
565 | + |
566 | + def test_recv(self): |
567 | + # Incoming messages are properly logged. |
568 | + self.mock_recv.return_value = (42, 'my message') |
569 | + with helpers.assert_logs(['API message: <-- my message'], 'debug'): |
570 | + self.conn.recv() |
571 | + self.mock_recv.assert_called_once_with() |
572 | + |
573 | + def test_recv_unicode(self): |
574 | + # Incoming unicode messages are properly logged. |
575 | + self.mock_recv.return_value = (42, self.snowman) |
576 | + expected = 'API message: <-- {}'.format(self.snowman.encode('utf-8')) |
577 | + with helpers.assert_logs([expected], 'debug'): |
578 | + self.conn.recv() |
579 | + self.mock_recv.assert_called_once_with() |
580 | |
581 | === modified file 'quickstart/tests/test_utils.py' |
582 | --- quickstart/tests/test_utils.py 2013-10-16 12:12:31 +0000 |
583 | +++ quickstart/tests/test_utils.py 2013-10-17 16:57:02 +0000 |
584 | @@ -216,3 +216,23 @@ |
585 | }) |
586 | agent_state = utils.parse_status_output(output) |
587 | self.assertEqual('started', agent_state) |
588 | + |
589 | + |
590 | +class TestUtf8(unittest.TestCase): |
591 | + |
592 | + def test_unicode(self): |
593 | + # A unicode value is correctly converted. |
594 | + value = utils.utf8(u'foo') |
595 | + self.assertIsInstance(value, str) |
596 | + self.assertEqual('foo', value) |
597 | + |
598 | + def test_bytes(self): |
599 | + # A bytes value is left untouched. |
600 | + original = 'foo' |
601 | + value = utils.utf8(original) |
602 | + self.assertIsInstance(value, str) |
603 | + self.assertIs(original, value) |
604 | + |
605 | + def test_none(self): |
606 | + # The None value is returned as is. |
607 | + self.assertIsNone(utils.utf8(None)) |
608 | |
609 | === modified file 'quickstart/utils.py' |
610 | --- quickstart/utils.py 2013-10-16 12:12:31 +0000 |
611 | +++ quickstart/utils.py 2013-10-17 16:57:02 +0000 |
612 | @@ -143,3 +143,13 @@ |
613 | if state is None: |
614 | raise ValueError('agent state not found in {}'.format(status)) |
615 | return state |
616 | + |
617 | + |
618 | +def utf8(value): |
619 | + """Return the utf8 encoded version of the given value. |
620 | + |
621 | + The given value is returned as is if already encoded or not a string. |
622 | + """ |
623 | + if isinstance(value, unicode): |
624 | + return value.encode('utf-8') |
625 | + return value |
Reviewers: mp+191679_ code.launchpad. net,
Message:
Please take a look.
Description:
Deploy and expose the GUI.
Use a customized jujuclient. Environment
object in order to be able deploy to a
specific machine and to log traffic.
Tests: make check
QA: make run -> no errors and a resulting
juju environment with the GUI exposed
and deploying in the bootstrap node.
https:/ /code.launchpad .net/~frankban/ juju-quickstart /quickstart- deploy/ +merge/ 191679
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/14764044/
Affected files (+499, -17 lines): manage. py tests/helpers. py tests/test_ app.py tests/test_ juju.py tests/test_ utils.py
M .lbox
M Makefile
A [revision details]
M quickstart/app.py
A quickstart/juju.py
M quickstart/
M quickstart/
M quickstart/
A quickstart/
M quickstart/
M quickstart/utils.py