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
=== modified file 'Makefile'
--- Makefile 2013-10-14 15:54:00 +0000
+++ Makefile 2013-10-16 07:23:37 +0000
@@ -15,10 +15,6 @@
15# along with this program. If not, see <http://www.gnu.org/licenses/>.15# along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
17PYTHON = python17PYTHON = python
18
19# Define the default Juju environment used by "make run".
20JUJU_ENV ?= local
21
22SYSDEPS = build-essential python-pip python-virtualenv18SYSDEPS = build-essential python-pip python-virtualenv
2319
24VENV = .venv20VENV = .venv
@@ -36,7 +32,7 @@
3632
37clean:33clean:
38 $(PYTHON) setup.py clean34 $(PYTHON) setup.py clean
39 rm -rfv build/ dist/ ./juju_quickstart.egg-info MANIFEST35 rm -rfv build/ dist/ juju_quickstart.egg-info MANIFEST
40 rm -rfv $(VENV)36 rm -rfv $(VENV)
41 find . -name '*.pyc' -delete37 find . -name '*.pyc' -delete
42 find . -name '__pycache__' -type d -delete38 find . -name '__pycache__' -type d -delete
@@ -53,9 +49,10 @@
53 @echo 'make source - Create source package.'49 @echo 'make source - Create source package.'
54 @echo 'make install - Install on local system.'50 @echo 'make install - Install on local system.'
55 @echo 'make run - Run the application in the development environment.\n'51 @echo 'make run - Run the application in the development environment.\n'
56 @echo ' An environment named "local" is used by default.'52 @echo ' If "juju switch" has been used to set a default environment, that'
57 @echo ' Define the JUJU_ENV environment variable to select another'53 @echo ' environment will be used. It is possible to override the default'
58 @echo ' environment, e.g.: "make run JUJU_ENV=ec2".'54 @echo ' Juju environment by setting the JUJU_ENV environment variable,'
55 @echo ' e.g.: "make run JUJU_ENV=ec2".'
59 @echo 'make clean - Get rid of bytecode files, build and dist dirs, venv.'56 @echo 'make clean - Get rid of bytecode files, build and dist dirs, venv.'
6057
61install:58install:
@@ -66,7 +63,7 @@
66 @$(VENV)/bin/flake8 --show-source --exclude=$(VENV) ./quickstart63 @$(VENV)/bin/flake8 --show-source --exclude=$(VENV) ./quickstart
6764
68run: setup65run: setup
69 $(VENV)/bin/python ./juju-quickstart --environment $(JUJU_ENV)66 $(VENV)/bin/python ./juju-quickstart
7067
71source:68source:
72 $(PYTHON) setup.py sdist69 $(PYTHON) setup.py sdist
@@ -77,5 +74,6 @@
77test: setup74test: setup
78 @$(VENV)/bin/nosetests -s --verbosity=2 \75 @$(VENV)/bin/nosetests -s --verbosity=2 \
79 --with-coverage --cover-package=quickstart quickstart76 --with-coverage --cover-package=quickstart quickstart
77 @rm .coverage
8078
81.PHONY: all clean check help install lint run setup source sysdeps test79.PHONY: all clean check help install lint run setup source sysdeps test
8280
=== modified file 'juju-quickstart'
--- juju-quickstart 2013-10-14 15:24:29 +0000
+++ juju-quickstart 2013-10-16 07:23:37 +0000
@@ -18,9 +18,17 @@
1818
19"""Juju Quickstart plugin entry point."""19"""Juju Quickstart plugin entry point."""
2020
21from quickstart import manage21import sys
22
23from quickstart import (
24 app,
25 manage,
26)
2227
2328
24if __name__ == '__main__':29if __name__ == '__main__':
25 options = manage.setup()30 options = manage.setup()
26 manage.run(options)31 try:
32 manage.run(options)
33 except app.ProgramExit as err:
34 sys.exit(str(err))
2735
=== added file 'quickstart/app.py'
--- quickstart/app.py 1970-01-01 00:00:00 +0000
+++ quickstart/app.py 2013-10-16 07:23:37 +0000
@@ -0,0 +1,42 @@
1# This file is part of the Juju GUI, which lets users view and manage Juju
2# environments within a graphical interface (https://launchpad.net/juju-gui).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Juju Quickstart base application functions."""
18
19
20class ProgramExit(Exception):
21 """An error occurred while setting up the Juju environment.
22
23 Raise this exception if you want the program to exit gracefully printing
24 the error message to stderr.
25 """
26
27 def __init__(self, message):
28 self.message = message
29
30 def __str__(self):
31 return 'juju-quickstart: error: {}'.format(self.message)
32
33
34def bootstrap(env_name):
35 """Bootstrap the Juju environment with the given name.
36
37 Return the environment API address (e.g. "api.example.com:17070").
38 Raise a ProgramExit if any error occurs in the bootstrap process.
39 Otherwise return when the environment is bootstrapped and the API server
40 is ready to accept connections.
41 """
42 # TODO: everything!
043
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2013-10-14 15:24:29 +0000
+++ quickstart/manage.py 2013-10-16 07:23:37 +0000
@@ -21,9 +21,13 @@
21import os21import os
2222
23import quickstart23import quickstart
2424from quickstart import (
2525 app,
26default_envfile = os.path.expanduser('~/.juju/environments.yaml')26 utils,
27)
28
29
30juju_home = os.getenv('JUJU_HOME', '~/.juju')
27description = 'set up a Juju environment (including the GUI) in very few steps'31description = 'set up a Juju environment (including the GUI) in very few steps'
28version = quickstart.get_version()32version = quickstart.get_version()
2933
@@ -36,26 +40,77 @@
36 parser.exit()40 parser.exit()
3741
3842
43def _validate_env(options, parser):
44 """Validate and process the provided environment related options.
45
46 Exit with an error if options are not valid.
47 """
48 env_file = os.path.abspath(os.path.expanduser(options.env_file))
49 # Validate the environment name.
50 env_name = options.env_name
51 if env_name is None:
52 return parser.error(
53 'unable to find an environment name to use\n'
54 'It is possible to specify the environment name by either:\n'
55 ' - passing the -e or --environment argument;\n'
56 ' - setting the JUJU_ENV environment variable;\n'
57 ' - using "juju switch" to select the default environment to use.'
58 )
59 # Validate the environment file.
60 try:
61 env_type, admin_secret = utils.parse_env_file(env_file, env_name)
62 except ValueError as err:
63 return parser.error(str(err))
64 # XXX 2013-10-15 frankban: add support for local providers.
65 if env_type == 'local':
66 return parser.error('the local provider is not currently supported')
67 # Update the options namespace with the new values.
68 options.admin_secret = admin_secret
69 options.env_file = env_file
70 options.env_type = env_type
71
72
39def setup():73def setup():
40 """Set up the application options."""74 """Set up the application options.
75
76 Return the options as a namespace containing the followin attributes:
77 - admin_secret: the password to use to access the Juju API;
78 - env_file: the absolute path of the Juju environments.yaml file;
79 - env_name: the name of the Juju environment to use;
80 - env_type: the provider type of the selected Juju environment.
81
82 Exit with an error if the provided arguments are not valid.
83 """
84 default_env_name = utils.get_default_env_name()
85 # Define the help message for the --environment option.
86 env_help = 'the name of the Juju environment to use'
87 if default_env_name is not None:
88 env_help = '{} (%(default)s)'.format(env_help)
89 # Create and set up the arguments parser.
41 parser = argparse.ArgumentParser(description=quickstart.__doc__)90 parser = argparse.ArgumentParser(description=quickstart.__doc__)
42 parser.add_argument(91 parser.add_argument(
43 '-e', '--environment', required=True, dest='env_name',92 '-e', '--environment', default=default_env_name, dest='env_name',
44 help='the name of the Juju environment to use')93 help=env_help)
45 parser.add_argument(94 parser.add_argument(
46 '--environments-file', dest='env_file', type=file,95 '--environments-file',
47 default=default_envfile,96 default=os.path.join(juju_home, 'environments.yaml'), dest='env_file',
48 help='the path to the Juju environments YAML file')97 help='the path to the Juju environments YAML file (%(default)s)')
49 parser.add_argument(98 parser.add_argument(
50 '--version', action='version', version='%(prog)s {}'.format(version))99 '--version', action='version', version='%(prog)s {}'.format(version))
51 # This is required by juju-core: see "juju help plugins".100 # This is required by juju-core: see "juju help plugins".
52 parser.add_argument(101 parser.add_argument(
53 '--description', action=_DescriptionAction, default=argparse.SUPPRESS,102 '--description', action=_DescriptionAction, default=argparse.SUPPRESS,
54 nargs=0, help="show program's description and exit")103 nargs=0, help="show program's description and exit")
55 return parser.parse_args()104 # Parse the provided arguments.
105 options = parser.parse_args()
106 # Validate and process the provided arguments.
107 _validate_env(options, parser)
108 return options
56109
57110
58def run(options):111def run(options):
59 """Run the application"""112 """Run the application."""
60 print('juju quickstart v{}'.format(version))113 print('juju quickstart v{}'.format(version))
61 # TODO: everything!114 print('bootstrapping the {} environment (type: {})'.format(
115 options.env_name, options.env_type))
116 app.bootstrap(options.env_name)
62117
=== added file 'quickstart/tests/helpers.py'
--- quickstart/tests/helpers.py 1970-01-01 00:00:00 +0000
+++ quickstart/tests/helpers.py 2013-10-16 07:23:37 +0000
@@ -0,0 +1,60 @@
1# This file is part of the Juju GUI, which lets users view and manage Juju
2# environments within a graphical interface (https://launchpad.net/juju-gui).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Test helpers for the Juju Quickstart plugin."""
18
19from contextlib import contextmanager
20import os
21import tempfile
22
23import yaml
24
25
26class ValueErrorTestsMixin(object):
27 """Set up some base methods for testing functions raising ValueErrors."""
28
29 @contextmanager
30 def assert_value_error(self, error):
31 """Ensure a ValueError is raised in the context block.
32
33 Also check that the exception includes the expected error message.
34 """
35 with self.assertRaises(ValueError) as context_manager:
36 yield
37 self.assertEqual(error, str(context_manager.exception))
38
39
40class EnvFileTestsMixin(object):
41 """Shared methods for testing a Juju environments file."""
42
43 valid_contents = yaml.safe_dump({
44 'environments': {'aws': {'admin-secret': 'Secret!', 'type': 'ec2'}},
45 })
46
47 def make_env_file(self, contents=None):
48 """Create a Juju environments file containing the given contents.
49
50 If contents is None, use the valid environment contents defined in
51 self.valid_contents.
52 Return the environments file path.
53 """
54 if contents is None:
55 contents = self.valid_contents
56 env_file = tempfile.NamedTemporaryFile(delete=False)
57 self.addCleanup(os.remove, env_file.name)
58 env_file.write(contents)
59 env_file.close()
60 return env_file.name
061
=== added file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 1970-01-01 00:00:00 +0000
+++ quickstart/tests/test_app.py 2013-10-16 07:23:37 +0000
@@ -0,0 +1,29 @@
1# This file is part of the Juju GUI, which lets users view and manage Juju
2# environments within a graphical interface (https://launchpad.net/juju-gui).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tests for the Juju Quickstart base application functions."""
18
19import unittest
20
21from quickstart import app
22
23
24class TestProgramExit(unittest.TestCase):
25
26 def test_string_representation(self):
27 # The error is properly represented as a string.
28 exception = app.ProgramExit('bad wolf')
29 self.assertEqual('juju-quickstart: error: bad wolf', str(exception))
030
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2013-10-14 15:24:29 +0000
+++ quickstart/tests/test_manage.py 2013-10-16 07:23:37 +0000
@@ -17,11 +17,15 @@
17"""Tests for the Juju Quickstart management infrastructure."""17"""Tests for the Juju Quickstart management infrastructure."""
1818
19import argparse19import argparse
20import os
20import unittest21import unittest
2122
22import mock23import mock
24import yaml
2325
26import quickstart
24from quickstart import manage27from quickstart import manage
28from quickstart.tests import helpers
2529
2630
27class TestDescriptionAction(unittest.TestCase):31class TestDescriptionAction(unittest.TestCase):
@@ -39,3 +43,130 @@
39 self.assertIsNone(args.test)43 self.assertIsNone(args.test)
40 mock_print.assert_called_once_with(manage.description)44 mock_print.assert_called_once_with(manage.description)
41 mock_exit.assert_called_once_with(0)45 mock_exit.assert_called_once_with(0)
46
47
48class TestValidateEnv(helpers.EnvFileTestsMixin, unittest.TestCase):
49
50 def setUp(self):
51 self.parser = mock.Mock()
52
53 def make_options(self, env_file, env_name=None):
54 """Return a mock options object which includes the passed arguments."""
55 return mock.Mock(env_file=env_file, env_name=env_name)
56
57 def test_resulting_options(self):
58 # The options object is correctly set up.
59 env_file = self.make_env_file()
60 options = self.make_options(env_file, env_name='aws')
61 manage._validate_env(options, self.parser)
62 self.assertEqual('Secret!', options.admin_secret)
63 self.assertEqual(env_file, options.env_file)
64 self.assertEqual('aws', options.env_name)
65 self.assertEqual('ec2', options.env_type)
66
67 def test_expand_user(self):
68 # The ~ construct is correctly expanded in the validation process.
69 env_file = self.make_env_file()
70 # Split the full path of the env file, e.g. from a full "/tmp/env.file"
71 # path retrieve the base path "/tmp" and the file name "env.file".
72 # By doing that we can simulate that the user's home is "/tmp" and that
73 # the env file is "~/env.file".
74 base_path, filename = os.path.split(env_file)
75 path = '~/{}'.format(filename)
76 options = self.make_options(env_file=path, env_name='aws')
77 with mock.patch('os.environ', {'HOME': base_path}):
78 manage._validate_env(options, self.parser)
79 self.assertEqual(env_file, options.env_file)
80
81 def test_no_env_name(self):
82 # A parser error is invoked if the environment name is missing.
83 options = self.make_options(self.make_env_file())
84 manage._validate_env(options, self.parser)
85 self.assertTrue(self.parser.error.called)
86 message = self.parser.error.call_args[0][0]
87 self.assertIn('unable to find an environment name to use', message)
88
89 def test_error_parsing_env_file(self):
90 # A parser error is invoked if an error occurs parsing the env file.
91 env_file = self.make_env_file()
92 options = self.make_options(env_file, env_name='no-such')
93 manage._validate_env(options, self.parser)
94 expected = 'environment no-such not found in {}'.format(env_file)
95 self.parser.error.assert_called_once_with(expected)
96
97 def test_local_provider(self):
98 # The parser exits with an error if the provided environment name
99 # refers to a local provider type.
100 contents = yaml.safe_dump({
101 'environments': {
102 'lxc': {'admin-secret': 'Secret!', 'type': 'local'},
103 },
104 })
105 env_file = self.make_env_file(contents)
106 options = self.make_options(env_file, env_name='lxc')
107 manage._validate_env(options, self.parser)
108 expected = 'the local provider is not currently supported'
109 self.parser.error.assert_called_once_with(expected)
110
111
112@mock.patch('quickstart.manage._validate_env', mock.Mock())
113class TestSetup(unittest.TestCase):
114
115 def patch_get_default_env_name(self, env_name=None):
116 """Patch the function used by setup() to retrieve the default env name.
117
118 This way the test does not rely on the user's Juju environment set up,
119 and it is also possible to simulate an arbitrary environment name.
120 """
121 mock_get_default_env_name = mock.Mock(return_value=env_name)
122 path = 'quickstart.manage.utils.get_default_env_name'
123 return mock.patch(path, mock_get_default_env_name)
124
125 def call_setup(self, args, env_name=None):
126 """Call the setup function simulating the given args and env name.
127
128 Also ensure the program exits without errors.
129 """
130 with mock.patch('sys.argv', ['juju-quickstart'] + args):
131 with mock.patch('sys.exit') as mock_exit:
132 with self.patch_get_default_env_name(env_name):
133 manage.setup()
134 mock_exit.assert_called_once_with(0)
135
136 def test_help(self):
137 # The program help message is properly formatted.
138 with mock.patch('sys.stdout') as mock_stdout:
139 self.call_setup(['--help'])
140 stdout_write = mock_stdout.write
141 self.assertTrue(stdout_write.called)
142 # Retrieve the output from the mock call.
143 output = stdout_write.call_args[0][0]
144 self.assertIn('usage: juju-quickstart', output)
145 self.assertIn(quickstart.__doc__, output)
146 self.assertIn('--environment', output)
147 # Without a default environment, the -e option has no default.
148 self.assertIn('the name of the Juju environment to use\n', output)
149
150 def test_help_with_default_environment(self):
151 # The program help message is properly formatted when a default Juju
152 # environment is found.
153 with mock.patch('sys.stdout') as mock_stdout:
154 self.call_setup(['--help'], env_name='hp')
155 stdout_write = mock_stdout.write
156 self.assertTrue(stdout_write.called)
157 # Retrieve the output from the mock call.
158 output = stdout_write.call_args[0][0]
159 self.assertIn('the name of the Juju environment to use (hp)\n', output)
160
161 def test_description(self):
162 # The program description is properly printed out as required by juju.
163 with mock.patch('__builtin__.print') as mock_print:
164 self.call_setup(['--description'])
165 mock_print.assert_called_once_with(manage.description)
166
167 def test_version(self):
168 # The program version is properly printed to stderr.
169 with mock.patch('sys.stderr') as mock_stderr:
170 self.call_setup(['--version'])
171 expected = 'juju-quickstart {}\n'.format(quickstart.get_version())
172 mock_stderr.write.assert_called_once_with(expected)
42173
=== added file 'quickstart/tests/test_utils.py'
--- quickstart/tests/test_utils.py 1970-01-01 00:00:00 +0000
+++ quickstart/tests/test_utils.py 2013-10-16 07:23:37 +0000
@@ -0,0 +1,178 @@
1# This file is part of the Juju GUI, which lets users view and manage Juju
2# environments within a graphical interface (https://launchpad.net/juju-gui).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tests for the Juju Quickstart utility functions and classes."""
18
19import unittest
20
21import mock
22import yaml
23
24from quickstart import utils
25from quickstart.tests import helpers
26
27
28class TestCall(unittest.TestCase):
29
30 def test_success(self):
31 # A zero exit code and the subprocess output are correctly returned.
32 retcode, output, error = utils.call('echo')
33 self.assertEqual(0, retcode)
34 self.assertEqual('\n', output)
35 self.assertEqual('', error)
36
37 def test_multiple_arguments(self):
38 # A zero exit code and the subprocess output are correctly returned
39 # when executing a command passing multiple arguments.
40 retcode, output, error = utils.call('echo', 'we are the borg!')
41 self.assertEqual(0, retcode)
42 self.assertEqual('we are the borg!\n', output)
43 self.assertEqual('', error)
44
45 def test_failure(self):
46 # An error code and the error are returned if the subprocess fails.
47 retcode, output, error = utils.call('ls', 'no-such-file')
48 self.assertNotEqual(0, retcode)
49 self.assertEqual('', output)
50 self.assertEqual(
51 'ls: cannot access no-such-file: No such file or directory\n',
52 error)
53
54 def test_invalid_command(self):
55 # An error code and the error are returned if the subprocess fails to
56 # find the provided command in the PATH.
57 retcode, output, error = utils.call('no-such-command')
58 self.assertEqual(127, retcode)
59 self.assertEqual('', output)
60 self.assertEqual(
61 'no-such-command: [Errno 2] No such file or directory',
62 error)
63
64
65class TestGetDefaultEnvName(unittest.TestCase):
66
67 def patch_call(self, retcode, output='', error=''):
68 """Patch the quickstart.utils.call function."""
69 mock_call = mock.Mock(return_value=(retcode, output, error))
70 return mock.patch('quickstart.utils.call', mock_call)
71
72 def test_environment_variable(self):
73 # The environment name is successfully returned if JUJU_ENV is set.
74 with mock.patch('os.environ', {'JUJU_ENV': 'ec2'}):
75 env_name = utils.get_default_env_name()
76 self.assertEqual('ec2', env_name)
77
78 def test_empty_environment_variable(self):
79 # The environment name is not found if JUJU_ENV is empty.
80 with self.patch_call(1):
81 with mock.patch('os.environ', {'JUJU_ENV': ' '}):
82 env_name = utils.get_default_env_name()
83 self.assertIsNone(env_name)
84
85 def test_no_environment_variable(self):
86 # The environment name is not found if JUJU_ENV is not defined.
87 with self.patch_call(1):
88 with mock.patch('os.environ', {}):
89 env_name = utils.get_default_env_name()
90 self.assertIsNone(env_name)
91
92 def test_juju_switch(self):
93 # The environment name is successfully returned if previously set using
94 # the "juju switch" command.
95 output = 'Current environment: "hp"\n'
96 with self.patch_call(0, output=output) as mock_call:
97 with mock.patch('os.environ', {}):
98 env_name = utils.get_default_env_name()
99 self.assertEqual('hp', env_name)
100 mock_call.assert_called_once_with('juju', 'switch')
101
102 def test_juju_switch_failure(self):
103 # The environment name is not found if "juju switch" returns an error.
104 with self.patch_call(1) as mock_call:
105 with mock.patch('os.environ', {}):
106 env_name = utils.get_default_env_name()
107 self.assertIsNone(env_name)
108 mock_call.assert_called_once_with('juju', 'switch')
109
110 def test_juju_switch_nonsense(self):
111 # The environment name is not found if "juju switch" goes crazy.
112 with self.patch_call(0, output='Exterminate!') as mock_call:
113 with mock.patch('os.environ', {}):
114 env_name = utils.get_default_env_name()
115 self.assertIsNone(env_name)
116 mock_call.assert_called_once_with('juju', 'switch')
117
118
119class TestParseEnvFile(
120 helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin,
121 unittest.TestCase):
122
123 def test_no_file(self):
124 # A ValueError is raised if the environments file is not found.
125 expected = (
126 'unable to open environments file: '
127 "[Errno 2] No such file or directory: '/no/such/file.yaml'"
128 )
129 with self.assert_value_error(expected):
130 utils.parse_env_file('/no/such/file.yaml', 'ec2')
131
132 def test_invalid_yaml(self):
133 # A ValueError is raised if the environments file is not a valid YAML.
134 env_file = self.make_env_file(':')
135 with self.assertRaises(ValueError) as context_manager:
136 utils.parse_env_file(env_file, 'ec2')
137 expected = 'unable to parse environments file {}'.format(env_file)
138 self.assertIn(expected, str(context_manager.exception))
139
140 def test_invalid_yaml_contents(self):
141 # A ValueError is raised if the environments file is not well formed.
142 env_file = self.make_env_file('a-string')
143 expected = 'invalid YAML contents in {}: a-string'.format(env_file)
144 with self.assert_value_error(expected):
145 utils.parse_env_file(env_file, 'ec2')
146
147 def test_no_env(self):
148 # A ValueError is raised if the environment is not found in the YAML.
149 contents = yaml.safe_dump({'environments': {'local': {}}})
150 env_file = self.make_env_file(contents)
151 expected = 'environment ec2 not found in {}'.format(env_file)
152 with self.assert_value_error(expected):
153 utils.parse_env_file(env_file, 'ec2')
154
155 def test_no_provider_type(self):
156 # A ValueError is raised if the provider type is not included in the
157 # environment info.
158 contents = yaml.safe_dump({'environments': {'aws': {}}})
159 env_file = self.make_env_file(contents)
160 expected = 'aws provider type not found in {}'.format(env_file)
161 with self.assert_value_error(expected):
162 utils.parse_env_file(env_file, 'aws')
163
164 def test_no_admin_secret(self):
165 # A ValueError is raised if the admin secret is not included in the
166 # environment info.
167 contents = yaml.safe_dump({'environments': {'aws': {'type': 'ec2'}}})
168 env_file = self.make_env_file(contents)
169 expected = 'aws admin secret not found in {}'.format(env_file)
170 with self.assert_value_error(expected):
171 utils.parse_env_file(env_file, 'aws')
172
173 def test_success(self):
174 # The environment provider type and admin secret are returned.
175 env_file = self.make_env_file()
176 env_type, admin_secret = utils.parse_env_file(env_file, 'aws')
177 self.assertEqual('ec2', env_type)
178 self.assertEqual('Secret!', admin_secret)
0179
=== added file 'quickstart/utils.py'
--- quickstart/utils.py 1970-01-01 00:00:00 +0000
+++ quickstart/utils.py 2013-10-16 07:23:37 +0000
@@ -0,0 +1,110 @@
1# This file is part of the Juju GUI, which lets users view and manage Juju
2# environments within a graphical interface (https://launchpad.net/juju-gui).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Juju Quickstart utility functions and classes."""
18
19import re
20import os
21import subprocess
22
23import yaml
24
25# Compile the regular expression used to parse the "juju switch" output.
26_juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n')
27
28
29def call(*args):
30 """Call a subprocess passing the given arguments.
31
32 Take the subcommand and its parameters as args.
33
34 Return a tuple containing the subprocess return code, output and error.
35 """
36 pipe = subprocess.PIPE
37 try:
38 process = subprocess.Popen(args, stdout=pipe, stderr=pipe)
39 except OSError as err:
40 # A return code 127 is returned by the shell when the command is not
41 # found in the PATH.
42 return 127, '', '{}: {}'.format(args[0], err)
43 output, error = process.communicate()
44 return process.poll(), output, error
45
46
47def get_default_env_name():
48 """Return the current Juju environment name.
49
50 The environment name can be set either by setting the JUJU_ENV environment
51 variable or by using "juju switch". The former overrides the latter.
52
53 Return None if a default environment is not found.
54 """
55 env_name = os.getenv('JUJU_ENV', '').strip()
56 if env_name:
57 return env_name
58 retcode, output, _ = call('juju', 'switch')
59 if retcode:
60 return None
61 match = _juju_switch_expression.match(output)
62 if match is not None:
63 return match.groups()[0]
64
65
66def parse_env_file(env_file, env_name):
67 """Parse the provided Juju environment.yaml file.
68
69 Return a tuple containing the provider type and the admin secret associated
70 with the given environment name.
71
72 Raise a ValueError if:
73 - the environment file is not found;
74 - the environment file contents are not parsable by YAML;
75 - the YAML contents are not properly structured;
76 - the environment named envname is not found;
77 - the environment does not include the "type" field;
78 - the environment does not include the "admin_secret" field.
79 """
80 # Load the Juju environments file.
81 try:
82 environments_file = open(env_file)
83 except IOError as err:
84 msg = 'unable to open environments file: {}'.format(err)
85 raise ValueError(msg)
86 # Parse the Juju environments file.
87 try:
88 environments = yaml.safe_load(environments_file)
89 except Exception as err:
90 msg = 'unable to parse environments file {}: {}'.format(env_file, err)
91 raise ValueError(msg)
92 # Retrieve the information about the current environment.
93 try:
94 environment = environments.get('environments', {}).get(env_name)
95 except AttributeError as err:
96 msg = 'invalid YAML contents in {}: {}'.format(env_file, environments)
97 raise ValueError(msg)
98 if environment is None:
99 msg = 'environment {} not found in {}'.format(env_name, env_file)
100 raise ValueError(msg)
101 # Retrieve the provider type and the admin secret.
102 env_type = environment.get('type')
103 if env_type is None:
104 msg = '{} provider type not found in {}'.format(env_name, env_file)
105 raise ValueError(msg)
106 admin_secret = environment.get('admin-secret')
107 if admin_secret is None:
108 msg = '{} admin secret not found in {}'.format(env_name, env_file)
109 raise ValueError(msg)
110 return env_type, admin_secret

Subscribers

People subscribed via source and target branches