Merge lp:~frankban/juju-gui/quickstart-bootstrap into lp:juju-gui/juju-quickstart
- quickstart-bootstrap
- Merge into juju-quickstart
Status: | Merged |
---|---|
Merged at revision: | 3 |
Proposed branch: | lp:~frankban/juju-gui/quickstart-bootstrap |
Merge into: | lp:juju-gui/juju-quickstart |
Diff against target: |
780 lines (+634/-23) 9 files modified
Makefile (+7/-9) juju-quickstart (+10/-2) quickstart/app.py (+42/-0) quickstart/manage.py (+67/-12) quickstart/tests/helpers.py (+60/-0) quickstart/tests/test_app.py (+29/-0) quickstart/tests/test_manage.py (+131/-0) quickstart/tests/test_utils.py (+178/-0) quickstart/utils.py (+110/-0) |
To merge this branch: | bzr merge lp:~frankban/juju-gui/quickstart-bootstrap |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+191234@code.launchpad.net |
Commit message
Description of the change
Parse and validate the environment file/options.
Check if the given environment name exists,
ensure it does not use the local provider,
ensure it includes an admin-secret,
retrieve the admin-secret.
Tests: make check
Francesco Banconi (frankban) wrote : | # |
Gary Poster (gary) wrote : | # |
LGTM with a few trivials. Thank you.
https:/
File Makefile (right):
https:/
Makefile:20: JUJU_ENV ?= quickstart
So...this is used by make run to override the normal Python logic of the
command. I'm not quite sure of the value, but I'm fine with it.
https:/
File quickstart/
https:/
quickstart/
plugin."""
Trivial: Test helpers
https:/
File quickstart/
https:/
quickstart/
simulate an arbitrary environment name.
trivial: and it is
https:/
File quickstart/
https:/
quickstart/
checking precise non-zero retcode seems a bit risky, even though it
probably is practically fine for something as ancient as ls.
https:/
quickstart/
likewise
https:/
quickstart/
output=
lol
https:/
File quickstart/utils.py (right):
https:/
quickstart/
I wonder which is more fragile: relying on a regex of a stdout reply, or
looking at ~/.juju/
~/.juju/
suppose that's a compelling argument, given that the answer to my
question is not obvious to me.
Madison Scott-Clary (makyo) wrote : | # |
LGTM, thank you!
- 9. By Francesco Banconi
-
Changes as per review.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Parse and validate the environment file/options.
Check if the given environment name exists,
ensure it does not use the local provider,
ensure it includes an admin-secret,
retrieve the admin-secret.
Tests: make check
R=gary.poster, matthew.scott
CC=
https:/
https:/
File Makefile (right):
https:/
Makefile:20: JUJU_ENV ?= quickstart
On 2013/10/15 17:02:11, gary.poster wrote:
> So...this is used by make run to override the normal Python logic of
the
> command. I'm not quite sure of the value, but I'm fine with it.
You are right, since JUJU_ENV and the default env are already handled by
the application, this no longer makes sense. Fixed.
https:/
File quickstart/
https:/
quickstart/
plugin."""
On 2013/10/15 17:02:11, gary.poster wrote:
> Trivial: Test helpers
Done.
https:/
File quickstart/
https:/
quickstart/
simulate an arbitrary environment name.
On 2013/10/15 17:02:11, gary.poster wrote:
> trivial: and it is
Done.
https:/
File quickstart/
https:/
quickstart/
On 2013/10/15 17:02:11, gary.poster wrote:
> checking precise non-zero retcode seems a bit risky, even though it
probably is
> practically fine for something as ancient as ls.
Good suggestion, fixed.
https:/
quickstart/
On 2013/10/15 17:02:11, gary.poster wrote:
> likewise
In this case this is safe because we return 127 ourselves.
https:/
File quickstart/utils.py (right):
https:/
quickstart/
On 2013/10/15 17:02:11, gary.poster wrote:
> I wonder which is more fragile: relying on a regex of a stdout reply,
or looking
> at ~/.juju/
This is
> certainly easier, so I suppose that's a compelling argument, given
that the
> answer to my question is not obvious to me.
As discussed, I will ask juju-core devs about
~/.juju/
--format json" a good idea?).
Francesco Banconi (frankban) wrote : | # |
Thank you Gary and Matthew!
Preview Diff
1 | === modified file 'Makefile' |
2 | --- Makefile 2013-10-14 15:54:00 +0000 |
3 | +++ Makefile 2013-10-16 07:23:37 +0000 |
4 | @@ -15,10 +15,6 @@ |
5 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
6 | |
7 | PYTHON = python |
8 | - |
9 | -# Define the default Juju environment used by "make run". |
10 | -JUJU_ENV ?= local |
11 | - |
12 | SYSDEPS = build-essential python-pip python-virtualenv |
13 | |
14 | VENV = .venv |
15 | @@ -36,7 +32,7 @@ |
16 | |
17 | clean: |
18 | $(PYTHON) setup.py clean |
19 | - rm -rfv build/ dist/ ./juju_quickstart.egg-info MANIFEST |
20 | + rm -rfv build/ dist/ juju_quickstart.egg-info MANIFEST |
21 | rm -rfv $(VENV) |
22 | find . -name '*.pyc' -delete |
23 | find . -name '__pycache__' -type d -delete |
24 | @@ -53,9 +49,10 @@ |
25 | @echo 'make source - Create source package.' |
26 | @echo 'make install - Install on local system.' |
27 | @echo 'make run - Run the application in the development environment.\n' |
28 | - @echo ' An environment named "local" is used by default.' |
29 | - @echo ' Define the JUJU_ENV environment variable to select another' |
30 | - @echo ' environment, e.g.: "make run JUJU_ENV=ec2".' |
31 | + @echo ' If "juju switch" has been used to set a default environment, that' |
32 | + @echo ' environment will be used. It is possible to override the default' |
33 | + @echo ' Juju environment by setting the JUJU_ENV environment variable,' |
34 | + @echo ' e.g.: "make run JUJU_ENV=ec2".' |
35 | @echo 'make clean - Get rid of bytecode files, build and dist dirs, venv.' |
36 | |
37 | install: |
38 | @@ -66,7 +63,7 @@ |
39 | @$(VENV)/bin/flake8 --show-source --exclude=$(VENV) ./quickstart |
40 | |
41 | run: setup |
42 | - $(VENV)/bin/python ./juju-quickstart --environment $(JUJU_ENV) |
43 | + $(VENV)/bin/python ./juju-quickstart |
44 | |
45 | source: |
46 | $(PYTHON) setup.py sdist |
47 | @@ -77,5 +74,6 @@ |
48 | test: setup |
49 | @$(VENV)/bin/nosetests -s --verbosity=2 \ |
50 | --with-coverage --cover-package=quickstart quickstart |
51 | + @rm .coverage |
52 | |
53 | .PHONY: all clean check help install lint run setup source sysdeps test |
54 | |
55 | === modified file 'juju-quickstart' |
56 | --- juju-quickstart 2013-10-14 15:24:29 +0000 |
57 | +++ juju-quickstart 2013-10-16 07:23:37 +0000 |
58 | @@ -18,9 +18,17 @@ |
59 | |
60 | """Juju Quickstart plugin entry point.""" |
61 | |
62 | -from quickstart import manage |
63 | +import sys |
64 | + |
65 | +from quickstart import ( |
66 | + app, |
67 | + manage, |
68 | +) |
69 | |
70 | |
71 | if __name__ == '__main__': |
72 | options = manage.setup() |
73 | - manage.run(options) |
74 | + try: |
75 | + manage.run(options) |
76 | + except app.ProgramExit as err: |
77 | + sys.exit(str(err)) |
78 | |
79 | === added file 'quickstart/app.py' |
80 | --- quickstart/app.py 1970-01-01 00:00:00 +0000 |
81 | +++ quickstart/app.py 2013-10-16 07:23:37 +0000 |
82 | @@ -0,0 +1,42 @@ |
83 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
84 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
85 | +# Copyright (C) 2013 Canonical Ltd. |
86 | +# |
87 | +# This program is free software: you can redistribute it and/or modify it under |
88 | +# the terms of the GNU Affero General Public License version 3, as published by |
89 | +# the Free Software Foundation. |
90 | +# |
91 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
92 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
93 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
94 | +# Affero General Public License for more details. |
95 | +# |
96 | +# You should have received a copy of the GNU Affero General Public License |
97 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
98 | + |
99 | +"""Juju Quickstart base application functions.""" |
100 | + |
101 | + |
102 | +class ProgramExit(Exception): |
103 | + """An error occurred while setting up the Juju environment. |
104 | + |
105 | + Raise this exception if you want the program to exit gracefully printing |
106 | + the error message to stderr. |
107 | + """ |
108 | + |
109 | + def __init__(self, message): |
110 | + self.message = message |
111 | + |
112 | + def __str__(self): |
113 | + return 'juju-quickstart: error: {}'.format(self.message) |
114 | + |
115 | + |
116 | +def bootstrap(env_name): |
117 | + """Bootstrap the Juju environment with the given name. |
118 | + |
119 | + Return the environment API address (e.g. "api.example.com:17070"). |
120 | + Raise a ProgramExit if any error occurs in the bootstrap process. |
121 | + Otherwise return when the environment is bootstrapped and the API server |
122 | + is ready to accept connections. |
123 | + """ |
124 | + # TODO: everything! |
125 | |
126 | === modified file 'quickstart/manage.py' |
127 | --- quickstart/manage.py 2013-10-14 15:24:29 +0000 |
128 | +++ quickstart/manage.py 2013-10-16 07:23:37 +0000 |
129 | @@ -21,9 +21,13 @@ |
130 | import os |
131 | |
132 | import quickstart |
133 | - |
134 | - |
135 | -default_envfile = os.path.expanduser('~/.juju/environments.yaml') |
136 | +from quickstart import ( |
137 | + app, |
138 | + utils, |
139 | +) |
140 | + |
141 | + |
142 | +juju_home = os.getenv('JUJU_HOME', '~/.juju') |
143 | description = 'set up a Juju environment (including the GUI) in very few steps' |
144 | version = quickstart.get_version() |
145 | |
146 | @@ -36,26 +40,77 @@ |
147 | parser.exit() |
148 | |
149 | |
150 | +def _validate_env(options, parser): |
151 | + """Validate and process the provided environment related options. |
152 | + |
153 | + Exit with an error if options are not valid. |
154 | + """ |
155 | + env_file = os.path.abspath(os.path.expanduser(options.env_file)) |
156 | + # Validate the environment name. |
157 | + env_name = options.env_name |
158 | + if env_name is None: |
159 | + return parser.error( |
160 | + 'unable to find an environment name to use\n' |
161 | + 'It is possible to specify the environment name by either:\n' |
162 | + ' - passing the -e or --environment argument;\n' |
163 | + ' - setting the JUJU_ENV environment variable;\n' |
164 | + ' - using "juju switch" to select the default environment to use.' |
165 | + ) |
166 | + # Validate the environment file. |
167 | + try: |
168 | + env_type, admin_secret = utils.parse_env_file(env_file, env_name) |
169 | + except ValueError as err: |
170 | + return parser.error(str(err)) |
171 | + # XXX 2013-10-15 frankban: add support for local providers. |
172 | + if env_type == 'local': |
173 | + return parser.error('the local provider is not currently supported') |
174 | + # Update the options namespace with the new values. |
175 | + options.admin_secret = admin_secret |
176 | + options.env_file = env_file |
177 | + options.env_type = env_type |
178 | + |
179 | + |
180 | def setup(): |
181 | - """Set up the application options.""" |
182 | + """Set up the application options. |
183 | + |
184 | + Return the options as a namespace containing the followin attributes: |
185 | + - admin_secret: the password to use to access the Juju API; |
186 | + - env_file: the absolute path of the Juju environments.yaml file; |
187 | + - env_name: the name of the Juju environment to use; |
188 | + - env_type: the provider type of the selected Juju environment. |
189 | + |
190 | + Exit with an error if the provided arguments are not valid. |
191 | + """ |
192 | + default_env_name = utils.get_default_env_name() |
193 | + # Define the help message for the --environment option. |
194 | + env_help = 'the name of the Juju environment to use' |
195 | + if default_env_name is not None: |
196 | + env_help = '{} (%(default)s)'.format(env_help) |
197 | + # Create and set up the arguments parser. |
198 | parser = argparse.ArgumentParser(description=quickstart.__doc__) |
199 | parser.add_argument( |
200 | - '-e', '--environment', required=True, dest='env_name', |
201 | - help='the name of the Juju environment to use') |
202 | + '-e', '--environment', default=default_env_name, dest='env_name', |
203 | + help=env_help) |
204 | parser.add_argument( |
205 | - '--environments-file', dest='env_file', type=file, |
206 | - default=default_envfile, |
207 | - help='the path to the Juju environments YAML file') |
208 | + '--environments-file', |
209 | + default=os.path.join(juju_home, 'environments.yaml'), dest='env_file', |
210 | + help='the path to the Juju environments YAML file (%(default)s)') |
211 | parser.add_argument( |
212 | '--version', action='version', version='%(prog)s {}'.format(version)) |
213 | # This is required by juju-core: see "juju help plugins". |
214 | parser.add_argument( |
215 | '--description', action=_DescriptionAction, default=argparse.SUPPRESS, |
216 | nargs=0, help="show program's description and exit") |
217 | - return parser.parse_args() |
218 | + # Parse the provided arguments. |
219 | + options = parser.parse_args() |
220 | + # Validate and process the provided arguments. |
221 | + _validate_env(options, parser) |
222 | + return options |
223 | |
224 | |
225 | def run(options): |
226 | - """Run the application""" |
227 | + """Run the application.""" |
228 | print('juju quickstart v{}'.format(version)) |
229 | - # TODO: everything! |
230 | + print('bootstrapping the {} environment (type: {})'.format( |
231 | + options.env_name, options.env_type)) |
232 | + app.bootstrap(options.env_name) |
233 | |
234 | === added file 'quickstart/tests/helpers.py' |
235 | --- quickstart/tests/helpers.py 1970-01-01 00:00:00 +0000 |
236 | +++ quickstart/tests/helpers.py 2013-10-16 07:23:37 +0000 |
237 | @@ -0,0 +1,60 @@ |
238 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
239 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
240 | +# Copyright (C) 2013 Canonical Ltd. |
241 | +# |
242 | +# This program is free software: you can redistribute it and/or modify it under |
243 | +# the terms of the GNU Affero General Public License version 3, as published by |
244 | +# the Free Software Foundation. |
245 | +# |
246 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
247 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
248 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
249 | +# Affero General Public License for more details. |
250 | +# |
251 | +# You should have received a copy of the GNU Affero General Public License |
252 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
253 | + |
254 | +"""Test helpers for the Juju Quickstart plugin.""" |
255 | + |
256 | +from contextlib import contextmanager |
257 | +import os |
258 | +import tempfile |
259 | + |
260 | +import yaml |
261 | + |
262 | + |
263 | +class ValueErrorTestsMixin(object): |
264 | + """Set up some base methods for testing functions raising ValueErrors.""" |
265 | + |
266 | + @contextmanager |
267 | + def assert_value_error(self, error): |
268 | + """Ensure a ValueError is raised in the context block. |
269 | + |
270 | + Also check that the exception includes the expected error message. |
271 | + """ |
272 | + with self.assertRaises(ValueError) as context_manager: |
273 | + yield |
274 | + self.assertEqual(error, str(context_manager.exception)) |
275 | + |
276 | + |
277 | +class EnvFileTestsMixin(object): |
278 | + """Shared methods for testing a Juju environments file.""" |
279 | + |
280 | + valid_contents = yaml.safe_dump({ |
281 | + 'environments': {'aws': {'admin-secret': 'Secret!', 'type': 'ec2'}}, |
282 | + }) |
283 | + |
284 | + def make_env_file(self, contents=None): |
285 | + """Create a Juju environments file containing the given contents. |
286 | + |
287 | + If contents is None, use the valid environment contents defined in |
288 | + self.valid_contents. |
289 | + Return the environments file path. |
290 | + """ |
291 | + if contents is None: |
292 | + contents = self.valid_contents |
293 | + env_file = tempfile.NamedTemporaryFile(delete=False) |
294 | + self.addCleanup(os.remove, env_file.name) |
295 | + env_file.write(contents) |
296 | + env_file.close() |
297 | + return env_file.name |
298 | |
299 | === added file 'quickstart/tests/test_app.py' |
300 | --- quickstart/tests/test_app.py 1970-01-01 00:00:00 +0000 |
301 | +++ quickstart/tests/test_app.py 2013-10-16 07:23:37 +0000 |
302 | @@ -0,0 +1,29 @@ |
303 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
304 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
305 | +# Copyright (C) 2013 Canonical Ltd. |
306 | +# |
307 | +# This program is free software: you can redistribute it and/or modify it under |
308 | +# the terms of the GNU Affero General Public License version 3, as published by |
309 | +# the Free Software Foundation. |
310 | +# |
311 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
312 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
313 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
314 | +# Affero General Public License for more details. |
315 | +# |
316 | +# You should have received a copy of the GNU Affero General Public License |
317 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
318 | + |
319 | +"""Tests for the Juju Quickstart base application functions.""" |
320 | + |
321 | +import unittest |
322 | + |
323 | +from quickstart import app |
324 | + |
325 | + |
326 | +class TestProgramExit(unittest.TestCase): |
327 | + |
328 | + def test_string_representation(self): |
329 | + # The error is properly represented as a string. |
330 | + exception = app.ProgramExit('bad wolf') |
331 | + self.assertEqual('juju-quickstart: error: bad wolf', str(exception)) |
332 | |
333 | === modified file 'quickstart/tests/test_manage.py' |
334 | --- quickstart/tests/test_manage.py 2013-10-14 15:24:29 +0000 |
335 | +++ quickstart/tests/test_manage.py 2013-10-16 07:23:37 +0000 |
336 | @@ -17,11 +17,15 @@ |
337 | """Tests for the Juju Quickstart management infrastructure.""" |
338 | |
339 | import argparse |
340 | +import os |
341 | import unittest |
342 | |
343 | import mock |
344 | +import yaml |
345 | |
346 | +import quickstart |
347 | from quickstart import manage |
348 | +from quickstart.tests import helpers |
349 | |
350 | |
351 | class TestDescriptionAction(unittest.TestCase): |
352 | @@ -39,3 +43,130 @@ |
353 | self.assertIsNone(args.test) |
354 | mock_print.assert_called_once_with(manage.description) |
355 | mock_exit.assert_called_once_with(0) |
356 | + |
357 | + |
358 | +class TestValidateEnv(helpers.EnvFileTestsMixin, unittest.TestCase): |
359 | + |
360 | + def setUp(self): |
361 | + self.parser = mock.Mock() |
362 | + |
363 | + def make_options(self, env_file, env_name=None): |
364 | + """Return a mock options object which includes the passed arguments.""" |
365 | + return mock.Mock(env_file=env_file, env_name=env_name) |
366 | + |
367 | + def test_resulting_options(self): |
368 | + # The options object is correctly set up. |
369 | + env_file = self.make_env_file() |
370 | + options = self.make_options(env_file, env_name='aws') |
371 | + manage._validate_env(options, self.parser) |
372 | + self.assertEqual('Secret!', options.admin_secret) |
373 | + self.assertEqual(env_file, options.env_file) |
374 | + self.assertEqual('aws', options.env_name) |
375 | + self.assertEqual('ec2', options.env_type) |
376 | + |
377 | + def test_expand_user(self): |
378 | + # The ~ construct is correctly expanded in the validation process. |
379 | + env_file = self.make_env_file() |
380 | + # Split the full path of the env file, e.g. from a full "/tmp/env.file" |
381 | + # path retrieve the base path "/tmp" and the file name "env.file". |
382 | + # By doing that we can simulate that the user's home is "/tmp" and that |
383 | + # the env file is "~/env.file". |
384 | + base_path, filename = os.path.split(env_file) |
385 | + path = '~/{}'.format(filename) |
386 | + options = self.make_options(env_file=path, env_name='aws') |
387 | + with mock.patch('os.environ', {'HOME': base_path}): |
388 | + manage._validate_env(options, self.parser) |
389 | + self.assertEqual(env_file, options.env_file) |
390 | + |
391 | + def test_no_env_name(self): |
392 | + # A parser error is invoked if the environment name is missing. |
393 | + options = self.make_options(self.make_env_file()) |
394 | + manage._validate_env(options, self.parser) |
395 | + self.assertTrue(self.parser.error.called) |
396 | + message = self.parser.error.call_args[0][0] |
397 | + self.assertIn('unable to find an environment name to use', message) |
398 | + |
399 | + def test_error_parsing_env_file(self): |
400 | + # A parser error is invoked if an error occurs parsing the env file. |
401 | + env_file = self.make_env_file() |
402 | + options = self.make_options(env_file, env_name='no-such') |
403 | + manage._validate_env(options, self.parser) |
404 | + expected = 'environment no-such not found in {}'.format(env_file) |
405 | + self.parser.error.assert_called_once_with(expected) |
406 | + |
407 | + def test_local_provider(self): |
408 | + # The parser exits with an error if the provided environment name |
409 | + # refers to a local provider type. |
410 | + contents = yaml.safe_dump({ |
411 | + 'environments': { |
412 | + 'lxc': {'admin-secret': 'Secret!', 'type': 'local'}, |
413 | + }, |
414 | + }) |
415 | + env_file = self.make_env_file(contents) |
416 | + options = self.make_options(env_file, env_name='lxc') |
417 | + manage._validate_env(options, self.parser) |
418 | + expected = 'the local provider is not currently supported' |
419 | + self.parser.error.assert_called_once_with(expected) |
420 | + |
421 | + |
422 | +@mock.patch('quickstart.manage._validate_env', mock.Mock()) |
423 | +class TestSetup(unittest.TestCase): |
424 | + |
425 | + def patch_get_default_env_name(self, env_name=None): |
426 | + """Patch the function used by setup() to retrieve the default env name. |
427 | + |
428 | + This way the test does not rely on the user's Juju environment set up, |
429 | + and it is also possible to simulate an arbitrary environment name. |
430 | + """ |
431 | + mock_get_default_env_name = mock.Mock(return_value=env_name) |
432 | + path = 'quickstart.manage.utils.get_default_env_name' |
433 | + return mock.patch(path, mock_get_default_env_name) |
434 | + |
435 | + def call_setup(self, args, env_name=None): |
436 | + """Call the setup function simulating the given args and env name. |
437 | + |
438 | + Also ensure the program exits without errors. |
439 | + """ |
440 | + with mock.patch('sys.argv', ['juju-quickstart'] + args): |
441 | + with mock.patch('sys.exit') as mock_exit: |
442 | + with self.patch_get_default_env_name(env_name): |
443 | + manage.setup() |
444 | + mock_exit.assert_called_once_with(0) |
445 | + |
446 | + def test_help(self): |
447 | + # The program help message is properly formatted. |
448 | + with mock.patch('sys.stdout') as mock_stdout: |
449 | + self.call_setup(['--help']) |
450 | + stdout_write = mock_stdout.write |
451 | + self.assertTrue(stdout_write.called) |
452 | + # Retrieve the output from the mock call. |
453 | + output = stdout_write.call_args[0][0] |
454 | + self.assertIn('usage: juju-quickstart', output) |
455 | + self.assertIn(quickstart.__doc__, output) |
456 | + self.assertIn('--environment', output) |
457 | + # Without a default environment, the -e option has no default. |
458 | + self.assertIn('the name of the Juju environment to use\n', output) |
459 | + |
460 | + def test_help_with_default_environment(self): |
461 | + # The program help message is properly formatted when a default Juju |
462 | + # environment is found. |
463 | + with mock.patch('sys.stdout') as mock_stdout: |
464 | + self.call_setup(['--help'], env_name='hp') |
465 | + stdout_write = mock_stdout.write |
466 | + self.assertTrue(stdout_write.called) |
467 | + # Retrieve the output from the mock call. |
468 | + output = stdout_write.call_args[0][0] |
469 | + self.assertIn('the name of the Juju environment to use (hp)\n', output) |
470 | + |
471 | + def test_description(self): |
472 | + # The program description is properly printed out as required by juju. |
473 | + with mock.patch('__builtin__.print') as mock_print: |
474 | + self.call_setup(['--description']) |
475 | + mock_print.assert_called_once_with(manage.description) |
476 | + |
477 | + def test_version(self): |
478 | + # The program version is properly printed to stderr. |
479 | + with mock.patch('sys.stderr') as mock_stderr: |
480 | + self.call_setup(['--version']) |
481 | + expected = 'juju-quickstart {}\n'.format(quickstart.get_version()) |
482 | + mock_stderr.write.assert_called_once_with(expected) |
483 | |
484 | === added file 'quickstart/tests/test_utils.py' |
485 | --- quickstart/tests/test_utils.py 1970-01-01 00:00:00 +0000 |
486 | +++ quickstart/tests/test_utils.py 2013-10-16 07:23:37 +0000 |
487 | @@ -0,0 +1,178 @@ |
488 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
489 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
490 | +# Copyright (C) 2013 Canonical Ltd. |
491 | +# |
492 | +# This program is free software: you can redistribute it and/or modify it under |
493 | +# the terms of the GNU Affero General Public License version 3, as published by |
494 | +# the Free Software Foundation. |
495 | +# |
496 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
497 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
498 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
499 | +# Affero General Public License for more details. |
500 | +# |
501 | +# You should have received a copy of the GNU Affero General Public License |
502 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
503 | + |
504 | +"""Tests for the Juju Quickstart utility functions and classes.""" |
505 | + |
506 | +import unittest |
507 | + |
508 | +import mock |
509 | +import yaml |
510 | + |
511 | +from quickstart import utils |
512 | +from quickstart.tests import helpers |
513 | + |
514 | + |
515 | +class TestCall(unittest.TestCase): |
516 | + |
517 | + def test_success(self): |
518 | + # A zero exit code and the subprocess output are correctly returned. |
519 | + retcode, output, error = utils.call('echo') |
520 | + self.assertEqual(0, retcode) |
521 | + self.assertEqual('\n', output) |
522 | + self.assertEqual('', error) |
523 | + |
524 | + def test_multiple_arguments(self): |
525 | + # A zero exit code and the subprocess output are correctly returned |
526 | + # when executing a command passing multiple arguments. |
527 | + retcode, output, error = utils.call('echo', 'we are the borg!') |
528 | + self.assertEqual(0, retcode) |
529 | + self.assertEqual('we are the borg!\n', output) |
530 | + self.assertEqual('', error) |
531 | + |
532 | + def test_failure(self): |
533 | + # An error code and the error are returned if the subprocess fails. |
534 | + retcode, output, error = utils.call('ls', 'no-such-file') |
535 | + self.assertNotEqual(0, retcode) |
536 | + self.assertEqual('', output) |
537 | + self.assertEqual( |
538 | + 'ls: cannot access no-such-file: No such file or directory\n', |
539 | + error) |
540 | + |
541 | + def test_invalid_command(self): |
542 | + # An error code and the error are returned if the subprocess fails to |
543 | + # find the provided command in the PATH. |
544 | + retcode, output, error = utils.call('no-such-command') |
545 | + self.assertEqual(127, retcode) |
546 | + self.assertEqual('', output) |
547 | + self.assertEqual( |
548 | + 'no-such-command: [Errno 2] No such file or directory', |
549 | + error) |
550 | + |
551 | + |
552 | +class TestGetDefaultEnvName(unittest.TestCase): |
553 | + |
554 | + def patch_call(self, retcode, output='', error=''): |
555 | + """Patch the quickstart.utils.call function.""" |
556 | + mock_call = mock.Mock(return_value=(retcode, output, error)) |
557 | + return mock.patch('quickstart.utils.call', mock_call) |
558 | + |
559 | + def test_environment_variable(self): |
560 | + # The environment name is successfully returned if JUJU_ENV is set. |
561 | + with mock.patch('os.environ', {'JUJU_ENV': 'ec2'}): |
562 | + env_name = utils.get_default_env_name() |
563 | + self.assertEqual('ec2', env_name) |
564 | + |
565 | + def test_empty_environment_variable(self): |
566 | + # The environment name is not found if JUJU_ENV is empty. |
567 | + with self.patch_call(1): |
568 | + with mock.patch('os.environ', {'JUJU_ENV': ' '}): |
569 | + env_name = utils.get_default_env_name() |
570 | + self.assertIsNone(env_name) |
571 | + |
572 | + def test_no_environment_variable(self): |
573 | + # The environment name is not found if JUJU_ENV is not defined. |
574 | + with self.patch_call(1): |
575 | + with mock.patch('os.environ', {}): |
576 | + env_name = utils.get_default_env_name() |
577 | + self.assertIsNone(env_name) |
578 | + |
579 | + def test_juju_switch(self): |
580 | + # The environment name is successfully returned if previously set using |
581 | + # the "juju switch" command. |
582 | + output = 'Current environment: "hp"\n' |
583 | + with self.patch_call(0, output=output) as mock_call: |
584 | + with mock.patch('os.environ', {}): |
585 | + env_name = utils.get_default_env_name() |
586 | + self.assertEqual('hp', env_name) |
587 | + mock_call.assert_called_once_with('juju', 'switch') |
588 | + |
589 | + def test_juju_switch_failure(self): |
590 | + # The environment name is not found if "juju switch" returns an error. |
591 | + with self.patch_call(1) as mock_call: |
592 | + with mock.patch('os.environ', {}): |
593 | + env_name = utils.get_default_env_name() |
594 | + self.assertIsNone(env_name) |
595 | + mock_call.assert_called_once_with('juju', 'switch') |
596 | + |
597 | + def test_juju_switch_nonsense(self): |
598 | + # The environment name is not found if "juju switch" goes crazy. |
599 | + with self.patch_call(0, output='Exterminate!') as mock_call: |
600 | + with mock.patch('os.environ', {}): |
601 | + env_name = utils.get_default_env_name() |
602 | + self.assertIsNone(env_name) |
603 | + mock_call.assert_called_once_with('juju', 'switch') |
604 | + |
605 | + |
606 | +class TestParseEnvFile( |
607 | + helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin, |
608 | + unittest.TestCase): |
609 | + |
610 | + def test_no_file(self): |
611 | + # A ValueError is raised if the environments file is not found. |
612 | + expected = ( |
613 | + 'unable to open environments file: ' |
614 | + "[Errno 2] No such file or directory: '/no/such/file.yaml'" |
615 | + ) |
616 | + with self.assert_value_error(expected): |
617 | + utils.parse_env_file('/no/such/file.yaml', 'ec2') |
618 | + |
619 | + def test_invalid_yaml(self): |
620 | + # A ValueError is raised if the environments file is not a valid YAML. |
621 | + env_file = self.make_env_file(':') |
622 | + with self.assertRaises(ValueError) as context_manager: |
623 | + utils.parse_env_file(env_file, 'ec2') |
624 | + expected = 'unable to parse environments file {}'.format(env_file) |
625 | + self.assertIn(expected, str(context_manager.exception)) |
626 | + |
627 | + def test_invalid_yaml_contents(self): |
628 | + # A ValueError is raised if the environments file is not well formed. |
629 | + env_file = self.make_env_file('a-string') |
630 | + expected = 'invalid YAML contents in {}: a-string'.format(env_file) |
631 | + with self.assert_value_error(expected): |
632 | + utils.parse_env_file(env_file, 'ec2') |
633 | + |
634 | + def test_no_env(self): |
635 | + # A ValueError is raised if the environment is not found in the YAML. |
636 | + contents = yaml.safe_dump({'environments': {'local': {}}}) |
637 | + env_file = self.make_env_file(contents) |
638 | + expected = 'environment ec2 not found in {}'.format(env_file) |
639 | + with self.assert_value_error(expected): |
640 | + utils.parse_env_file(env_file, 'ec2') |
641 | + |
642 | + def test_no_provider_type(self): |
643 | + # A ValueError is raised if the provider type is not included in the |
644 | + # environment info. |
645 | + contents = yaml.safe_dump({'environments': {'aws': {}}}) |
646 | + env_file = self.make_env_file(contents) |
647 | + expected = 'aws provider type not found in {}'.format(env_file) |
648 | + with self.assert_value_error(expected): |
649 | + utils.parse_env_file(env_file, 'aws') |
650 | + |
651 | + def test_no_admin_secret(self): |
652 | + # A ValueError is raised if the admin secret is not included in the |
653 | + # environment info. |
654 | + contents = yaml.safe_dump({'environments': {'aws': {'type': 'ec2'}}}) |
655 | + env_file = self.make_env_file(contents) |
656 | + expected = 'aws admin secret not found in {}'.format(env_file) |
657 | + with self.assert_value_error(expected): |
658 | + utils.parse_env_file(env_file, 'aws') |
659 | + |
660 | + def test_success(self): |
661 | + # The environment provider type and admin secret are returned. |
662 | + env_file = self.make_env_file() |
663 | + env_type, admin_secret = utils.parse_env_file(env_file, 'aws') |
664 | + self.assertEqual('ec2', env_type) |
665 | + self.assertEqual('Secret!', admin_secret) |
666 | |
667 | === added file 'quickstart/utils.py' |
668 | --- quickstart/utils.py 1970-01-01 00:00:00 +0000 |
669 | +++ quickstart/utils.py 2013-10-16 07:23:37 +0000 |
670 | @@ -0,0 +1,110 @@ |
671 | +# This file is part of the Juju GUI, which lets users view and manage Juju |
672 | +# environments within a graphical interface (https://launchpad.net/juju-gui). |
673 | +# Copyright (C) 2013 Canonical Ltd. |
674 | +# |
675 | +# This program is free software: you can redistribute it and/or modify it under |
676 | +# the terms of the GNU Affero General Public License version 3, as published by |
677 | +# the Free Software Foundation. |
678 | +# |
679 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
680 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
681 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
682 | +# Affero General Public License for more details. |
683 | +# |
684 | +# You should have received a copy of the GNU Affero General Public License |
685 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
686 | + |
687 | +"""Juju Quickstart utility functions and classes.""" |
688 | + |
689 | +import re |
690 | +import os |
691 | +import subprocess |
692 | + |
693 | +import yaml |
694 | + |
695 | +# Compile the regular expression used to parse the "juju switch" output. |
696 | +_juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n') |
697 | + |
698 | + |
699 | +def call(*args): |
700 | + """Call a subprocess passing the given arguments. |
701 | + |
702 | + Take the subcommand and its parameters as args. |
703 | + |
704 | + Return a tuple containing the subprocess return code, output and error. |
705 | + """ |
706 | + pipe = subprocess.PIPE |
707 | + try: |
708 | + process = subprocess.Popen(args, stdout=pipe, stderr=pipe) |
709 | + except OSError as err: |
710 | + # A return code 127 is returned by the shell when the command is not |
711 | + # found in the PATH. |
712 | + return 127, '', '{}: {}'.format(args[0], err) |
713 | + output, error = process.communicate() |
714 | + return process.poll(), output, error |
715 | + |
716 | + |
717 | +def get_default_env_name(): |
718 | + """Return the current Juju environment name. |
719 | + |
720 | + The environment name can be set either by setting the JUJU_ENV environment |
721 | + variable or by using "juju switch". The former overrides the latter. |
722 | + |
723 | + Return None if a default environment is not found. |
724 | + """ |
725 | + env_name = os.getenv('JUJU_ENV', '').strip() |
726 | + if env_name: |
727 | + return env_name |
728 | + retcode, output, _ = call('juju', 'switch') |
729 | + if retcode: |
730 | + return None |
731 | + match = _juju_switch_expression.match(output) |
732 | + if match is not None: |
733 | + return match.groups()[0] |
734 | + |
735 | + |
736 | +def parse_env_file(env_file, env_name): |
737 | + """Parse the provided Juju environment.yaml file. |
738 | + |
739 | + Return a tuple containing the provider type and the admin secret associated |
740 | + with the given environment name. |
741 | + |
742 | + Raise a ValueError if: |
743 | + - the environment file is not found; |
744 | + - the environment file contents are not parsable by YAML; |
745 | + - the YAML contents are not properly structured; |
746 | + - the environment named envname is not found; |
747 | + - the environment does not include the "type" field; |
748 | + - the environment does not include the "admin_secret" field. |
749 | + """ |
750 | + # Load the Juju environments file. |
751 | + try: |
752 | + environments_file = open(env_file) |
753 | + except IOError as err: |
754 | + msg = 'unable to open environments file: {}'.format(err) |
755 | + raise ValueError(msg) |
756 | + # Parse the Juju environments file. |
757 | + try: |
758 | + environments = yaml.safe_load(environments_file) |
759 | + except Exception as err: |
760 | + msg = 'unable to parse environments file {}: {}'.format(env_file, err) |
761 | + raise ValueError(msg) |
762 | + # Retrieve the information about the current environment. |
763 | + try: |
764 | + environment = environments.get('environments', {}).get(env_name) |
765 | + except AttributeError as err: |
766 | + msg = 'invalid YAML contents in {}: {}'.format(env_file, environments) |
767 | + raise ValueError(msg) |
768 | + if environment is None: |
769 | + msg = 'environment {} not found in {}'.format(env_name, env_file) |
770 | + raise ValueError(msg) |
771 | + # Retrieve the provider type and the admin secret. |
772 | + env_type = environment.get('type') |
773 | + if env_type is None: |
774 | + msg = '{} provider type not found in {}'.format(env_name, env_file) |
775 | + raise ValueError(msg) |
776 | + admin_secret = environment.get('admin-secret') |
777 | + if admin_secret is None: |
778 | + msg = '{} admin secret not found in {}'.format(env_name, env_file) |
779 | + raise ValueError(msg) |
780 | + return env_type, admin_secret |
Reviewers: mp+191234_ code.launchpad. net,
Message:
Please take a look.
Description:
Parse and validate the environment file/options.
Check if the given environment name exists,
ensure it does not use the local provider,
ensure it includes an admin-secret,
retrieve the admin-secret.
Tests: make check
https:/ /code.launchpad .net/~frankban/ juju-gui/ quickstart- bootstrap/ +merge/ 191234
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/14669047/
Affected files (+633, -17 lines): manage. py tests/helpers. py tests/test_ app.py tests/test_ manage. py tests/test_ utils.py
M Makefile
A [revision details]
M juju-quickstart
A quickstart/app.py
M quickstart/
A quickstart/
A quickstart/
M quickstart/
A quickstart/
A quickstart/utils.py