Merge lp:~frankban/juju-quickstart/handle-jenv-envs into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 109
Proposed branch: lp:~frankban/juju-quickstart/handle-jenv-envs
Merge into: lp:juju-quickstart
Diff against target: 725 lines (+364/-80)
8 files modified
quickstart/app.py (+13/-15)
quickstart/manage.py (+26/-12)
quickstart/models/jenv.py (+85/-7)
quickstart/tests/helpers.py (+49/-9)
quickstart/tests/models/test_jenv.py (+141/-7)
quickstart/tests/test_app.py (+22/-23)
quickstart/tests/test_juju.py (+9/-0)
quickstart/tests/test_manage.py (+19/-7)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/handle-jenv-envs
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+244880@code.launchpad.net

Description of the change

Promote jenv files as first class envs.

Quickstart no longer refuses to use an environment
which is only present as a jenv file.

Add some more helper functions to the

Also retrieve the environment type, in the case
the environment is already bootstrapped, from
the WebSocket connection and not from the jenv:
jenv files are not assumed to include the type.

Tests: `make check`.

QA:
- use quickstart to bootstrap an environment:
  `.venv/bin/python juju-quickstart`;

- re-run quickstart again to reopen the same environment:
  `.venv/bin/python juju-quickstart`;

- in both cases, check auto-login works and the output
  is sane;

- generate a new environment user and put the
  resulting jenv in your Juju home:
  `juju user add myuser --generate -o ~/.juju/environments/myenv.jenv`;

- use quickstart with the new environment:
  `.venv/bin/python juju-quickstart -e myenv`;

- check that the new credentials are printed to stdout
  and that the auto-login still works;

- destroy the environment.

Thanks a lot!

https://codereview.appspot.com/188300043/

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

Reviewers: mp+244880_code.launchpad.net,

Message:
Please take a look.

Description:
Promote jenv files as first class envs.

Quickstart no longer refuses to use an environment
which is only present as a jenv file.

Add some more helper functions to the

Also retrieve the environment type, in the case
the environment is already bootstrapped, from
the WebSocket connection and not from the jenv:
jenv files are not assumed to include the type.

Tests: `make check`.

QA:
- use quickstart to bootstrap an environment:
   `.venv/bin/python juju-quickstart`;

- re-run quickstart again to reopen the same environment:
   `.venv/bin/python juju-quickstart`;

- in both cases, check auto-login works and the output
   is sane;

- generate a new environment user and put the
   resulting jenv in your Juju home:
   `juju user add myuser --generate -o ~/.juju/environments/myenv.jenv`;

- use quickstart with the new environment:
   `.venv/bin/python juju-quickstart -e myenv`;

- check that the new credentials are printed to stdout
   and that the auto-login still works;

- destroy the environment.

Thanks a lot!

https://code.launchpad.net/~frankban/juju-quickstart/handle-jenv-envs/+merge/244880

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/188300043/

Affected files (+366, -80 lines):
   A [revision details]
   M quickstart/app.py
   M quickstart/manage.py
   M quickstart/models/jenv.py
   M quickstart/tests/helpers.py
   M quickstart/tests/models/test_jenv.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_juju.py
   M quickstart/tests/test_manage.py

Revision history for this message
Brad Crittenden (bac) wrote :

LGTM. WIll QA next.

https://codereview.appspot.com/188300043/diff/1/quickstart/manage.py
File quickstart/manage.py (right):

https://codereview.appspot.com/188300043/diff/1/quickstart/manage.py#newcode241
quickstart/manage.py:241: # If the environment was found in the
environments.yaml file, we can also
This comment seems wrong since we could've come through the second path,
meaning it was not in environments.yaml.

https://codereview.appspot.com/188300043/

Revision history for this message
Brad Crittenden (bac) wrote :

QA was OK.

Note juju will not let you destroy the environment with the generated
name. If 'local' was originally booted and a mytest.jenv file was
generated by 'juju user add' then 'juju destroy-environment mytest'
prints "removing empty environment file" but does not destroy the
environment.

Not a quickstart issue but interesting.

https://codereview.appspot.com/188300043/

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

Thanks for the reviews!

https://codereview.appspot.com/188300043/diff/1/quickstart/manage.py
File quickstart/manage.py (right):

https://codereview.appspot.com/188300043/diff/1/quickstart/manage.py#newcode241
quickstart/manage.py:241: # If the environment was found in the
environments.yaml file, we can also
On 2014/12/16 17:36:37, bac wrote:
> This comment seems wrong since we could've come through the second
path, meaning
> it was not in environments.yaml.

But this point is never reached in that case: the second path (outer
except block) always returns and exits the function.

https://codereview.appspot.com/188300043/

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

*** Submitted:

Promote jenv files as first class envs.

Quickstart no longer refuses to use an environment
which is only present as a jenv file.

Add some more helper functions to the

Also retrieve the environment type, in the case
the environment is already bootstrapped, from
the WebSocket connection and not from the jenv:
jenv files are not assumed to include the type.

Tests: `make check`.

QA:
- use quickstart to bootstrap an environment:
   `.venv/bin/python juju-quickstart`;

- re-run quickstart again to reopen the same environment:
   `.venv/bin/python juju-quickstart`;

- in both cases, check auto-login works and the output
   is sane;

- generate a new environment user and put the
   resulting jenv in your Juju home:
   `juju user add myuser --generate -o ~/.juju/environments/myenv.jenv`;

- use quickstart with the new environment:
   `.venv/bin/python juju-quickstart -e myenv`;

- check that the new credentials are printed to stdout
   and that the auto-login still works;

- destroy the environment.

Thanks a lot!

R=bac, rharding
CC=
https://codereview.appspot.com/188300043

https://codereview.appspot.com/188300043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'quickstart/app.py'
--- quickstart/app.py 2014-12-16 11:10:10 +0000
+++ quickstart/app.py 2014-12-16 16:32:00 +0000
@@ -278,21 +278,6 @@
278 raise ProgramExit('the state server is not ready:\n{}'.format(details))278 raise ProgramExit('the state server is not ready:\n{}'.format(details))
279279
280280
281def get_env_type(env_name):
282 """Return the Juju environment type for the given environment name.
283
284 Since the environment type is retrieved by parsing the jenv file, the
285 environment must be already bootstrapped.
286
287 Raise a ProgramExit if the environment type cannot be retrieved.
288 """
289 try:
290 return jenv.get_value(env_name, 'bootstrap-config', 'type')
291 except ValueError as err:
292 msg = b'cannot retrieve environment type: {}'.format(err)
293 raise ProgramExit(msg)
294
295
296def get_credentials(env_name):281def get_credentials(env_name):
297 """Return the Juju credentials for the given environment name.282 """Return the Juju credentials for the given environment name.
298283
@@ -353,6 +338,19 @@
353 return env338 return env
354339
355340
341def get_env_type(env):
342 """Return the Juju environment type for the given environment connection.
343
344 Raise a ProgramExit if the environment type cannot be retrieved.
345 """
346 try:
347 info = env.info()
348 except jujuclient.EnvError as err:
349 msg = 'cannot retrieve the environment type: {}'.format(err.message)
350 raise ProgramExit(msg)
351 return info['ProviderType']
352
353
356def create_auth_token(env):354def create_auth_token(env):
357 """Return a new authentication token.355 """Return a new authentication token.
358356
359357
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2014-12-15 17:31:23 +0000
+++ quickstart/manage.py 2014-12-16 16:32:00 +0000
@@ -42,6 +42,7 @@
42from quickstart.models import (42from quickstart.models import (
43 charms,43 charms,
44 envs,44 envs,
45 jenv,
45)46)
4647
4748
@@ -222,16 +223,23 @@
222 return env_data223 return env_data
223224
224225
225def _retrieve_env_data(parser, env_type_db, env_db, env_name):226def _retrieve_env_data(parser, env_type_db, env_db, jenv_db, env_name):
226 """Retrieve and return the env_data corresponding to the given env_name.227 """Retrieve and return the env_data corresponding to the given env_name.
227228
228 Invoke a parser error if the environment does not exist or is not valid.229 Invoke a parser error if the environment does not exist or is not valid.
229 """230 """
230 try:231 try:
231 env_data = envs.get_env_data(env_db, env_name)232 env_data = envs.get_env_data(env_db, env_name)
232 except ValueError as err:233 except ValueError:
233 # The specified environment does not exist.234 # The specified environment does not exist in the environments file.
234 return parser.error(bytes(err))235 # Check if this is an imported environment.
236 try:
237 return envs.get_env_data(jenv_db, env_name)
238 except ValueError as err:
239 # The environment cannot be found anywhere, exit with an error.
240 return parser.error(bytes(err))
241 # If the environment was found in the environments.yaml file, we can also
242 # validate it.
235 env_metadata = envs.get_env_metadata(env_type_db, env_data)243 env_metadata = envs.get_env_metadata(env_type_db, env_data)
236 errors = envs.validate(env_metadata, env_data)244 errors = envs.validate(env_metadata, env_data)
237 if errors:245 if errors:
@@ -275,6 +283,7 @@
275283
276 # Retrieve the environment database (or create an in-memory empty one).284 # Retrieve the environment database (or create an in-memory empty one).
277 env_db = _retrieve_env_db(parser, env_file if env_file_exists else None)285 env_db = _retrieve_env_db(parser, env_file if env_file_exists else None)
286 jenv_db = jenv.get_env_db()
278287
279 # Validate the environment.288 # Validate the environment.
280 env_type_db = envs.get_env_type_db()289 env_type_db = envs.get_env_type_db()
@@ -285,7 +294,8 @@
285 else:294 else:
286 # This is a non-interactive session and we need to validate the295 # This is a non-interactive session and we need to validate the
287 # selected environment before proceeding.296 # selected environment before proceeding.
288 env_data = _retrieve_env_data(parser, env_type_db, env_db, env_name)297 env_data = _retrieve_env_data(
298 parser, env_type_db, env_db, jenv_db, env_name)
289 # Check for local support, if requested.299 # Check for local support, if requested.
290 options.env_type = env_data['type']300 options.env_type = env_data['type']
291 no_local_support = not platform_support.supports_local(options.platform)301 no_local_support = not platform_support.supports_local(options.platform)
@@ -510,8 +520,7 @@
510 env_type = options.env_type520 env_type = options.env_type
511 api_url = app.check_bootstrapped(options.env_name)521 api_url = app.check_bootstrapped(options.env_name)
512 if api_url is None:522 if api_url is None:
513 print('bootstrapping the {} environment (type: {})'.format(523 print('bootstrapping the {} environment'.format(options.env_name))
514 options.env_name, env_type))
515 if env_type == 'local':524 if env_type == 'local':
516 # If this is a local environment, notify the user that "sudo" will525 # If this is a local environment, notify the user that "sudo" will
517 # be required by Juju to bootstrap the environment.526 # be required by Juju to bootstrap the environment.
@@ -524,11 +533,8 @@
524 upload_series=options.upload_series,533 upload_series=options.upload_series,
525 constraints=options.constraints)534 constraints=options.constraints)
526 if already_bootstrapped:535 if already_bootstrapped:
527 # Retrieve the environment type from the jenv file: it may be different536 print('reusing the already bootstrapped {} environment'.format(
528 # from the one declared on the environments.yaml file.537 options.env_name))
529 env_type = app.get_env_type(options.env_name)
530 print('reusing the already bootstrapped {} environment '
531 '(type: {})'.format(options.env_name, env_type))
532538
533 # Retrieve the environment status, ensure it is in a ready state and539 # Retrieve the environment status, ensure it is in a ready state and
534 # contextually fetch the bootstrap node series.540 # contextually fetch the bootstrap node series.
@@ -548,6 +554,14 @@
548 print('connecting to {}'.format(api_url))554 print('connecting to {}'.format(api_url))
549 env = app.connect(api_url, username, password)555 env = app.connect(api_url, username, password)
550556
557 if already_bootstrapped:
558 # Retrieve the environment type from the live environment: it may be
559 # different from the one declared on the environments.yaml file.
560 # Moreover, an imported jenv file could be in use, in which case the
561 # environment type is probably still unknown.
562 env_type = app.get_env_type(env)
563 print('environment type: {}'.format(env_type))
564
551 # Inspect the environment and deploy the charm if required.565 # Inspect the environment and deploy the charm if required.
552 charm_url, machine, service_data, unit_data = app.check_environment(566 charm_url, machine, service_data, unit_data = app.check_environment(
553 env, settings.JUJU_GUI_SERVICE_NAME, options.charm_url,567 env, settings.JUJU_GUI_SERVICE_NAME, options.charm_url,
554568
=== modified file 'quickstart/models/jenv.py'
--- quickstart/models/jenv.py 2014-12-16 11:10:10 +0000
+++ quickstart/models/jenv.py 2014-12-16 16:32:00 +0000
@@ -33,6 +33,8 @@
3333
34# Define the default Juju user when an environment is initially bootstrapped.34# Define the default Juju user when an environment is initially bootstrapped.
35JUJU_DEFAULT_USER = 'admin'35JUJU_DEFAULT_USER = 'admin'
36# Define an env type to use when the real type is not included in the jenv.
37UNKNOWN_ENV_TYPE = '__unknown__'
3638
3739
38def exists(env_name):40def exists(env_name):
@@ -78,7 +80,20 @@
78 """80 """
79 jenv_path = _get_jenv_path(env_name)81 jenv_path = _get_jenv_path(env_name)
80 data = serializers.yaml_load_from_path(jenv_path)82 data = serializers.yaml_load_from_path(jenv_path)
8183 try:
84 return _get_credentials(data)
85 except ValueError as err:
86 msg = b'cannot parse {}: {}'.format(jenv_path.encode('utf-8'), err)
87 raise ValueError(msg)
88
89
90def _get_credentials(data):
91 """Return the Juju environment credentials from the YAML decoded data.
92
93 Raise a ValueError if the credentials cannot be found.
94 See get_credentials for further information on how the credentials are
95 retrieved.
96 """
82 # Retrieve the user name.97 # Retrieve the user name.
83 try:98 try:
84 username = _get_value_from_yaml(data, 'user')99 username = _get_value_from_yaml(data, 'user')
@@ -86,9 +101,8 @@
86 # This is probably an old version of Juju not supporting multiple101 # This is probably an old version of Juju not supporting multiple
87 # users. Return the default user name.102 # users. Return the default user name.
88 logging.warn(103 logging.warn(
89 'cannot retrieve the user name from {}: {}: '104 b'cannot retrieve the user name: {}: '
90 'falling back to the default user name' .format(105 b'falling back to the default user name'.format(err))
91 jenv_path, bytes(err).decode('utf-8')))
92 username = JUJU_DEFAULT_USER106 username = JUJU_DEFAULT_USER
93107
94 # Retrieve the password.108 # Retrieve the password.
@@ -97,9 +111,8 @@
97 except ValueError as err:111 except ValueError as err:
98 # This is probably an old version of Juju not supporting multiple112 # This is probably an old version of Juju not supporting multiple
99 # users. Fall back to the admin secret.113 # users. Fall back to the admin secret.
100 msg = b'cannot retrieve the password from {}: '.format(114 msg = b'cannot retrieve the password: '
101 jenv_path.encode('utf-8'))115 logging.debug(msg + bytes(err) + ': trying with the admin-secret')
102 logging.debug(msg + bytes(err))
103 try:116 try:
104 password = _get_value_from_yaml(117 password = _get_value_from_yaml(
105 data, 'bootstrap-config', 'admin-secret')118 data, 'bootstrap-config', 'admin-secret')
@@ -109,6 +122,71 @@
109 return username, password122 return username, password
110123
111124
125def get_env_db():
126 """Return an environment database parsing the existing jenv files.
127
128 The returned db is similar to what is returned by models.envs.load().
129
130 When a jenv file is created using "juju user add", the resulting YAML data
131 is very concise, not even including the environment type.
132 For this reason, the environment database returned by this function does
133 not contain the usual fields used as bootstrap options, but just the
134 environment name and the type. If the environment type is not included in
135 the jenv, UNKNOWN_ENV_TYPE is used.
136 """
137 db = {'environments': {}}
138 path = os.path.expanduser(os.path.join(settings.JUJU_HOME, 'environments'))
139 # Check if the Juju home is already configured.
140 if not os.path.isdir(path):
141 logging.debug('environments directory not found in the Juju home')
142 return db
143 # Collect the environments.
144 environments = db['environments']
145 for filename in os.listdir(path):
146 fullpath = os.path.join(path, filename)
147 # Check that the current file is a jenv file.
148 if not os.path.isfile(fullpath):
149 continue
150 name, ext = os.path.splitext(filename)
151 if ext != '.jenv':
152 continue
153 # Validate the jenv contents.
154 try:
155 data = serializers.yaml_load_from_path(fullpath)
156 validate(data)
157 except ValueError as err:
158 logging.warn('ignoring invalid jenv file {}: {}'. format(
159 filename, bytes(err).decode('utf-8')))
160 continue
161 # Try to retrieve the environment type from the jenv data.
162 try:
163 env_type = _get_value_from_yaml(data, 'bootstrap-config', 'type')
164 except ValueError:
165 # This is expected when a jenv is generated with "juju user add".
166 env_type = UNKNOWN_ENV_TYPE
167 environments[name] = {'type': env_type}
168 return db
169
170
171def validate(data):
172 """Validate the given YAML decoded jenv data.
173
174 This is a weak validation: from the quickstart point of view a jenv file is
175 valid if it includes the juju environment credentials and the API servers.
176
177 Raise a ValueError if:
178 - the environment file is not found;
179 - the environment file contents are not parsable by YAML;
180 - the environment data is not valid.
181 """
182 _get_credentials(data)
183 servers = data.get('state-servers')
184 if not isinstance(servers, (list, tuple)):
185 raise ValueError(b'invalid state-servers field')
186 if not len(servers):
187 raise ValueError(b'no state-servers found')
188
189
112def _get_value_from_yaml(data, *args):190def _get_value_from_yaml(data, *args):
113 """Read and return a value from the given YAML decoded data.191 """Read and return a value from the given YAML decoded data.
114192
115193
=== modified file 'quickstart/tests/helpers.py'
--- quickstart/tests/helpers.py 2014-11-12 12:21:09 +0000
+++ quickstart/tests/helpers.py 2014-12-16 16:32:00 +0000
@@ -142,6 +142,7 @@
142142
143 jenv_data = {143 jenv_data = {
144 'user': 'admin',144 'user': 'admin',
145 'password': 'Secret!',
145 'state-servers': ['localhost:17070', '10.0.3.1:17070'],146 'state-servers': ['localhost:17070', '10.0.3.1:17070'],
146 'bootstrap-config': {147 'bootstrap-config': {
147 'admin-secret': 'Secret!',148 'admin-secret': 'Secret!',
@@ -151,6 +152,24 @@
151 'life': {'universe': {'everything': 42}},152 'life': {'universe': {'everything': 42}},
152 }153 }
153154
155 def _make_playground(self):
156 """Create and return a mock Juju home."""
157 playground = tempfile.mkdtemp()
158 self.addCleanup(shutil.rmtree, playground)
159 os.mkdir(os.path.join(playground, 'environments'))
160 return playground
161
162 def write_jenv(self, juju_home, name, contents):
163 """Create a jenv file in the given Juju home.
164
165 Use the given name and YAML encoded contents to create the jenv.
166 Return the resulting jenv path.
167 """
168 path = os.path.join(juju_home, 'environments', '{}.jenv'.format(name))
169 with open(path, 'w') as jenv_file:
170 jenv_file.write(contents)
171 return path
172
154 @contextmanager173 @contextmanager
155 def make_jenv(self, env_name, contents):174 def make_jenv(self, env_name, contents):
156 """Create a temporary jenv file with the given env_name and contents.175 """Create a temporary jenv file with the given env_name and contents.
@@ -160,19 +179,30 @@
160179
161 Return the jenv file path.180 Return the jenv file path.
162 """181 """
163 # Create a temporary JUJU_HOME.182 playground = self._make_playground()
164 playground = tempfile.mkdtemp()183 jenv_path = self.write_jenv(playground, env_name, contents)
165 self.addCleanup(shutil.rmtree, playground)
166 environments_dir = os.path.join(playground, 'environments')
167 os.mkdir(environments_dir)
168 # Create the jenv file inside the temporary JUJU_HOME.
169 jenv_path = os.path.join(environments_dir, '{}.jenv'.format(env_name))
170 with open(jenv_path, 'w') as jenv_file:
171 jenv_file.write(contents)
172 # Patch the JUJU_HOME and return the jenv file path.184 # Patch the JUJU_HOME and return the jenv file path.
173 with mock.patch('quickstart.settings.JUJU_HOME', playground):185 with mock.patch('quickstart.settings.JUJU_HOME', playground):
174 yield jenv_path186 yield jenv_path
175187
188 @contextmanager
189 def make_multiple_jenvs(self, data):
190 """Create multiple temporary jenv files with the data.
191
192 Data is a dict mapping env names to jenv contents.
193
194 In the context manager block, the JUJU_HOME is set to the ancestor
195 of the generated temporary files.
196
197 Return the Juju home path.
198 """
199 playground = self._make_playground()
200 for name, contents in data.items():
201 self.write_jenv(playground, name, contents)
202 # Patch the JUJU_HOME and return the jenv file path.
203 with mock.patch('quickstart.settings.JUJU_HOME', playground):
204 yield playground
205
176206
177def make_env_db(default=None, exclude_invalid=False):207def make_env_db(default=None, exclude_invalid=False):
178 """Create and return an env_db.208 """Create and return an env_db.
@@ -229,6 +259,16 @@
229 return env_db259 return env_db
230260
231261
262def make_jenv_db():
263 """Create and return a jenv files database."""
264 environments = {
265 'ec2-west': {'type': '__unknown__'},
266 'lxc': {'type': 'local'},
267 'test-jenv': {'type': '__unknown__'},
268 }
269 return {'environments': environments}
270
271
232# Mock the builtin print function.272# Mock the builtin print function.
233mock_print = mock.patch('__builtin__.print')273mock_print = mock.patch('__builtin__.print')
234274
235275
=== modified file 'quickstart/tests/models/test_jenv.py'
--- quickstart/tests/models/test_jenv.py 2014-12-16 11:10:10 +0000
+++ quickstart/tests/models/test_jenv.py 2014-12-16 16:32:00 +0000
@@ -18,8 +18,10 @@
1818
19from __future__ import unicode_literals19from __future__ import unicode_literals
2020
21import os
21import unittest22import unittest
2223
24import mock
23import yaml25import yaml
2426
25from quickstart.models import jenv27from quickstart.models import jenv
@@ -126,12 +128,12 @@
126 # The default user name is returned if it's not possible to retrieve it128 # The default user name is returned if it's not possible to retrieve it
127 # otherwise.129 # otherwise.
128 data = {'password': 'Secret!'}130 data = {'password': 'Secret!'}
129 with self.make_jenv('local', yaml.safe_dump(data)) as path:131 with self.make_jenv('local', yaml.safe_dump(data)):
130 expected_logs = [132 expected_logs = [
131 'cannot retrieve the user name from {}: '133 'cannot retrieve the user name: '
132 'invalid YAML contents: '134 'invalid YAML contents: '
133 'user key not found in the root section: '135 'user key not found in the root section: '
134 'falling back to the default user name'.format(path)]136 'falling back to the default user name']
135 with helpers.assert_logs(expected_logs, 'warn'):137 with helpers.assert_logs(expected_logs, 'warn'):
136 username, password = jenv.get_credentials('local')138 username, password = jenv.get_credentials('local')
137 self.assertEqual('admin', username)139 self.assertEqual('admin', username)
@@ -144,11 +146,12 @@
144 'user': 'who',146 'user': 'who',
145 'bootstrap-config': {'admin-secret': 'Admin!'},147 'bootstrap-config': {'admin-secret': 'Admin!'},
146 }148 }
147 with self.make_jenv('local', yaml.safe_dump(data)) as path:149 with self.make_jenv('local', yaml.safe_dump(data)):
148 expected_logs = [150 expected_logs = [
149 'cannot retrieve the password from {}: '151 'cannot retrieve the password: '
150 'invalid YAML contents: '152 'invalid YAML contents: '
151 'password key not found in the root section'.format(path)]153 'password key not found in the root section: '
154 'trying with the admin-secret']
152 with helpers.assert_logs(expected_logs, 'debug'):155 with helpers.assert_logs(expected_logs, 'debug'):
153 username, password = jenv.get_credentials('local')156 username, password = jenv.get_credentials('local')
154 self.assertEqual('Admin!', password)157 self.assertEqual('Admin!', password)
@@ -157,8 +160,139 @@
157 # A ValueError is raised if the password cannot be found anywhere.160 # A ValueError is raised if the password cannot be found anywhere.
158 with self.make_jenv('local', yaml.safe_dump({})) as path:161 with self.make_jenv('local', yaml.safe_dump({})) as path:
159 expected_error = (162 expected_error = (
160 'cannot retrieve the password from {}: '163 'cannot parse {}: '
164 'cannot retrieve the password: '
161 'invalid YAML contents: bootstrap-config key '165 'invalid YAML contents: bootstrap-config key '
162 'not found in the root section'.format(path))166 'not found in the root section'.format(path))
163 with self.assert_value_error(expected_error):167 with self.assert_value_error(expected_error):
164 jenv.get_credentials('local')168 jenv.get_credentials('local')
169
170
171class TestGetEnvDb(helpers.JenvFileTestsMixin, unittest.TestCase):
172
173 def test_no_juju_home(self):
174 # An empty db is returned if the Juju home is not set up.
175 with mock.patch('quickstart.settings.JUJU_HOME', '/no/such/dir'):
176 jenv_db = jenv.get_env_db()
177 self.assertEqual({'environments': {}}, jenv_db)
178
179 def test_no_jenv_files(self):
180 # An empty db is returned if there are no jenv files.
181 with self.make_multiple_jenvs({}):
182 jenv_db = jenv.get_env_db()
183 self.assertEqual({'environments': {}}, jenv_db)
184
185 def test_no_valid_jenv_files(self):
186 # An empty db is returned if there are no valid jenv files.
187 with self.make_jenv('local', yaml.safe_dump({})):
188 expected_logs = [
189 'ignoring invalid jenv file local.jenv: '
190 'cannot retrieve the password: invalid YAML contents: '
191 'bootstrap-config key not found in the root section']
192 with helpers.assert_logs(expected_logs, 'warn'):
193 jenv_db = jenv.get_env_db()
194 self.assertEqual({'environments': {}}, jenv_db)
195
196 def test_single_valid_jenv(self):
197 # Only the valid environments are returned.
198 with self.make_multiple_jenvs({
199 'local': yaml.safe_dump({}),
200 'ec2': yaml.safe_dump(self.jenv_data),
201 }):
202 jenv_db = jenv.get_env_db()
203 self.assertEqual({
204 'environments': {'ec2': {'type': 'ec2'}},
205 }, jenv_db)
206
207 def test_multiple_jenv_files(self):
208 # Multiple environments are correctly returned.
209 jenv_data1 = {
210 'user': 'admin',
211 'password': 'Secret!',
212 'state-servers': ['localhost:17070'],
213 'bootstrap-config': {'type': 'hp'},
214 }
215 jenv_data2 = {
216 'user': 'admin',
217 'password': 'Secret!',
218 'state-servers': ['localhost:17070'],
219 'bootstrap-config': {'type': 'maas'},
220 }
221 with self.make_multiple_jenvs({
222 'hp': yaml.safe_dump(jenv_data1),
223 'maas': yaml.safe_dump(jenv_data2),
224 }):
225 jenv_db = jenv.get_env_db()
226 self.assertEqual({
227 'environments': {
228 'hp': {'type': 'hp'},
229 'maas': {'type': 'maas'},
230 },
231 }, jenv_db)
232
233 def test_unknown_env_type(self):
234 # If the jenv file does not include the env type, jenv.UNKNOWN_ENV_TYPE
235 # is used as the environment type.
236 jenv_data = {
237 'user': 'admin',
238 'password': 'Secret!',
239 'state-servers': ['localhost:17070'],
240 }
241 with self.make_jenv('local', yaml.safe_dump(jenv_data)):
242 jenv_db = jenv.get_env_db()
243 self.assertEqual({
244 'environments': {'local': {'type': jenv.UNKNOWN_ENV_TYPE}},
245 }, jenv_db)
246
247 def test_extraneous_files(self):
248 # Extraneous files are ignored.
249 with self.make_multiple_jenvs({}) as juju_home:
250 envs_dir = os.path.join(juju_home, 'environments')
251 os.mkdir(os.path.join(envs_dir, 'local.jenv'))
252 with open(os.path.join(envs_dir, 'not-a-jenv'), 'w') as stream:
253 stream.write('bad wolf')
254 jenv_db = jenv.get_env_db()
255 self.assertEqual({'environments': {}}, jenv_db)
256
257
258class TestValidate(
259 helpers.JenvFileTestsMixin, helpers.ValueErrorTestsMixin,
260 unittest.TestCase):
261
262 def test_validation_success(self):
263 # A valid jenv file is successfully validated.
264 jenv.validate(self.jenv_data)
265
266 def test_invalid_credentials(self):
267 # A ValueError is raised if the credentials cannot be retrieved.
268 expected_error = (
269 'cannot retrieve the password: invalid YAML contents: '
270 'bootstrap-config key not found in the root section')
271 with self.assert_value_error(expected_error):
272 jenv.validate({})
273
274 def test_missing_state_servers(self):
275 # A ValueError is raised if the Juju state servers cannot be retrieved.
276 with self.assert_value_error('invalid state-servers field'):
277 jenv.validate({
278 'user': 'admin',
279 'password': 'Secret!',
280 })
281
282 def test_invalid_state_servers(self):
283 # A ValueError is raised if the Juju servers have an invalid type.
284 with self.assert_value_error('invalid state-servers field'):
285 jenv.validate({
286 'user': 'admin',
287 'password': 'Secret!',
288 'state-servers': 'NO!',
289 })
290
291 def test_no_state_servers(self):
292 # A ValueError is raised if the state server list is empty.
293 with self.assert_value_error('no state-servers found'):
294 jenv.validate({
295 'user': 'admin',
296 'password': 'Secret!',
297 'state-servers': [],
298 })
165299
=== modified file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 2014-12-16 11:10:10 +0000
+++ quickstart/tests/test_app.py 2014-12-16 16:32:00 +0000
@@ -700,26 +700,6 @@
700 mock_call.assert_has_calls(expected_calls)700 mock_call.assert_has_calls(expected_calls)
701701
702702
703class TestGetEnvType(
704 helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
705
706 def test_success(self):
707 # The environment type is successfully retrieved.
708 with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)):
709 env_type = app.get_env_type('ec2')
710 self.assertEqual('ec2', env_type)
711
712 def test_error(self):
713 # A ProgramExit is raised if the environment type cannot be retrieved.
714 with self.make_jenv('aws', '') as path:
715 expected_error = (
716 'cannot retrieve environment type: cannot read {}: invalid '
717 'YAML contents: bootstrap-config key not found in the root '
718 'section'.format(path))
719 with self.assert_program_exit(expected_error):
720 app.get_env_type('aws')
721
722
723class TestGetCredentials(703class TestGetCredentials(
724 helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):704 helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
725705
@@ -734,9 +714,10 @@
734 # A ProgramExit is raised if the credentials cannot be retrieved.714 # A ProgramExit is raised if the credentials cannot be retrieved.
735 with self.make_jenv('ec2', '') as path:715 with self.make_jenv('ec2', '') as path:
736 expected_error = (716 expected_error = (
737 'cannot retrieve environment credentials: cannot retrieve the '717 'cannot retrieve environment credentials: cannot parse {}: '
738 'password from {}: invalid YAML contents: bootstrap-config '718 'cannot retrieve the password: invalid YAML contents: '
739 'key not found in the root section'.format(path))719 'bootstrap-config key not found in the root section'
720 ''.format(path))
740 with self.assert_program_exit(expected_error):721 with self.assert_program_exit(expected_error):
741 app.get_credentials('ec2')722 app.get_credentials('ec2')
742723
@@ -842,6 +823,24 @@
842 self.assertIs(error, context_manager.exception)823 self.assertIs(error, context_manager.exception)
843824
844825
826class TestGetEnvType(ProgramExitTestsMixin, unittest.TestCase):
827
828 def test_success(self):
829 # The environment type is successfully retrieved.
830 env = mock.Mock()
831 env.info.return_value = {'ProviderType': 'ec2'}
832 env_type = app.get_env_type(env)
833 self.assertEqual('ec2', env_type)
834
835 def test_error(self):
836 # A ProgramExit is raised if the environment type cannot be retrieved.
837 env = mock.Mock()
838 env.info.side_effect = self.make_env_error('bad wolf')
839 expected_error = 'cannot retrieve the environment type: bad wolf'
840 with self.assert_program_exit(expected_error):
841 app.get_env_type(env)
842
843
845class TestCreateAuthToken(unittest.TestCase):844class TestCreateAuthToken(unittest.TestCase):
846845
847 def test_success(self):846 def test_success(self):
848847
=== modified file 'quickstart/tests/test_juju.py'
--- quickstart/tests/test_juju.py 2014-12-16 11:10:10 +0000
+++ quickstart/tests/test_juju.py 2014-12-16 16:32:00 +0000
@@ -277,6 +277,15 @@
277 mock_rpc.assert_called_once_with(expected)277 mock_rpc.assert_called_once_with(expected)
278278
279 @patch_rpc279 @patch_rpc
280 def test_info(self, mock_rpc):
281 # The EnvironmentInfo API call is properly generated.
282 self.env.info()
283 mock_rpc.assert_called_once_with({
284 'Type': 'Client',
285 'Request': 'EnvironmentInfo',
286 })
287
288 @patch_rpc
280 def test_create_auth_token(self, mock_rpc):289 def test_create_auth_token(self, mock_rpc):
281 self.env.create_auth_token()290 self.env.create_auth_token()
282 expected = dict(Type='GUIToken', Request='Create')291 expected = dict(Type='GUIToken', Request='Create')
283292
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2014-12-15 17:31:23 +0000
+++ quickstart/tests/test_manage.py 2014-12-16 16:32:00 +0000
@@ -428,26 +428,37 @@
428 self.parser = mock.Mock()428 self.parser = mock.Mock()
429 self.env_type_db = envs.get_env_type_db()429 self.env_type_db = envs.get_env_type_db()
430 self.env_db = helpers.make_env_db()430 self.env_db = helpers.make_env_db()
431 self.jenv_db = helpers.make_jenv_db()
431432
432 def test_resulting_env_data(self):433 def test_resulting_env_data(self):
433 # The env_data is correctly validated and returned.434 # The env_data is correctly validated and returned.
434 expected_env_data = envs.get_env_data(self.env_db, 'lxc')435 expected_env_data = envs.get_env_data(self.env_db, 'lxc')
435 env_data = manage._retrieve_env_data(436 env_data = manage._retrieve_env_data(
436 self.parser, self.env_type_db, self.env_db, 'lxc')437 self.parser, self.env_type_db, self.env_db, self.jenv_db, 'lxc')
438 self.assertEqual(expected_env_data, env_data)
439
440 def test_jenv_data(self):
441 # The env_data is correctly retrieved from the jenv database.
442 expected_env_data = envs.get_env_data(self.jenv_db, 'test-jenv')
443 env_data = manage._retrieve_env_data(
444 self.parser, self.env_type_db, self.env_db, self.jenv_db,
445 'test-jenv')
437 self.assertEqual(expected_env_data, env_data)446 self.assertEqual(expected_env_data, env_data)
438447
439 def test_error_environment_not_found(self):448 def test_error_environment_not_found(self):
440 # A parser error is invoked if the provided environment is not included449 # A parser error is invoked if the provided environment is not included
441 # in the environments database.450 # in the environments database.
442 manage._retrieve_env_data(451 manage._retrieve_env_data(
443 self.parser, self.env_type_db, self.env_db, 'no-such')452 self.parser, self.env_type_db, self.env_db, self.jenv_db,
453 'no-such')
444 self.parser.error.assert_called_once_with(454 self.parser.error.assert_called_once_with(
445 'environment no-such not found')455 'environment no-such not found')
446456
447 def test_error_environment_not_valid(self):457 def test_error_environment_not_valid(self):
448 # A parser error is invoked if the selected environment is not valid.458 # A parser error is invoked if the selected environment is not valid.
449 manage._retrieve_env_data(459 manage._retrieve_env_data(
450 self.parser, self.env_type_db, self.env_db, 'local-with-errors')460 self.parser, self.env_type_db, self.env_db, self.jenv_db,
461 'local-with-errors')
451 self.parser.error.assert_called_once_with(462 self.parser.error.assert_called_once_with(
452 'cannot use the local-with-errors environment:\n'463 'cannot use the local-with-errors environment:\n'
453 'the storage port field requires an integer value')464 'the storage port field requires an integer value')
@@ -860,13 +871,14 @@
860871
861 def test_already_bootstrapped(self, mock_app, mock_open):872 def test_already_bootstrapped(self, mock_app, mock_open):
862 # The application correctly reuses an already bootstrapped environment.873 # The application correctly reuses an already bootstrapped environment.
863 self.configure_app(mock_app, check_bootstrapped='wss://example.com')874 env = self.configure_app(
875 mock_app, check_bootstrapped='wss://example.com')
864 # Run the application.876 # Run the application.
865 options = self.make_options()877 options = self.make_options()
866 with self.patch_get_juju_command():878 with self.patch_get_juju_command():
867 manage.run(options)879 manage.run(options)
868 # The environment type is retrieved from the jenv.880 # The environment type is retrieved from the jenv.
869 mock_app.get_env_type.assert_called_once_with(options.env_name)881 mock_app.get_env_type.assert_called_once_with(env)
870 # No reason to call bootstrap or get_api_url functions.882 # No reason to call bootstrap or get_api_url functions.
871 self.assertFalse(mock_app.bootstrap.called)883 self.assertFalse(mock_app.bootstrap.called)
872 self.assertFalse(mock_app.get_api_url.called)884 self.assertFalse(mock_app.get_api_url.called)
@@ -875,7 +887,7 @@
875 # The application correctly reuses an already bootstrapped environment.887 # The application correctly reuses an already bootstrapped environment.
876 # In this case, the environment seems not bootstrapped at first, but888 # In this case, the environment seems not bootstrapped at first, but
877 # it ended up being up and running later.889 # it ended up being up and running later.
878 self.configure_app(mock_app, bootstrap=True)890 env = self.configure_app(mock_app, bootstrap=True)
879 # Run the application.891 # Run the application.
880 options = self.make_options()892 options = self.make_options()
881 with self.patch_get_juju_command():893 with self.patch_get_juju_command():
@@ -891,7 +903,7 @@
891 constraints=options.constraints)903 constraints=options.constraints)
892 mock_app.get_api_url.assert_called_once_with(904 mock_app.get_api_url.assert_called_once_with(
893 options.env_name, self.juju_command)905 options.env_name, self.juju_command)
894 mock_app.get_env_type.assert_called_once_with(options.env_name)906 mock_app.get_env_type.assert_called_once_with(env)
895907
896 def test_no_token(self, mock_app, mock_open):908 def test_no_token(self, mock_app, mock_open):
897 # The process continues even if the authentication token cannot be909 # The process continues even if the authentication token cannot be

Subscribers

People subscribed via source and target branches