Merge lp:~frankban/juju-gui/quickstart-bootstrap into lp:juju-gui/juju-quickstart

Proposed by Francesco Banconi
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
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+191234@code.launchpad.net

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

https://codereview.appspot.com/14669047/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

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):
   M Makefile
   A [revision details]
   M juju-quickstart
   A quickstart/app.py
   M quickstart/manage.py
   A quickstart/tests/helpers.py
   A quickstart/tests/test_app.py
   M quickstart/tests/test_manage.py
   A quickstart/tests/test_utils.py
   A quickstart/utils.py

Revision history for this message
Gary Poster (gary) wrote :

LGTM with a few trivials. Thank you.

https://codereview.appspot.com/14669047/diff/1/Makefile
File Makefile (right):

https://codereview.appspot.com/14669047/diff/1/Makefile#newcode20
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://codereview.appspot.com/14669047/diff/1/quickstart/tests/helpers.py
File quickstart/tests/helpers.py (right):

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/helpers.py#newcode17
quickstart/tests/helpers.py:17: """Tests helpers for the Juju Quickstart
plugin."""
Trivial: Test helpers

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_manage.py
File quickstart/tests/test_manage.py (right):

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_manage.py#newcode119
quickstart/tests/test_manage.py:119: and a it is also possible to
simulate an arbitrary environment name.
trivial: and it is

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_utils.py
File quickstart/tests/test_utils.py (right):

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_utils.py#newcode48
quickstart/tests/test_utils.py:48: self.assertEqual(2, retcode)
checking precise non-zero retcode seems a bit risky, even though it
probably is practically fine for something as ancient as ls.

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_utils.py#newcode58
quickstart/tests/test_utils.py:58: self.assertEqual(127, retcode)
likewise

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_utils.py#newcode112
quickstart/tests/test_utils.py:112: with self.patch_call(0,
output='Exterminate!') as mock_call:
lol

https://codereview.appspot.com/14669047/diff/1/quickstart/utils.py
File quickstart/utils.py (right):

https://codereview.appspot.com/14669047/diff/1/quickstart/utils.py#newcode58
quickstart/utils.py:58: retcode, output, _ = call('juju', 'switch')
I wonder which is more fragile: relying on a regex of a stdout reply, or
looking at ~/.juju/current-environment and
~/.juju/environments.yaml[default]? This is certainly easier, so I
suppose that's a compelling argument, given that the answer to my
question is not obvious to me.

https://codereview.appspot.com/14669047/

Revision history for this message
Madison Scott-Clary (makyo) wrote :
9. By Francesco Banconi

Changes as per review.

Revision history for this message
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://codereview.appspot.com/14669047

https://codereview.appspot.com/14669047/diff/1/Makefile
File Makefile (right):

https://codereview.appspot.com/14669047/diff/1/Makefile#newcode20
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://codereview.appspot.com/14669047/diff/1/quickstart/tests/helpers.py
File quickstart/tests/helpers.py (right):

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/helpers.py#newcode17
quickstart/tests/helpers.py:17: """Tests helpers for the Juju Quickstart
plugin."""
On 2013/10/15 17:02:11, gary.poster wrote:
> Trivial: Test helpers

Done.

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_manage.py
File quickstart/tests/test_manage.py (right):

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_manage.py#newcode119
quickstart/tests/test_manage.py:119: and a it is also possible to
simulate an arbitrary environment name.
On 2013/10/15 17:02:11, gary.poster wrote:
> trivial: and it is

Done.

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_utils.py
File quickstart/tests/test_utils.py (right):

https://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_utils.py#newcode48
quickstart/tests/test_utils.py:48: self.assertEqual(2, retcode)
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://codereview.appspot.com/14669047/diff/1/quickstart/tests/test_utils.py#newcode58
quickstart/tests/test_utils.py:58: self.assertEqual(127, retcode)
On 2013/10/15 17:02:11, gary.poster wrote:
> likewise

In this case this is safe because we return 127 ourselves.

https://codereview.appspot.com/14669047/diff/1/quickstart/utils.py
File quickstart/utils.py (right):

https://codereview.appspot.com/14669047/diff/1/quickstart/utils.py#newcode58
quickstart/utils.py:58: retcode, output, _ = call('juju', 'switch')
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/current-environment and ~/.juju/environments.yaml[default]?
  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/current-environment (is it reliable?, if not, is "juju switch
--format json" a good idea?).

https://codereview.appspot.com/14669047/

Revision history for this message
Francesco Banconi (frankban) wrote :

Thank you Gary and Matthew!

https://codereview.appspot.com/14669047/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches