Merge lp:~frankban/juju-quickstart/code-reorg into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 104
Proposed branch: lp:~frankban/juju-quickstart/code-reorg
Merge into: lp:juju-quickstart
Diff against target: 849 lines (+370/-222)
12 files modified
HACKING.rst (+55/-1)
quickstart/app.py (+10/-21)
quickstart/manage.py (+3/-15)
quickstart/models/envs.py (+1/-30)
quickstart/models/jenv.py (+71/-0)
quickstart/serializers.py (+20/-0)
quickstart/tests/helpers.py (+36/-0)
quickstart/tests/models/test_envs.py (+0/-76)
quickstart/tests/models/test_jenv.py (+110/-0)
quickstart/tests/test_app.py (+16/-18)
quickstart/tests/test_manage.py (+7/-61)
quickstart/tests/test_serializers.py (+41/-0)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/code-reorg
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+241416@code.launchpad.net

Description of the change

Code reorganization + docs.

Reorganize the jenv related code in preparation of the
bootstrap strategy change.

Separate jenv handling from the environments.yaml file
management in models.

As a consequence of only supporting juju >= 1.18 we
can now safely always retrieve the admin-secret from
the jenv file.

Also updated the HACKING docs to include a brief
description of the project structure.

Tests: `make check`.

QA: use quickstart as usual.

https://codereview.appspot.com/169380043/

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

Reviewers: mp+241416_code.launchpad.net,

Message:
Please take a look.

Description:
Code reorganization + docs.

Reorganize the jenv related code in preparation of the
bootstrap strategy change.

Separate jenv handling from the environments.yaml file
management in models.

As a consequence of only supporting juju >= 1.18 we
can now safely always retrieve the admin-secret from
the jenv file.

Also updated the HACKING docs to include a brief
description of the project structure.

Tests: `make check`.

QA: use quickstart as usual.

https://code.launchpad.net/~frankban/juju-quickstart/code-reorg/+merge/241416

(do not edit description out of merge proposal)

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

Affected files (+372, -222 lines):
   M HACKING.rst
   A [revision details]
   M quickstart/app.py
   M quickstart/manage.py
   M quickstart/models/envs.py
   A quickstart/models/jenv.py
   M quickstart/serializers.py
   M quickstart/tests/helpers.py
   M quickstart/tests/models/test_envs.py
   A quickstart/tests/models/test_jenv.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_manage.py
   M quickstart/tests/test_serializers.py

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

LGTM. Will QA now.

https://codereview.appspot.com/169380043/diff/1/HACKING.rst
File HACKING.rst (right):

https://codereview.appspot.com/169380043/diff/1/HACKING.rst#newcode78
HACKING.rst:78:
A very nice introduction to the code layout. Thanks for including it.

https://codereview.appspot.com/169380043/diff/1/quickstart/models/jenv.py
File quickstart/models/jenv.py (right):

https://codereview.appspot.com/169380043/diff/1/quickstart/models/jenv.py#newcode39
quickstart/models/jenv.py:39: def get_value(env_name, *args):
Why did you chose to use *args here instead of a tuple of key names?

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

https://codereview.appspot.com/169380043/diff/1/quickstart/tests/helpers.py#newcode156
quickstart/tests/helpers.py:156: In the contentx manager block, the
JUJU_HOME is set to the ancestor
typo: contextx

https://codereview.appspot.com/169380043/

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

Thanks for the update. I just have one forward looking question that
doesn't directly effect this branch but I'm curious about while looking
at this branch.

LGTM, no qa

https://codereview.appspot.com/169380043/diff/1/HACKING.rst
File HACKING.rst (right):

https://codereview.appspot.com/169380043/diff/1/HACKING.rst#newcode128
HACKING.rst:128: the code: go have a look!
nice ty

https://codereview.appspot.com/169380043/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/169380043/diff/1/quickstart/app.py#newcode263
quickstart/app.py:263: Parse the jenv file to retrieve the admin secret.
do you know if we can use the password field in the jenv for this? Next
week we hope to start updating the gui for the new username/password
work in juju 1.21 and I know we'll need to update quickstart to pull the
username/password from the jenv file.

https://codereview.appspot.com/169380043/diff/1/quickstart/models/envs.py
File quickstart/models/envs.py (left):

https://codereview.appspot.com/169380043/diff/1/quickstart/models/envs.py#oldcode202
quickstart/models/envs.py:202: def load_generated(env_file,
section='bootstrap-config'):
was this unused?

https://codereview.appspot.com/169380043/

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

Had some QA oddities but it looks OK.

https://codereview.appspot.com/169380043/

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

Thanks for the reviews!

https://codereview.appspot.com/169380043/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/169380043/diff/1/quickstart/app.py#newcode263
quickstart/app.py:263: Parse the jenv file to retrieve the admin secret.
On 2014/11/11 15:52:36, rharding wrote:
> do you know if we can use the password field in the jenv for this?
Next week we
> hope to start updating the gui for the new username/password work in
juju 1.21
> and I know we'll need to update quickstart to pull the
username/password from
> the jenv file.

With this change it should be easy to grab what we want from the jenv
with jenv.get_value(). We will want to use some fallback logic to
support backward compatibility.
If the user is != user-admin, we will need to update the one-show
automatic authentication with token as well in the guiserver and maybe
in the GUI itself.

https://codereview.appspot.com/169380043/diff/1/quickstart/models/envs.py
File quickstart/models/envs.py (left):

https://codereview.appspot.com/169380043/diff/1/quickstart/models/envs.py#oldcode202
quickstart/models/envs.py:202: def load_generated(env_file,
section='bootstrap-config'):
On 2014/11/11 15:52:36, rharding wrote:
> was this unused?

It was used, it is no longer required now.

https://codereview.appspot.com/169380043/diff/1/quickstart/models/jenv.py
File quickstart/models/jenv.py (right):

https://codereview.appspot.com/169380043/diff/1/quickstart/models/jenv.py#newcode39
quickstart/models/jenv.py:39: def get_value(env_name, *args):
On 2014/11/11 15:37:55, bac wrote:
> Why did you chose to use *args here instead of a tuple of key names?

No real reason actually, I started with just one single key as the
argument and then, after deciding to allow nested keys, it just felt
natural.

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

https://codereview.appspot.com/169380043/diff/1/quickstart/tests/helpers.py#newcode156
quickstart/tests/helpers.py:156: In the contentx manager block, the
JUJU_HOME is set to the ancestor
On 2014/11/11 15:37:55, bac wrote:
> typo: contextx

Done.

https://codereview.appspot.com/169380043/

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

*** Submitted:

Code reorganization + docs.

Reorganize the jenv related code in preparation of the
bootstrap strategy change.

Separate jenv handling from the environments.yaml file
management in models.

As a consequence of only supporting juju >= 1.18 we
can now safely always retrieve the admin-secret from
the jenv file.

Also updated the HACKING docs to include a brief
description of the project structure.

Tests: `make check`.

QA: use quickstart as usual.

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

https://codereview.appspot.com/169380043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'HACKING.rst'
--- HACKING.rst 2014-09-01 14:49:47 +0000
+++ HACKING.rst 2014-11-11 14:26:05 +0000
@@ -73,6 +73,60 @@
7373
74 $ .venv/bin/python juju-quickstart --help74 $ .venv/bin/python juju-quickstart --help
7575
76Project structure
77~~~~~~~~~~~~~~~~~
78
79Juju Quickstart is a Python project. Source files can be found in the
80``quickstart`` Python package included in this distribution.
81
82A brief description of the project structure follows, including the goals of
83each module in the ``quickstart`` package.
84
85* ``manage.py`` includes the main entry points for the ``juju-quickstart``
86 command. Specifically:
87 * ``manage.setup`` is used to set up the command line parser, the logging
88 infrastructure and the interactive session (if required);
89 * ``manage.run`` executes the application with the given options.
90
91* ``app.py`` defines the main application functions, like bootstrapping an
92 environment, deploying the Juju GUI or watching the deployment progress.
93 The ``app.ProgramExit`` error can only be raised by functions in this module,
94 and it causes the immediate exit (with an error) from the application.
95
96The ``manage.py`` and ``app.py`` modules must be considered ``juju-quickstart``
97execution specific: for this reason those modules are unlikely to be used as
98libraries. All the other packages/modules in the application (except for views,
99see below), should only include library-like code that can be reused in
100different places by either Juju Quickstart or external programs.
101
102* ``setting.py`` includes the application settings. Those must be considered as
103 constant values to be reused throughout the application. The settings module
104 should not import other ``quickstart`` modules.
105
106* ``utils.py`` defines general purpose helper functions: when writing such
107 utility objects it is likely that this is the right place where they should
108 live. Separate modules are created when a set of utilities are related each
109 other: this is the case, for instance, of ``serializers.py`` (YAML/JSON
110 serialization utilities), ``ssh.py`` (SSH protocol related functions),
111 ``platform_support.py`` etc.
112
113* ``juju.py`` defines the WebSocket API client used by Juju Quickstart to
114 connect to the Juju API server.
115
116* the ``models`` package includes a module for each application model. Models
117 are abstractions representing the data and information managed by Juju
118 Quickstart, e.g. environment files, jenv files or charms. In the
119 implementation of models, an effort has been made to use simple data
120 structures in order to represent entities/objects, and composable functions
121 to manipulate this information.
122
123* the ``cli`` package contains the command line interactive session handling.
124 Juju Quickstart uses Urwid to implement an ncurses-like interactive session:
125 Urwid code must only live in the ``cli`` package.
126
127That said, module docstrings often describe the goals and usage of each part of
128the code: go have a look!
129
76Pre-release QA130Pre-release QA
77~~~~~~~~~~~~~~131~~~~~~~~~~~~~~
78132
@@ -98,7 +152,7 @@
98152
99* Verify an environment that has already been bootstrapped is recogized and153* Verify an environment that has already been bootstrapped is recogized and
100 the GUI is deployed. This test also shows that a remote bundle is properly154 the GUI is deployed. This test also shows that a remote bundle is properly
101 deployed.::155 deployed::
102156
103 juju bootstrap -e local157 juju bootstrap -e local
104 juju quickstart -e local bundle:mediawiki/single158 juju quickstart -e local bundle:mediawiki/single
105159
=== modified file 'quickstart/app.py'
--- quickstart/app.py 2014-11-10 11:39:40 +0000
+++ quickstart/app.py 2014-11-11 14:26:05 +0000
@@ -23,7 +23,6 @@
2323
24import json24import json
25import logging25import logging
26import os
27import sys26import sys
28import time27import time
2928
@@ -37,7 +36,7 @@
37 utils,36 utils,
38 watchers,37 watchers,
39)38)
40from quickstart.models import envs39from quickstart.models import jenv
4140
4241
43class ProgramExit(Exception):42class ProgramExit(Exception):
@@ -258,27 +257,17 @@
258 raise ProgramExit('the state server is not ready:\n{}'.format(details))257 raise ProgramExit('the state server is not ready:\n{}'.format(details))
259258
260259
261def get_value_from_jenv(env_name, juju_home, key):260def get_admin_secret(env_name):
262 """Read the key from the generated environment file.261 """Return the Juju admin secret for the given environment name.
263262
264 At bootstrap, juju (v1.16 and later) writes a generated file263 Parse the jenv file to retrieve the admin secret.
265 located in JUJU_HOME. Return the value for the key.264 Raise a ProgramExit if the admin secret cannot be retrieved.
266
267 Raise a ValueError if:
268 - the environment file is not found;
269 - the environment file contents are not parsable by YAML;
270 - the YAML contents are not properly structured;
271 - the key is not found.
272 """265 """
273 filename = '{}.jenv'.format(env_name)
274 juju_env_file = os.path.expanduser(
275 os.path.join(juju_home, 'environments', filename))
276 jenv_db = envs.load_generated(juju_env_file)
277 try:266 try:
278 return jenv_db[key]267 return jenv.get_value(env_name, 'bootstrap-config', 'admin-secret')
279 except KeyError:268 except ValueError as err:
280 msg = '{} not found in {}'.format(key, juju_env_file)269 msg = b'cannot retrieve environment admin-secret: {}'.format(err)
281 raise ValueError(msg.encode('utf-8'))270 raise ProgramExit(msg)
282271
283272
284def get_api_url(env_name, juju_command):273def get_api_url(env_name, juju_command):
285274
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2014-11-10 11:31:02 +0000
+++ quickstart/manage.py 2014-11-11 14:26:05 +0000
@@ -293,7 +293,6 @@
293 'this host platform does not support local environments'293 'this host platform does not support local environments'
294 )294 )
295 # Update the options namespace with the new values.295 # Update the options namespace with the new values.
296 options.admin_secret = env_data.get('admin-secret')
297 options.env_file = env_file296 options.env_file = env_file
298 options.env_name = env_data['name']297 options.env_name = env_data['name']
299 options.default_series = env_data.get('default-series')298 options.default_series = env_data.get('default-series')
@@ -343,10 +342,9 @@
343 """Set up the application options and logger.342 """Set up the application options and logger.
344343
345 Return the options as a namespace containing the following attributes:344 Return the options as a namespace containing the following attributes:
346 - admin_secret: the password to use to access the Juju API or None if
347 no admin-secret is present in the $JUJU_HOME/environment.yaml file;
348 - bundle: the optional bundle (path or URL) to be deployed;345 - bundle: the optional bundle (path or URL) to be deployed;
349 - charm_url: the Juju GUI charm URL or None if not specified;346 - charm_url: the Juju GUI charm URL or None if not specified;
347 - constraints: the environment constrains or None if not set;
350 - debug: whether debug mode is activated;348 - debug: whether debug mode is activated;
351 - distro_only: install Juju only using the distribution packages;349 - distro_only: install Juju only using the distribution packages;
352 - env_file: the absolute path of the Juju environments.yaml file;350 - env_file: the absolute path of the Juju environments.yaml file;
@@ -357,8 +355,7 @@
357 - platform: The host platform;355 - platform: The host platform;
358 - upload_tools: whether to upload local version of tools;356 - upload_tools: whether to upload local version of tools;
359 - upload_series: the comma-separated series list for which tools will357 - upload_series: the comma-separated series list for which tools will
360 be uploaded, or None if not set;358 be uploaded, or None if not set.
361 - constraints: the environment constrains or None if not set.
362359
363 The following attributes will also be included in the namespace if a bundle360 The following attributes will also be included in the namespace if a bundle
364 deployment is requested:361 deployment is requested:
@@ -522,16 +519,7 @@
522 constraints=options.constraints)519 constraints=options.constraints)
523520
524 # Retrieve the admin-secret for the current environment.521 # Retrieve the admin-secret for the current environment.
525 try:522 admin_secret = app.get_admin_secret(options.env_name)
526 admin_secret = app.get_value_from_jenv(
527 options.env_name, settings.JUJU_HOME, 'admin-secret')
528 except ValueError as err:
529 admin_secret = options.admin_secret
530 if admin_secret is None:
531 # The admin-secret cannot be found in the jenv file and is not
532 # explicitly specified in the environments.yaml file.
533 msg = b'{} or {}'.format(err, options.env_file.encode('utf-8'))
534 raise app.ProgramExit(msg)
535523
536 print('retrieving the Juju API address')524 print('retrieving the Juju API address')
537 api_url = app.get_api_url(options.env_name, juju_command)525 api_url = app.get_api_url(options.env_name, juju_command)
538526
=== modified file 'quickstart/models/envs.py'
--- quickstart/models/envs.py 2014-10-09 13:14:30 +0000
+++ quickstart/models/envs.py 2014-11-11 14:26:05 +0000
@@ -134,23 +134,6 @@
134 return {'environments': {}}134 return {'environments': {}}
135135
136136
137def _load_file(env_file):
138 """Load the given file and return the YAML-parsed contents."""
139 # Load the Juju environments file.
140 try:
141 environments_file = open(env_file.encode('utf-8'))
142 except IOError as err:
143 msg = b'unable to open environments file: {}'.format(err)
144 raise ValueError(msg)
145 # Parse the Juju environments file.
146 try:
147 contents = serializers.yaml_load(environments_file)
148 except Exception as err:
149 msg = b'unable to parse environments file {}: {}'
150 raise ValueError(msg.format(env_file.encode('utf-8'), err))
151 return contents
152
153
154def load(env_file):137def load(env_file):
155 """Load and parse the provided Juju environments.yaml file.138 """Load and parse the provided Juju environments.yaml file.
156139
@@ -174,7 +157,7 @@
174 - the environment file contents are not parsable by YAML;157 - the environment file contents are not parsable by YAML;
175 - the YAML contents are not properly structured.158 - the YAML contents are not properly structured.
176 """159 """
177 contents = _load_file(env_file)160 contents = serializers.yaml_load_from_path(env_file)
178 if contents is None:161 if contents is None:
179 return create_empty_env_db()162 return create_empty_env_db()
180 # Retrieve the environment list.163 # Retrieve the environment list.
@@ -199,18 +182,6 @@
199 return env_db182 return env_db
200183
201184
202def load_generated(env_file, section='bootstrap-config'):
203 """Given the path to a YAML file, load the file and return the section."""
204 contents = _load_file(env_file)
205 try:
206 section_contents = contents[section]
207 except (KeyError, TypeError):
208 msg = 'invalid YAML contents in {}: {}'.format(env_file, contents)
209 raise ValueError(msg.encode('utf-8'))
210
211 return section_contents
212
213
214def save(env_file, env_db, backup_function=None):185def save(env_file, env_db, backup_function=None):
215 """Save the given env_db to the provided environments.yaml file.186 """Save the given env_db to the provided environments.yaml file.
216187
217188
=== added file 'quickstart/models/jenv.py'
--- quickstart/models/jenv.py 1970-01-01 00:00:00 +0000
+++ quickstart/models/jenv.py 2014-11-11 14:26:05 +0000
@@ -0,0 +1,71 @@
1# This file is part of the Juju Quickstart Plugin, which lets users set up a
2# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
3# Copyright (C) 2014 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 jenv generated files handling.
18
19At bootstrap, Juju writes a generated file (jenv) located in JUJU_HOME.
20This module includes functions to load and parse those jenv file contents.
21"""
22
23from __future__ import unicode_literals
24
25import os
26
27from quickstart import (
28 serializers,
29 settings,
30)
31
32
33def exists(env_name):
34 """Report whether the jenv generated file exists for the given env_name."""
35 jenv_path = _get_jenv_path(env_name)
36 return os.path.isfile(jenv_path)
37
38
39def get_value(env_name, *args):
40 """Read a value from the Juju generated environment file (jenv).
41
42 Return the value corresponding to the section specified in args.
43 For instance, calling get_value('ec2', 'bootstrap-config', 'admin-secret')
44 returns the value associated with the "admin-secret" key included on the
45 "bootstrap-config" section of the jenv file.
46
47 Raise a ValueError if:
48 - the environment file is not found;
49 - the environment file contents are not parsable by YAML;
50 - the YAML contents are not properly structured;
51 - one the keys in args is not found.
52 """
53 jenv_path = _get_jenv_path(env_name)
54 data = serializers.yaml_load_from_path(jenv_path)
55 section = 'root'
56 for key in args:
57 try:
58 data = data[key]
59 except (KeyError, TypeError):
60 msg = ('invalid YAML contents in {}: ''{} key not found in the {} '
61 'section'.format(jenv_path, key, section))
62 raise ValueError(msg.encode('utf-8'))
63 section = key
64 return data
65
66
67def _get_jenv_path(env_name):
68 """Return the path to the generated jenv file for the given env_name."""
69 filename = '{}.jenv'.format(env_name)
70 path = os.path.join(settings.JUJU_HOME, 'environments', filename)
71 return os.path.expanduser(path)
072
=== modified file 'quickstart/serializers.py'
--- quickstart/serializers.py 2013-12-06 17:17:19 +0000
+++ quickstart/serializers.py 2014-11-11 14:26:05 +0000
@@ -45,3 +45,23 @@
45 Always serialize collections in the block style.45 Always serialize collections in the block style.
46 """46 """
47 return yaml.safe_dump(data, stream, default_flow_style=False)47 return yaml.safe_dump(data, stream, default_flow_style=False)
48
49
50def yaml_load_from_path(path):
51 """Load the given file path and return the YAML-parsed contents.
52
53 Raise a ValueError if the file cannot be opened or parsed.
54 """
55 filename = path.encode('utf-8')
56 try:
57 yaml_file = open(filename)
58 except IOError as err:
59 msg = b'unable to open file {}: {}'.format(filename, err)
60 raise ValueError(msg)
61 # Parse the Juju environments file.
62 try:
63 contents = yaml_load(yaml_file)
64 except Exception as err:
65 msg = b'unable to parse file {}: {}'.format(filename, err)
66 raise ValueError(msg)
67 return contents
4868
=== modified file 'quickstart/tests/helpers.py'
--- quickstart/tests/helpers.py 2014-11-10 09:08:44 +0000
+++ quickstart/tests/helpers.py 2014-11-11 14:26:05 +0000
@@ -136,6 +136,42 @@
136 return env_file.name136 return env_file.name
137137
138138
139class JenvFileTestsMixin(object):
140 """Shared methods for testing Juju generated environment files (jenv)."""
141
142 jenv_data = {
143 'user': 'admin',
144 'state-servers': ['localhost:17070', '10.0.3.1:17070'],
145 'bootstrap-config': {
146 'admin-secret': 'Secret!',
147 'api-port': 17070,
148 },
149 'life': {'universe': {'everything': 42}},
150 }
151
152 @contextmanager
153 def make_jenv(self, env_name, contents):
154 """Create a temporary jenv file with the given env_name and contents.
155
156 In the contentx manager block, the JUJU_HOME is set to the ancestor
157 of the generated temporary file.
158
159 Return the jenv file path.
160 """
161 # Create a temporary JUJU_HOME.
162 playground = tempfile.mkdtemp()
163 self.addCleanup(shutil.rmtree, playground)
164 environments_dir = os.path.join(playground, 'environments')
165 os.mkdir(environments_dir)
166 # Create the jenv file inside the temporary JUJU_HOME.
167 jenv_path = os.path.join(environments_dir, '{}.jenv'.format(env_name))
168 with open(jenv_path, 'w') as jenv_file:
169 jenv_file.write(contents)
170 # Patch the JUJU_HOME and return the jenv file path.
171 with mock.patch('quickstart.settings.JUJU_HOME', playground):
172 yield jenv_path
173
174
139def make_env_db(default=None, exclude_invalid=False):175def make_env_db(default=None, exclude_invalid=False):
140 """Create and return an env_db.176 """Create and return an env_db.
141177
142178
=== modified file 'quickstart/tests/models/test_envs.py'
--- quickstart/tests/models/test_envs.py 2014-10-09 15:29:46 +0000
+++ quickstart/tests/models/test_envs.py 2014-11-11 14:26:05 +0000
@@ -99,28 +99,6 @@
99 self.assertEqual({'environments': {}}, env_db)99 self.assertEqual({'environments': {}}, env_db)
100100
101101
102class TestLoadFile(
103 helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin,
104 unittest.TestCase):
105
106 def test_no_file(self):
107 # A ValueError is raised if the environments file is not found.
108 expected = (
109 "unable to open environments file: "
110 "[Errno 2] No such file or directory: '/no/such/file.yaml'"
111 )
112 with self.assert_value_error(expected):
113 envs._load_file('/no/such/file.yaml')
114
115 def test_invalid_yaml(self):
116 # A ValueError is raised if the environments file is not a valid YAML.
117 env_file = self.make_env_file(':')
118 with self.assertRaises(ValueError) as context_manager:
119 envs._load_file(env_file)
120 expected = 'unable to parse environments file {}'.format(env_file)
121 self.assertIn(expected, bytes(context_manager.exception))
122
123
124class TestLoad(102class TestLoad(
125 helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin,103 helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin,
126 unittest.TestCase):104 unittest.TestCase):
@@ -205,60 +183,6 @@
205 self.assertEqual(expected, env_db)183 self.assertEqual(expected, env_db)
206184
207185
208class TestLoadGenerated(
209 helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin,
210 unittest.TestCase):
211
212 def test_empty_file(self):
213 # A ValueError is raised if the environments file is empty.
214 env_file = self.make_env_file('')
215 expected = 'invalid YAML contents in {}: None'.format(env_file)
216 with self.assert_value_error(expected):
217 envs.load_generated(env_file)
218
219 def test_invalid_yaml_contents(self):
220 # A ValueError is raised if the environments file is not well formed.
221 env_file = self.make_env_file('a-string')
222 expected = 'invalid YAML contents in {}: a-string'.format(env_file)
223 with self.assert_value_error(expected):
224 envs.load_generated(env_file)
225
226 def test_section_not_found(self):
227 expected = {
228 'shoehorn-config': {
229 'admin-secret': 'Secret!',
230 'type': 'ec2'},
231 }
232 yaml_contents = expected.copy()
233 env_file = self.make_env_file(yaml.safe_dump(yaml_contents))
234 expected = 'invalid YAML contents in {}: {}'.format(
235 env_file, yaml_contents)
236 with self.assert_value_error(expected):
237 envs.load_generated(env_file)
238
239 def test_successful_default_section(self):
240 expected = {
241 'bootstrap-config': {
242 'admin-secret': 'Secret!',
243 'type': 'ec2'},
244 }
245 yaml_contents = expected.copy()
246 env_file = self.make_env_file(yaml.safe_dump(yaml_contents))
247 env_db = envs.load_generated(env_file)
248 self.assertEqual(expected['bootstrap-config'], env_db)
249
250 def test_successful_specified_section(self):
251 expected = {
252 'my-config': {
253 'admin-secret': 'Secret!',
254 'type': 'ec2'},
255 }
256 yaml_contents = expected.copy()
257 env_file = self.make_env_file(yaml.safe_dump(yaml_contents))
258 env_db = envs.load_generated(env_file, section='my-config')
259 self.assertEqual(expected['my-config'], env_db)
260
261
262class TestSave(helpers.EnvFileTestsMixin, unittest.TestCase):186class TestSave(helpers.EnvFileTestsMixin, unittest.TestCase):
263187
264 def setUp(self):188 def setUp(self):
265189
=== added file 'quickstart/tests/models/test_jenv.py'
--- quickstart/tests/models/test_jenv.py 1970-01-01 00:00:00 +0000
+++ quickstart/tests/models/test_jenv.py 2014-11-11 14:26:05 +0000
@@ -0,0 +1,110 @@
1# This file is part of the Juju Quickstart Plugin, which lets users set up a
2# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
3# Copyright (C) 2014 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 jenv generated files handling."""
18
19from __future__ import unicode_literals
20
21import unittest
22
23import yaml
24
25from quickstart.models import jenv
26from quickstart.tests import helpers
27
28
29class TestExists(helpers.JenvFileTestsMixin, unittest.TestCase):
30
31 def test_found(self):
32 # True is returned if the jenv file exists.
33 with self.make_jenv('ec2', ''):
34 exists = jenv.exists('ec2')
35 self.assertTrue(exists)
36
37 def test_not_found(self):
38 # False is returned if the jenv file does not exist.
39 with self.make_jenv('ec2', ''):
40 exists = jenv.exists('local')
41 self.assertFalse(exists)
42
43
44class TestGetValue(
45 helpers.JenvFileTestsMixin, helpers.ValueErrorTestsMixin,
46 unittest.TestCase):
47
48 def test_whole_content(self):
49 # The function correctly returns the whole jenv content.
50 with self.make_jenv('local', yaml.safe_dump(self.jenv_data)):
51 data = jenv.get_value('local')
52 self.assertEqual(self.jenv_data, data)
53
54 def test_section(self):
55 # The function correctly returns a whole section.
56 with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)):
57 data = jenv.get_value('ec2', 'bootstrap-config')
58 self.assertEqual(self.jenv_data['bootstrap-config'], data)
59
60 def test_value(self):
61 # The function correctly returns a value in the root node.
62 with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)):
63 value = jenv.get_value('ec2', 'user')
64 self.assertEqual('admin', value)
65
66 def test_section_value(self):
67 # The function correctly returns a section value.
68 with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)):
69 value = jenv.get_value('ec2', 'bootstrap-config', 'admin-secret')
70 self.assertEqual('Secret!', value)
71
72 def test_nested_section(self):
73 # The function correctly returns nested section's values.
74 with self.make_jenv('hp', yaml.safe_dump(self.jenv_data)):
75 value = jenv.get_value('hp', 'life', 'universe', 'everything')
76 self.assertEqual(42, value)
77
78 def test_file_not_found(self):
79 # A ValueError is raised if the jenv file cannot be found.
80 expected_error = 'unable to open file'
81 with self.make_jenv('hp', yaml.safe_dump(self.jenv_data)):
82 with self.assertRaises(ValueError) as context_manager:
83 jenv.get_value('local')
84 self.assertIn(expected_error, bytes(context_manager.exception))
85
86 def test_section_not_found(self):
87 # A ValueError is raised if the specified section cannot be found.
88 with self.make_jenv('local', yaml.safe_dump(self.jenv_data)) as path:
89 expected_error = (
90 'invalid YAML contents in {}: no-such key not found in the '
91 'root section'.format(path))
92 with self.assert_value_error(expected_error):
93 jenv.get_value('local', 'no-such')
94
95 def test_subsection_not_found(self):
96 # A ValueError is raised if the specified subsection cannot be found.
97 with self.make_jenv('local', yaml.safe_dump(self.jenv_data)) as path:
98 expected_error = (
99 'invalid YAML contents in {}: series key not found in the '
100 'bootstrap-config section'.format(path))
101 with self.assert_value_error(expected_error):
102 jenv.get_value('local', 'bootstrap-config', 'series')
103
104 def test_invalid_yaml_contents(self):
105 # A ValueError is raised if the jenv file is not well formed.
106 expected_error = 'unable to parse file'
107 with self.make_jenv('ec2', ':'):
108 with self.assertRaises(ValueError) as context_manager:
109 jenv.get_value('ec2')
110 self.assertIn(expected_error, bytes(context_manager.exception))
0111
=== modified file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 2014-11-10 11:39:40 +0000
+++ quickstart/tests/test_app.py 2014-11-11 14:26:05 +0000
@@ -673,26 +673,24 @@
673 ] + self.make_status_calls(2))673 ] + self.make_status_calls(2))
674674
675675
676class TestGetValueFromJenv(unittest.TestCase):676class TestGetAdminSecret(
677677 helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
678 def test_no_key(self):
679 with mock.patch('quickstart.manage.envs.load_generated',
680 lambda x: {}):
681 with self.assertRaises(ValueError) as exc:
682 app.get_value_from_jenv(
683 'local', '/home/bac/.juju', 'my-key')
684 expected = (
685 u'my-key not found in '
686 '/home/bac/.juju/environments/local.jenv')
687 self.assertIn(expected, bytes(exc.exception))
688678
689 def test_success(self):679 def test_success(self):
690 expected = 'superchunk'680 # The admin secret is successfully retrieved.
691 with mock.patch('quickstart.manage.envs.load_generated',681 with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)):
692 lambda x: {'my-key': expected}):682 admin_secret = app.get_admin_secret('ec2')
693 secret = app.get_value_from_jenv(683 self.assertEqual('Secret!', admin_secret)
694 'local', '~bac/.juju', 'my-key')684
695 self.assertEqual(expected, secret)685 def test_error(self):
686 # A ProgramExit is raised if the admin secret cannot be retrieved.
687 with self.make_jenv('ec2', '') as path:
688 expected_error = (
689 'cannot retrieve environment admin-secret: invalid YAML '
690 'contents in {}: bootstrap-config key not found in the root '
691 'section'.format(path))
692 with self.assert_program_exit(expected_error):
693 app.get_admin_secret('ec2')
696694
697695
698class TestGetApiUrl(696class TestGetApiUrl(
699697
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2014-11-10 11:27:04 +0000
+++ quickstart/tests/test_manage.py 2014-11-11 14:26:05 +0000
@@ -509,7 +509,6 @@
509 options = self.make_options(509 options = self.make_options(
510 env_file, env_name='aws', interactive=False)510 env_file, env_name='aws', interactive=False)
511 manage._setup_env(options, self.parser)511 manage._setup_env(options, self.parser)
512 self.assertEqual('Secret!', options.admin_secret)
513 self.assertEqual(env_file, options.env_file)512 self.assertEqual(env_file, options.env_file)
514 self.assertEqual('aws', options.env_name)513 self.assertEqual('aws', options.env_name)
515 self.assertEqual('ec2', options.env_type)514 self.assertEqual('ec2', options.env_type)
@@ -550,7 +549,6 @@
550 options = self.make_options(549 options = self.make_options(
551 env_file, env_name='lxc', interactive=False)550 env_file, env_name='lxc', interactive=False)
552 manage._setup_env(options, self.parser)551 manage._setup_env(options, self.parser)
553 self.assertEqual('Secret!', options.admin_secret)
554 self.assertEqual(env_file, options.env_file)552 self.assertEqual(env_file, options.env_file)
555 self.assertEqual('lxc', options.env_name)553 self.assertEqual('lxc', options.env_name)
556 self.assertEqual('local', options.env_type)554 self.assertEqual('local', options.env_type)
@@ -593,7 +591,6 @@
593 mock_interactive.assert_called_once_with(591 mock_interactive.assert_called_once_with(
594 self.parser, mock_get_env_type_db(), env_db, env_file)592 self.parser, mock_get_env_type_db(), env_db, env_file)
595 # The options is updated with data from the selected environment.593 # The options is updated with data from the selected environment.
596 self.assertEqual('Secret!', options.admin_secret)
597 self.assertEqual(env_file, options.env_file)594 self.assertEqual(env_file, options.env_file)
598 self.assertEqual('aws', options.env_name)595 self.assertEqual('aws', options.env_name)
599 self.assertEqual('ec2', options.env_type)596 self.assertEqual('ec2', options.env_type)
@@ -754,7 +751,6 @@
754 def make_options(self, **kwargs):751 def make_options(self, **kwargs):
755 """Set up the options to be passed to the run function."""752 """Set up the options to be passed to the run function."""
756 options = {753 options = {
757 'admin_secret': 'Secretz!',
758 'bundle': None,754 'bundle': None,
759 'bundle_id': None,755 'bundle_id': None,
760 'charm_url': None,756 'charm_url': None,
@@ -767,17 +763,6 @@
767 options.update(kwargs)763 options.update(kwargs)
768 return mock.Mock(**options)764 return mock.Mock(**options)
769765
770 @staticmethod
771 def mock_get_value_from_jenv_success(name, home, key):
772 return 'from jenv'
773
774 @staticmethod
775 def mock_get_value_from_jenv_error(name, home, key):
776 fn = '{}.jenv'.format(name)
777 path = os.path.join(home, 'environments', fn)
778 msg = '{} not found in {}'.format(key, path)
779 raise ValueError(msg.encode('utf-8'))
780
781 def test_no_bundle(self, mock_app, mock_open):766 def test_no_bundle(self, mock_app, mock_open):
782 # The application runs correctly if no bundle is provided.767 # The application runs correctly if no bundle is provided.
783 mock_app.ensure_dependencies.return_value = (1, 18, 0)768 mock_app.ensure_dependencies.return_value = (1, 18, 0)
@@ -790,7 +775,8 @@
790 unit_data = {'Name': 'juju-gui/0'}775 unit_data = {'Name': 'juju-gui/0'}
791 mock_app.check_environment.return_value = (776 mock_app.check_environment.return_value = (
792 'cs:trusty/juju-gui-42', '0', service_data, unit_data)777 'cs:trusty/juju-gui-42', '0', service_data, unit_data)
793 mock_app.get_value_from_jenv = self.mock_get_value_from_jenv_error778 # Make mock_app.get_admin_secret return the admin secret
779 mock_app.get_admin_secret.return_value = 'Secret!'
794 # Make mock_app.watch return the Juju GUI unit address.780 # Make mock_app.watch return the Juju GUI unit address.
795 mock_app.watch.return_value = '1.2.3.4'781 mock_app.watch.return_value = '1.2.3.4'
796 # Make mock_app.create_auth_token return a fake authentication token.782 # Make mock_app.create_auth_token return a fake authentication token.
@@ -810,10 +796,9 @@
810 mock_app.get_api_url.assert_called_once_with(796 mock_app.get_api_url.assert_called_once_with(
811 options.env_name, self.juju_command)797 options.env_name, self.juju_command)
812 mock_app.connect.assert_has_calls([798 mock_app.connect.assert_has_calls([
813 mock.call(799 mock.call(mock_app.get_api_url(), 'Secret!'),
814 mock_app.get_api_url(), options.admin_secret),
815 mock.call().close(),800 mock.call().close(),
816 mock.call('wss://1.2.3.4:443/ws', options.admin_secret),801 mock.call('wss://1.2.3.4:443/ws', 'Secret!'),
817 mock.call().close(),802 mock.call().close(),
818 ])803 ])
819 mock_app.check_environment.assert_called_once_with(804 mock_app.check_environment.assert_called_once_with(
@@ -906,44 +891,9 @@
906 manage.run(options)891 manage.run(options)
907 self.assertFalse(mock_open.called)892 self.assertFalse(mock_open.called)
908893
909 def test_admin_secret_fetched(self, mock_app, mock_open):
910 # If an admin secret is fetched from jenv it is used, even if one is
911 # found in environments.yaml, as set in options.admin_secret.
912 mock_app.get_value_from_jenv = self.mock_get_value_from_jenv_success
913 # Make mock_app.bootstrap return the already_bootstrapped flag and the
914 # bootstrap node series.
915 mock_app.bootstrap.return_value = (True, 'precise')
916 # Make mock_app.check_environment return the charm URL, the machine
917 # where to deploy the charm, the service and unit data.
918 mock_app.check_environment.return_value = (
919 'cs:precise/juju-gui-42', '0', None, None)
920 options = self.make_options(admin_secret='secret in environments.yaml')
921 manage.run(options)
922 mock_app.connect.assert_has_calls([
923 mock.call(mock_app.get_api_url(), 'from jenv'),
924 ])
925
926 def test_admin_secret_from_environments_yaml(self, mock_app, mock_open):
927 # If an admin secret is not fetched from jenv, then the one from
928 # environments.yaml is used, as found in options.admin_secret.
929 mock_app.get_value_from_jenv = self.mock_get_value_from_jenv_error
930 # Make mock_app.bootstrap return the already_bootstrapped flag and the
931 # bootstrap node series.
932 mock_app.bootstrap.return_value = (True, 'precise')
933 # Make mock_app.check_environment return the charm URL, the machine
934 # where to deploy the charm, the service and unit data.
935 mock_app.check_environment.return_value = (
936 'cs:precise/juju-gui-42', '0', None, None)
937 options = self.make_options(admin_secret='secret in environments.yaml')
938 manage.run(options)
939 mock_app.connect.assert_has_calls([
940 mock.call(mock_app.get_api_url(), 'secret in environments.yaml'),
941 ])
942
943 def test_no_admin_secret_found(self, mock_app, mock_open):894 def test_no_admin_secret_found(self, mock_app, mock_open):
944 # If admin-secret cannot be found anywhere a ProgramExit is called.895 # If admin-secret cannot be found a ProgramExit is called.
945 mock_app.ProgramExit = app.ProgramExit896 mock_app.get_admin_secret.side_effect = app.ProgramExit('bad wolf')
946 mock_app.get_value_from_jenv = self.mock_get_value_from_jenv_error
947 # Make mock_app.bootstrap return the already_bootstrapped flag and the897 # Make mock_app.bootstrap return the already_bootstrapped flag and the
948 # bootstrap node series.898 # bootstrap node series.
949 mock_app.bootstrap.return_value = (True, 'precise')899 mock_app.bootstrap.return_value = (True, 'precise')
@@ -952,15 +902,11 @@
952 mock_app.check_environment.return_value = (902 mock_app.check_environment.return_value = (
953 'cs:precise/juju-gui-42', '0', None, None)903 'cs:precise/juju-gui-42', '0', None, None)
954 options = self.make_options(904 options = self.make_options(
955 admin_secret=None,
956 env_name='local',905 env_name='local',
957 env_file='environments.yaml')906 env_file='environments.yaml')
958 with self.assertRaises(app.ProgramExit) as context:907 with self.assertRaises(app.ProgramExit) as context:
959 manage.run(options)908 manage.run(options)
960 expected = (909 self.assertEqual('bad wolf', context.exception.message)
961 u'admin-secret not found in {}/environments/local.jenv '
962 'or environments.yaml'.format(settings.JUJU_HOME))
963 self.assertEqual(expected, context.exception.message)
964910
965 def test_juju_environ_var_set(self, mock_app, mock_open):911 def test_juju_environ_var_set(self, mock_app, mock_open):
966 mock_app.bootstrap.return_value = (True, 'precise')912 mock_app.bootstrap.return_value = (True, 'precise')
967913
=== modified file 'quickstart/tests/test_serializers.py'
--- quickstart/tests/test_serializers.py 2013-12-06 18:16:02 +0000
+++ quickstart/tests/test_serializers.py 2014-11-11 14:26:05 +0000
@@ -18,12 +18,15 @@
1818
19from __future__ import unicode_literals19from __future__ import unicode_literals
2020
21import os
22import tempfile
21import unittest23import unittest
2224
23import mock25import mock
24import yaml26import yaml
2527
26from quickstart import serializers28from quickstart import serializers
29from quickstart.tests import helpers
2730
2831
29class TestYamlLoad(unittest.TestCase):32class TestYamlLoad(unittest.TestCase):
@@ -56,3 +59,41 @@
56 # Collections are serialized in the block style.59 # Collections are serialized in the block style.
57 contents = serializers.yaml_dump(self.data)60 contents = serializers.yaml_dump(self.data)
58 self.assertEqual('myint: 42\nmystring: foo\n', contents)61 self.assertEqual('myint: 42\nmystring: foo\n', contents)
62
63
64class TestYamlLoadFromPath(helpers.ValueErrorTestsMixin, unittest.TestCase):
65
66 def make_file(self, contents):
67 """Create a temporary file with the given contents.
68
69 Return the file path.
70 """
71 yaml_file = tempfile.NamedTemporaryFile(delete=False)
72 self.addCleanup(os.remove, yaml_file.name)
73 yaml_file.write(contents)
74 yaml_file.close()
75 return yaml_file.name
76
77 def test_resulting_contents(self):
78 # The YAML deserialized contents are correctly returned.
79 expected_data = {'myint': 42, 'mystring': 'foo'}
80 path = self.make_file(yaml.safe_dump(expected_data))
81 obtained_data = serializers.yaml_load_from_path(path)
82 self.assertEqual(expected_data, obtained_data)
83
84 def test_no_file(self):
85 # A ValueError is raised if the given file is not found.
86 expected_error = (
87 "unable to open file /no/such/file.yaml: "
88 "[Errno 2] No such file or directory: '/no/such/file.yaml'"
89 )
90 with self.assert_value_error(expected_error):
91 serializers.yaml_load_from_path('/no/such/file.yaml')
92
93 def test_invalid_yaml(self):
94 # A ValueError is raised if the given file is not a valid YAML.
95 path = self.make_file(':')
96 with self.assertRaises(ValueError) as context_manager:
97 serializers.yaml_load_from_path(path)
98 expected = 'unable to parse file {}'.format(path)
99 self.assertIn(expected, bytes(context_manager.exception))

Subscribers

People subscribed via source and target branches