Merge lp:~frankban/juju-gui/quickstart-real-bootstrap into lp:juju-gui/juju-quickstart
- quickstart-real-bootstrap
- Merge into juju-quickstart
Status: | Merged |
---|---|
Merged at revision: | 4 |
Proposed branch: | lp:~frankban/juju-gui/quickstart-real-bootstrap |
Merge into: | lp:juju-gui/juju-quickstart |
Diff against target: |
584 lines (+365/-33) 7 files modified
quickstart/app.py (+39/-5) quickstart/manage.py (+28/-2) quickstart/tests/helpers.py (+40/-12) quickstart/tests/test_app.py (+155/-0) quickstart/tests/test_manage.py (+17/-3) quickstart/tests/test_utils.py (+47/-7) quickstart/utils.py (+39/-4) |
To merge this branch: | bzr merge lp:~frankban/juju-gui/quickstart-real-bootstrap |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+191410@code.launchpad.net |
Commit message
Description of the change
Bootstrap the Juju environment.
Wait until the API server is ready.
Retrieve the API address.
Also added --debug support (logging).
Tests: `make check`.
QA: assuming you have a local provider env
named 'local' and an ec2 one named 'ec2'
- run `.venv/bin/python juju-quickstart -e local`
-> error: the local provider is not currently supported;
- run `.venv/bin/python juju-quickstart -e no-such-env`
-> error: environment no-such-env not found in ...;
- run `juju switch ec2`;
- run `.venv/bin/python juju-quickstart --debug`
-> the debug messages are shown, the env is bootstrapped,
the API URL is printed at the end of the process.
- run `.venv/bin/python juju-quickstart`
-? like above but this time no debug messages are shown.
Francesco Banconi (frankban) wrote : | # |
Brad Crittenden (bac) wrote : | # |
LGTM Francesco. I have not yet done QA but can if needed.
https:/
File quickstart/
https:/
quickstart/
A comment here explaining what you're doing would be welcome.
- 15. By Francesco Banconi
-
Changes as per review.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
https:/
File quickstart/
https:/
quickstart/
On 2013/10/16 14:51:20, bac wrote:
> A comment here explaining what you're doing would be welcome.
Done.
Gary Poster (gary) wrote : | # |
LGTM with an extremely small suggestion that you are welcome to ignore.
It is interesting and convenient that so many of the commands have
(seemingly) reliably parsable output. The branch has nice and
interesting tests. Thank you!
https:/
File quickstart/app.py (right):
https:/
quickstart/
'status', '-e', env_name)
I'm OK with this for expediency, but wouldn't the API (Kapil's library)
be better in a revision?
[later] Oh, it's YAML, yeah? I guess that's OK. I think it would be
nice to add a comment to that effect, but others might disagree. Just a
suggestion.
https:/
quickstart/
'--format', 'json')
yay, --format json
https:/
File quickstart/utils.py (right):
https:/
quickstart/
to avoid the fragility
oh, cool.
Brad Crittenden (bac) wrote : | # |
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Bootstrap the Juju environment.
Wait until the API server is ready.
Retrieve the API address.
Also added --debug support (logging).
Tests: `make check`.
QA: assuming you have a local provider env
named 'local' and an ec2 one named 'ec2'
- run `.venv/bin/python juju-quickstart -e local`
-> error: the local provider is not currently supported;
- run `.venv/bin/python juju-quickstart -e no-such-env`
-> error: environment no-such-env not found in ...;
- run `juju switch ec2`;
- run `.venv/bin/python juju-quickstart --debug`
-> the debug messages are shown, the env is bootstrapped,
the API URL is printed at the end of the process.
- run `.venv/bin/python juju-quickstart`
-? like above but this time no debug messages are shown.
R=bac, gary.poster
CC=
https:/
https:/
File quickstart/app.py (right):
https:/
quickstart/
'status', '-e', env_name)
On 2013/10/16 15:43:40, gary.poster wrote:
> I'm OK with this for expediency, but wouldn't the API (Kapil's
library) be
> better in a revision?
> [later] Oh, it's YAML, yeah? I guess that's OK. I think it would be
nice to
> add a comment to that effect, but others might disagree. Just a
suggestion.
Good suggestion. As discussed, I will explicitly add "--format yaml"
here.
Francesco Banconi (frankban) wrote : | # |
Thank you both!
Preview Diff
1 | === modified file 'quickstart/app.py' |
2 | --- quickstart/app.py 2013-10-15 15:31:12 +0000 |
3 | +++ quickstart/app.py 2013-10-16 15:23:20 +0000 |
4 | @@ -16,6 +16,10 @@ |
5 | |
6 | """Juju Quickstart base application functions.""" |
7 | |
8 | +import json |
9 | + |
10 | +from quickstart import utils |
11 | + |
12 | |
13 | class ProgramExit(Exception): |
14 | """An error occurred while setting up the Juju environment. |
15 | @@ -34,9 +38,39 @@ |
16 | def bootstrap(env_name): |
17 | """Bootstrap the Juju environment with the given name. |
18 | |
19 | - Return the environment API address (e.g. "api.example.com:17070"). |
20 | + Return when the bootstrap node is ready. |
21 | Raise a ProgramExit if any error occurs in the bootstrap process. |
22 | - Otherwise return when the environment is bootstrapped and the API server |
23 | - is ready to accept connections. |
24 | - """ |
25 | - # TODO: everything! |
26 | + """ |
27 | + retcode, _, error = utils.call('juju', 'bootstrap', '-e', env_name) |
28 | + if retcode: |
29 | + raise ProgramExit(error) |
30 | + # Call "juju status" multiple times until the bootstrap node is ready. |
31 | + for _ in range(5): |
32 | + retcode, output, error = utils.call('juju', 'status', '-e', env_name) |
33 | + if retcode: |
34 | + continue |
35 | + # Ensure the state server is up and the agent is started. |
36 | + try: |
37 | + agent_state = utils.parse_status_output(output) |
38 | + except ValueError: |
39 | + continue |
40 | + if agent_state == 'started': |
41 | + return |
42 | + details = ''.join(filter(None, [output, error])).strip() |
43 | + raise ProgramExit('the state server is not ready:\n{}'.format(details)) |
44 | + |
45 | + |
46 | +def get_api_url(env_name): |
47 | + """Return a Juju API URL for the given environment name. |
48 | + |
49 | + Use the Juju CLI in a subprocess in order to retrieve the API addresses. |
50 | + Return the complete URL, e.g. "wss://api.example.com:17070". |
51 | + """ |
52 | + retcode, output, error = utils.call( |
53 | + 'juju', 'api-endpoints', '-e', env_name, '--format', 'json') |
54 | + if retcode: |
55 | + raise ProgramExit(error) |
56 | + # Assuming there is always at least one API address, grab the first one |
57 | + # from the JSON output. |
58 | + api_address = json.loads(output)[0] |
59 | + return 'wss://{}'.format(api_address) |
60 | |
61 | === modified file 'quickstart/manage.py' |
62 | --- quickstart/manage.py 2013-10-15 15:40:31 +0000 |
63 | +++ quickstart/manage.py 2013-10-16 15:23:20 +0000 |
64 | @@ -18,6 +18,7 @@ |
65 | |
66 | from __future__ import print_function |
67 | import argparse |
68 | +import logging |
69 | import os |
70 | |
71 | import quickstart |
72 | @@ -54,7 +55,8 @@ |
73 | 'It is possible to specify the environment name by either:\n' |
74 | ' - passing the -e or --environment argument;\n' |
75 | ' - setting the JUJU_ENV environment variable;\n' |
76 | - ' - using "juju switch" to select the default environment to use.' |
77 | + ' - using "juju switch" to select the default environment;\n' |
78 | + ' - setting the default environment in {}.'.format(env_file) |
79 | ) |
80 | # Validate the environment file. |
81 | try: |
82 | @@ -70,8 +72,25 @@ |
83 | options.env_type = env_type |
84 | |
85 | |
86 | +def _configure_logging(level): |
87 | + """Set up the application logging.""" |
88 | + root = logging.getLogger() |
89 | + # Remove any previous handler on the root logger. |
90 | + for handler in root.handlers[:]: |
91 | + root.removeHandler(handler) |
92 | + logging.basicConfig( |
93 | + level=level, |
94 | + format=( |
95 | + '%(asctime)s %(levelname)s ' |
96 | + '%(module)s@%(funcName)s:%(lineno)d ' |
97 | + '%(message)s' |
98 | + ), |
99 | + datefmt='%H:%M:%S', |
100 | + ) |
101 | + |
102 | + |
103 | def setup(): |
104 | - """Set up the application options. |
105 | + """Set up the application options and logger. |
106 | |
107 | Return the options as a namespace containing the followin attributes: |
108 | - admin_secret: the password to use to access the Juju API; |
109 | @@ -97,6 +116,8 @@ |
110 | help='the path to the Juju environments YAML file (%(default)s)') |
111 | parser.add_argument( |
112 | '--version', action='version', version='%(prog)s {}'.format(version)) |
113 | + parser.add_argument( |
114 | + '--debug', action='store_true', help='turn debug mode on') |
115 | # This is required by juju-core: see "juju help plugins". |
116 | parser.add_argument( |
117 | '--description', action=_DescriptionAction, default=argparse.SUPPRESS, |
118 | @@ -105,6 +126,8 @@ |
119 | options = parser.parse_args() |
120 | # Validate and process the provided arguments. |
121 | _validate_env(options, parser) |
122 | + # Set up logging. |
123 | + _configure_logging(logging.DEBUG if options.debug else logging.INFO) |
124 | return options |
125 | |
126 | |
127 | @@ -114,3 +137,6 @@ |
128 | print('bootstrapping the {} environment (type: {})'.format( |
129 | options.env_name, options.env_type)) |
130 | app.bootstrap(options.env_name) |
131 | + print('retrieving the Juju API address') |
132 | + api_url = app.get_api_url(options.env_name) |
133 | + print('connecting to {}'.format(api_url)) |
134 | |
135 | === modified file 'quickstart/tests/helpers.py' |
136 | --- quickstart/tests/helpers.py 2013-10-15 18:25:57 +0000 |
137 | +++ quickstart/tests/helpers.py 2013-10-16 15:23:20 +0000 |
138 | @@ -20,21 +20,35 @@ |
139 | import os |
140 | import tempfile |
141 | |
142 | +import mock |
143 | import yaml |
144 | |
145 | |
146 | -class ValueErrorTestsMixin(object): |
147 | - """Set up some base methods for testing functions raising ValueErrors.""" |
148 | - |
149 | - @contextmanager |
150 | - def assert_value_error(self, error): |
151 | - """Ensure a ValueError is raised in the context block. |
152 | - |
153 | - Also check that the exception includes the expected error message. |
154 | - """ |
155 | - with self.assertRaises(ValueError) as context_manager: |
156 | - yield |
157 | - self.assertEqual(error, str(context_manager.exception)) |
158 | +class CallTestsMixin(object): |
159 | + """Easily use the quickstart.utils.call function.""" |
160 | + |
161 | + def patch_call(self, retcode, output='', error=''): |
162 | + """Patch the quickstart.utils.call function.""" |
163 | + mock_call = mock.Mock(return_value=(retcode, output, error)) |
164 | + return mock.patch('quickstart.utils.call', mock_call) |
165 | + |
166 | + def patch_multiple_calls(self, side_effect): |
167 | + """Patch multiple subsequent quickstart.utils.call calls.""" |
168 | + mock_call = mock.Mock(side_effect=side_effect) |
169 | + return mock.patch('quickstart.utils.call', mock_call) |
170 | + |
171 | + |
172 | +@contextmanager |
173 | +def assert_logs(messages, level='debug'): |
174 | + """Ensure the given messages are logged using the given log level. |
175 | + |
176 | + Use this function as a context manager: the code executed in the context |
177 | + block must add the expected log entries. |
178 | + """ |
179 | + with mock.patch('logging.{}'.format(level.lower())) as mock_log: |
180 | + yield |
181 | + expected_calls = [mock.call(message) for message in messages] |
182 | + mock_log.assert_has_calls(expected_calls) |
183 | |
184 | |
185 | class EnvFileTestsMixin(object): |
186 | @@ -58,3 +72,17 @@ |
187 | env_file.write(contents) |
188 | env_file.close() |
189 | return env_file.name |
190 | + |
191 | + |
192 | +class ValueErrorTestsMixin(object): |
193 | + """Set up some base methods for testing functions raising ValueErrors.""" |
194 | + |
195 | + @contextmanager |
196 | + def assert_value_error(self, error): |
197 | + """Ensure a ValueError is raised in the context block. |
198 | + |
199 | + Also check that the exception includes the expected error message. |
200 | + """ |
201 | + with self.assertRaises(ValueError) as context_manager: |
202 | + yield |
203 | + self.assertEqual(error, str(context_manager.exception)) |
204 | |
205 | === modified file 'quickstart/tests/test_app.py' |
206 | --- quickstart/tests/test_app.py 2013-10-15 15:31:32 +0000 |
207 | +++ quickstart/tests/test_app.py 2013-10-16 15:23:20 +0000 |
208 | @@ -16,9 +16,15 @@ |
209 | |
210 | """Tests for the Juju Quickstart base application functions.""" |
211 | |
212 | +from contextlib import contextmanager |
213 | +import json |
214 | import unittest |
215 | |
216 | +import mock |
217 | +import yaml |
218 | + |
219 | from quickstart import app |
220 | +from quickstart.tests import helpers |
221 | |
222 | |
223 | class TestProgramExit(unittest.TestCase): |
224 | @@ -27,3 +33,152 @@ |
225 | # The error is properly represented as a string. |
226 | exception = app.ProgramExit('bad wolf') |
227 | self.assertEqual('juju-quickstart: error: bad wolf', str(exception)) |
228 | + |
229 | + |
230 | +class ProgramExitTestsMixin(object): |
231 | + """Set up some base methods for testing functions raising ProgramExit.""" |
232 | + |
233 | + @contextmanager |
234 | + def assert_program_exit(self, error): |
235 | + """Ensure a ProgramExit is raised in the context block. |
236 | + |
237 | + Also check that the exception includes the expected error message. |
238 | + """ |
239 | + with self.assertRaises(app.ProgramExit) as context_manager: |
240 | + yield |
241 | + expected = 'juju-quickstart: error: {}'.format(error) |
242 | + self.assertEqual(expected, str(context_manager.exception)) |
243 | + |
244 | + |
245 | +class TestBootstrap( |
246 | + helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
247 | + |
248 | + env_name = 'ec2' |
249 | + |
250 | + def make_status_output(self, agent_state): |
251 | + """Create and return a YAML status output.""" |
252 | + return yaml.safe_dump({ |
253 | + 'machines': {'0': {'agent-state': agent_state}}, |
254 | + }) |
255 | + |
256 | + def make_status_calls(self, number): |
257 | + """Return a list containing the given number of status calls.""" |
258 | + return [ |
259 | + mock.call('juju', 'status', '-e', self.env_name) |
260 | + for _ in range(number) |
261 | + ] |
262 | + |
263 | + def assert_status_retried(self, side_effects): |
264 | + """Ensure the "juju status" command is retried several times. |
265 | + |
266 | + Receive the list of side effects the mock status call will return. |
267 | + """ |
268 | + with self.patch_multiple_calls(side_effects) as mock_call: |
269 | + app.bootstrap(self.env_name) |
270 | + mock_call.assert_has_calls([ |
271 | + mock.call('juju', 'bootstrap', '-e', self.env_name), |
272 | + ] + self.make_status_calls(5)) |
273 | + |
274 | + def test_success(self): |
275 | + # The environment is successfully bootstrapped. |
276 | + side_effects = [ |
277 | + (0, '', ''), # Add a bootstrap call. |
278 | + (0, self.make_status_output('started'), ''), # Add a status call. |
279 | + ] |
280 | + with self.patch_multiple_calls(side_effects) as mock_call: |
281 | + app.bootstrap(self.env_name) |
282 | + mock_call.assert_has_calls([ |
283 | + mock.call('juju', 'bootstrap', '-e', self.env_name), |
284 | + ] + self.make_status_calls(1)) |
285 | + |
286 | + def test_bootstrap_failure(self): |
287 | + # A ProgramExit is raised if an error occurs while bootstrapping. |
288 | + with self.patch_call(1, error='bad wolf') as mock_call: |
289 | + with self.assert_program_exit('bad wolf'): |
290 | + app.bootstrap(self.env_name) |
291 | + mock_call.assert_called_once_with( |
292 | + 'juju', 'bootstrap', '-e', self.env_name) |
293 | + |
294 | + def test_status_retry_error(self): |
295 | + # Before raising a ProgramExit, the functions tries to call |
296 | + # "juju status" multiple times if it exits with an error. |
297 | + side_effects = [ |
298 | + (0, '', ''), # Add the bootstrap call. |
299 | + # Add four status calls with a non-zero exit code. |
300 | + (1, '', 'these'), |
301 | + (2, '', 'are'), |
302 | + (3, '', 'the'), |
303 | + (4, '', 'voyages'), |
304 | + # Add a final valid status call. |
305 | + (0, self.make_status_output('started'), ''), |
306 | + ] |
307 | + self.assert_status_retried(side_effects) |
308 | + |
309 | + def test_status_retry_invalid_output(self): |
310 | + # Before raising a ProgramExit, the functions tries to call |
311 | + # "juju status" multiple times if its output is not well formed or if |
312 | + # the agent is not started. |
313 | + side_effects = [ |
314 | + (0, '', ''), # Add the bootstrap call. |
315 | + (0, '', ''), # Add the first status call: no output. |
316 | + (0, ':', ''), # Add the second status call: not YAML. |
317 | + (0, 'just-a-string', ''), # Add the third status call: bad YAML. |
318 | + # Add the fourth status call: the agent is still pending. |
319 | + (0, self.make_status_output('pending'), ''), |
320 | + # Add a final valid status call. |
321 | + (0, self.make_status_output('started'), ''), |
322 | + ] |
323 | + self.assert_status_retried(side_effects) |
324 | + |
325 | + def test_status_retry_both(self): |
326 | + # Before raising a ProgramExit, the functions tries to call |
327 | + # "juju status" multiple times in any case. |
328 | + side_effects = [ |
329 | + (0, '', ''), # Add the bootstrap call. |
330 | + (1, '', 'error'), # Add the first status call: error. |
331 | + (2, '', 'another error'), # Add the second status call: error. |
332 | + # Add the third status call: the agent is still pending. |
333 | + (0, self.make_status_output('pending'), ''), |
334 | + (0, 'just-a-string', ''), # Add the fourth status call: bad YAML. |
335 | + # Add a final valid status call. |
336 | + (0, self.make_status_output('started'), ''), |
337 | + ] |
338 | + self.assert_status_retried(side_effects) |
339 | + |
340 | + def test_status_failure(self): |
341 | + # A ProgramExit is raised if "juju status" keeps failing. |
342 | + status_side_effects = [ |
343 | + (i, 'output #{}\n'.format(i), 'error #{}\n'.format(i)) |
344 | + for i in range(5) |
345 | + ] |
346 | + side_effects = [(0, '', '')] + status_side_effects |
347 | + expected = 'the state server is not ready:\noutput #4\nerror #4' |
348 | + with self.patch_multiple_calls(side_effects) as mock_call: |
349 | + with self.assert_program_exit(expected): |
350 | + app.bootstrap(self.env_name) |
351 | + mock_call.assert_has_calls([ |
352 | + mock.call('juju', 'bootstrap', '-e', self.env_name), |
353 | + ] + self.make_status_calls(5)) |
354 | + |
355 | + |
356 | +class TestGetApiUrl( |
357 | + helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
358 | + |
359 | + env_name = 'ec2' |
360 | + |
361 | + def test_success(self): |
362 | + # The API URL is correctly returned. |
363 | + api_addresses = json.dumps(['api.example.com:17070', 'not-today']) |
364 | + with self.patch_call(0, output=api_addresses) as mock_call: |
365 | + api_url = app.get_api_url(self.env_name) |
366 | + self.assertEqual('wss://api.example.com:17070', api_url) |
367 | + mock_call.assert_called_once_with( |
368 | + 'juju', 'api-endpoints', '-e', self.env_name, '--format', 'json') |
369 | + |
370 | + def test_failure(self): |
371 | + # A ProgramExit is raised if an error occurs retrieving the API URL. |
372 | + with self.patch_call(1, error='bad wolf') as mock_call: |
373 | + with self.assert_program_exit('bad wolf'): |
374 | + app.get_api_url(self.env_name) |
375 | + mock_call.assert_called_once_with( |
376 | + 'juju', 'api-endpoints', '-e', self.env_name, '--format', 'json') |
377 | |
378 | === modified file 'quickstart/tests/test_manage.py' |
379 | --- quickstart/tests/test_manage.py 2013-10-15 18:25:57 +0000 |
380 | +++ quickstart/tests/test_manage.py 2013-10-16 15:23:20 +0000 |
381 | @@ -17,6 +17,7 @@ |
382 | """Tests for the Juju Quickstart management infrastructure.""" |
383 | |
384 | import argparse |
385 | +import logging |
386 | import os |
387 | import unittest |
388 | |
389 | @@ -122,16 +123,17 @@ |
390 | path = 'quickstart.manage.utils.get_default_env_name' |
391 | return mock.patch(path, mock_get_default_env_name) |
392 | |
393 | - def call_setup(self, args, env_name=None): |
394 | + def call_setup(self, args, env_name=None, exit_called=True): |
395 | """Call the setup function simulating the given args and env name. |
396 | |
397 | - Also ensure the program exits without errors. |
398 | + Also ensure the program exits without errors if exit_called is True. |
399 | """ |
400 | with mock.patch('sys.argv', ['juju-quickstart'] + args): |
401 | with mock.patch('sys.exit') as mock_exit: |
402 | with self.patch_get_default_env_name(env_name): |
403 | manage.setup() |
404 | - mock_exit.assert_called_once_with(0) |
405 | + if exit_called: |
406 | + mock_exit.assert_called_once_with(0) |
407 | |
408 | def test_help(self): |
409 | # The program help message is properly formatted. |
410 | @@ -170,3 +172,15 @@ |
411 | self.call_setup(['--version']) |
412 | expected = 'juju-quickstart {}\n'.format(quickstart.get_version()) |
413 | mock_stderr.write.assert_called_once_with(expected) |
414 | + |
415 | + def test_configure_logging(self): |
416 | + # Logging is properly set up at the info level. |
417 | + logger = logging.getLogger() |
418 | + self.call_setup([], 'ec2', exit_called=False) |
419 | + self.assertEqual(logging.INFO, logger.level) |
420 | + |
421 | + def test_configure_logging_debug(self): |
422 | + # Logging is properly set up at the debug level. |
423 | + logger = logging.getLogger() |
424 | + self.call_setup(['--debug'], 'ec2', exit_called=False) |
425 | + self.assertEqual(logging.DEBUG, logger.level) |
426 | |
427 | === modified file 'quickstart/tests/test_utils.py' |
428 | --- quickstart/tests/test_utils.py 2013-10-15 18:25:57 +0000 |
429 | +++ quickstart/tests/test_utils.py 2013-10-16 15:23:20 +0000 |
430 | @@ -61,13 +61,17 @@ |
431 | 'no-such-command: [Errno 2] No such file or directory', |
432 | error) |
433 | |
434 | - |
435 | -class TestGetDefaultEnvName(unittest.TestCase): |
436 | - |
437 | - def patch_call(self, retcode, output='', error=''): |
438 | - """Patch the quickstart.utils.call function.""" |
439 | - mock_call = mock.Mock(return_value=(retcode, output, error)) |
440 | - return mock.patch('quickstart.utils.call', mock_call) |
441 | + def test_logging(self): |
442 | + # The command line call and the results are properly logged. |
443 | + expected_messages = ( |
444 | + "running the following: echo 'we are the borg!'", |
445 | + r"retcode: 0 | output: 'we are the borg!\n' | error: ''", |
446 | + ) |
447 | + with helpers.assert_logs(expected_messages): |
448 | + utils.call('echo', 'we are the borg!') |
449 | + |
450 | + |
451 | +class TestGetDefaultEnvName(helpers.CallTestsMixin, unittest.TestCase): |
452 | |
453 | def test_environment_variable(self): |
454 | # The environment name is successfully returned if JUJU_ENV is set. |
455 | @@ -176,3 +180,39 @@ |
456 | env_type, admin_secret = utils.parse_env_file(env_file, 'aws') |
457 | self.assertEqual('ec2', env_type) |
458 | self.assertEqual('Secret!', admin_secret) |
459 | + |
460 | + |
461 | +class TestParseStatusOutput(helpers.ValueErrorTestsMixin, unittest.TestCase): |
462 | + |
463 | + def test_invalid_yaml(self): |
464 | + # A ValueError is raised if the output is not a valid YAML. |
465 | + with self.assertRaises(ValueError) as context_manager: |
466 | + utils.parse_status_output(':') |
467 | + expected = 'unable to parse the output' |
468 | + self.assertIn(expected, str(context_manager.exception)) |
469 | + |
470 | + def test_invalid_yaml_contents(self): |
471 | + # A ValueError is raised if the output is not well formed. |
472 | + with self.assert_value_error('invalid YAML contents: a-string'): |
473 | + utils.parse_status_output('a-string') |
474 | + |
475 | + def test_no_env(self): |
476 | + # A ValueError is raised if the agent-state is not found in the YAML. |
477 | + data = { |
478 | + 'machines': { |
479 | + '0': {'agent-version': '1.17.0.1'}, |
480 | + }, |
481 | + } |
482 | + expected = 'agent state not found in {}'.format(str(data)) |
483 | + with self.assert_value_error(expected): |
484 | + utils.parse_status_output(yaml.safe_dump(data)) |
485 | + |
486 | + def test_success(self): |
487 | + # The agent state is correctly returned. |
488 | + output = yaml.safe_dump({ |
489 | + 'machines': { |
490 | + '0': {'agent-version': '1.17.0.1', 'agent-state': 'started'}, |
491 | + }, |
492 | + }) |
493 | + agent_state = utils.parse_status_output(output) |
494 | + self.assertEqual('started', agent_state) |
495 | |
496 | === modified file 'quickstart/utils.py' |
497 | --- quickstart/utils.py 2013-10-15 15:47:31 +0000 |
498 | +++ quickstart/utils.py 2013-10-16 15:23:20 +0000 |
499 | @@ -18,6 +18,8 @@ |
500 | |
501 | import re |
502 | import os |
503 | +import logging |
504 | +import pipes |
505 | import subprocess |
506 | |
507 | import yaml |
508 | @@ -34,6 +36,8 @@ |
509 | Return a tuple containing the subprocess return code, output and error. |
510 | """ |
511 | pipe = subprocess.PIPE |
512 | + cmdline = ' '.join(map(pipes.quote, args)) |
513 | + logging.debug('running the following: {}'.format(cmdline)) |
514 | try: |
515 | process = subprocess.Popen(args, stdout=pipe, stderr=pipe) |
516 | except OSError as err: |
517 | @@ -41,20 +45,32 @@ |
518 | # found in the PATH. |
519 | return 127, '', '{}: {}'.format(args[0], err) |
520 | output, error = process.communicate() |
521 | - return process.poll(), output, error |
522 | + retcode = process.poll() |
523 | + logging.debug('retcode: {} | output: {!r} | error: {!r}'.format( |
524 | + retcode, output, error)) |
525 | + return retcode, output, error |
526 | |
527 | |
528 | def get_default_env_name(): |
529 | """Return the current Juju environment name. |
530 | |
531 | - The environment name can be set either by setting the JUJU_ENV environment |
532 | - variable or by using "juju switch". The former overrides the latter. |
533 | + The environment name can be set either by |
534 | + - setting the JUJU_ENV environment variable; |
535 | + - using "juju switch my-env-name"; |
536 | + - setting the default environment in the environments.yaml file. |
537 | + The former overrides the latter. |
538 | |
539 | Return None if a default environment is not found. |
540 | """ |
541 | env_name = os.getenv('JUJU_ENV', '').strip() |
542 | if env_name: |
543 | return env_name |
544 | + # XXX 2013-10-16 frankban bug=1193244: |
545 | + # Switch to using "juju switch --raw" in order to avoid the fragility |
546 | + # of parsing the command output. |
547 | + # The "juju switch" command parses ~/.juju/current-environment file. If the |
548 | + # environment name is not found there, then it tries to retrieve the name |
549 | + # from the "default" section of the ~/.juju/environments.yaml file. |
550 | retcode, output, _ = call('juju', 'switch') |
551 | if retcode: |
552 | return None |
553 | @@ -64,7 +80,7 @@ |
554 | |
555 | |
556 | def parse_env_file(env_file, env_name): |
557 | - """Parse the provided Juju environment.yaml file. |
558 | + """Parse the provided Juju environments.yaml file. |
559 | |
560 | Return a tuple containing the provider type and the admin secret associated |
561 | with the given environment name. |
562 | @@ -108,3 +124,22 @@ |
563 | msg = '{} admin secret not found in {}'.format(env_name, env_file) |
564 | raise ValueError(msg) |
565 | return env_type, admin_secret |
566 | + |
567 | + |
568 | +def parse_status_output(output): |
569 | + """Parse the output of juju status. |
570 | + |
571 | + Return the agent state. |
572 | + Raise a ValueError if the agent state cannot be retrieved. |
573 | + """ |
574 | + try: |
575 | + status = yaml.safe_load(output) |
576 | + except Exception as err: |
577 | + raise ValueError('unable to parse the output: {}'.format(err)) |
578 | + try: |
579 | + state = status.get('machines', {}).get('0', {}).get('agent-state') |
580 | + except AttributeError as err: |
581 | + raise ValueError('invalid YAML contents: {}'.format(status)) |
582 | + if state is None: |
583 | + raise ValueError('agent state not found in {}'.format(status)) |
584 | + return state |
Reviewers: mp+191410_ code.launchpad. net,
Message:
Please take a look.
Description:
Bootstrap the Juju environment.
Wait until the API server is ready.
Retrieve the API address.
Also added --debug support (logging).
Tests: `make check`.
QA: assuming you have a local provider env
named 'local' and an ec2 one named 'ec2'
- run `.venv/bin/python juju-quickstart -e local`
-> error: the local provider is not currently supported;
- run `.venv/bin/python juju-quickstart -e no-such-env`
-> error: environment no-such-env not found in ...;
- run `juju switch ec2`;
- run `.venv/bin/python juju-quickstart --debug`
-> the debug messages are shown, the env is bootstrapped,
the API URL is printed at the end of the process.
- run `.venv/bin/python juju-quickstart`
-? like above but this time no debug messages are shown.
https:/ /code.launchpad .net/~frankban/ juju-gui/ quickstart- real-bootstrap/ +merge/ 191410
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/14441074/
Affected files (+371, -33 lines): manage. py tests/helpers. py tests/test_ app.py tests/test_ manage. py tests/test_ utils.py
A [revision details]
M quickstart/app.py
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/utils.py