Merge lp:~frankban/juju-quickstart/enable-env-management into lp:juju-quickstart
- enable-env-management
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 44 | ||||
Proposed branch: | lp:~frankban/juju-quickstart/enable-env-management | ||||
Merge into: | lp:juju-quickstart | ||||
Diff against target: |
1392 lines (+644/-378) 13 files modified
cli-app-demo.py (+0/-57) quickstart/__init__.py (+1/-1) quickstart/app.py (+0/-19) quickstart/cli/views.py (+41/-1) quickstart/manage.py (+118/-22) quickstart/models/envs.py (+105/-131) quickstart/tests/cli/test_views.py (+42/-0) quickstart/tests/helpers.py (+4/-1) quickstart/tests/models/test_envs.py (+44/-79) quickstart/tests/test_app.py (+0/-34) quickstart/tests/test_manage.py (+221/-33) quickstart/tests/test_utils.py (+52/-0) quickstart/utils.py (+16/-0) |
||||
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/enable-env-management | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
Integrate the env management functionality.
This branch enables the environment management
Urwid interactive session feature in quickstart,
but also includes some other improvements, described
below.
Exposed a way to let the user quickly
create and bootstrap a local env if no environments
are already configured. This is implemented
adding a closure (create_
env_index view: this could have been implemented
as a separate function as well (more easily testable,
less surprising), but I decided to stick with the
pattern used by the other views for now.
Changed the env_edit view so that the first created
environment is automatically set as default.
Renamed manage.
the function now does a lot more than validation, e.g.
it lunches the interactive session, handles the
case when the Juju home is not yet configured, etc.
This function also sets up a save_callable to be used
by views. I could have used partial to create such a
function, but I decided to implement a HO
_create_
later in order to enable the backup functionality.
Bumped version up.
The diff is very long, my apologies, but you can safely
ignore deleted code:
- the demo application, no longer required;
- the ensure_environments function + its tests;
- envs.parse_env_file + its tests (
replaced by envs.load + envs.validate).
Moreover, the env_type_db definition in
quickstart.
of the original one: the only difference is that
an ordered dict is used in place of the usual dict.
This change has been introduced so that views
can list the supported environment types in the order
they are defined.
Tests: `make check`.
QA: if you have time, given the amount of code
enabled by this branch, and given the differences
in each one Juju configuration, I'd appreciate both
reviewers to QA the interactive session.
The instructions follow, thank you very much!
- Run `make`.
- Backup your Juju home:
`mv ~/.juju ~/juju-home-backup`
This way we can simulate Juju is not configured at all.
- Run `.venv/bin/python juju-quickstart`.
Quickstart should welcome you and propose, among
other things, to automatically create and bootstrap
a local environment. Do that, provide your sudo
password if requested, wait until the process completes
and the GUI opens in your default browser as usual.
This can take several minutes based on the sanity of your
LXC configuration. Before opening the browser, you should
see instructions on re-running quickstart to reopen
and log in to the GUI later or restarting the interactive
session. Also note that an admin password has been
automatically generated and printed to stdout.
- Run `.venv/bin/python juju-quickstart`.
This time no interactive session is started, the application
recognizes the environment is already bootstrapped and
quickly reopens the browser and logs in to the GUI.
- Run `cat ~/.juju/
Check the generated environment file looks sane.
- Destroy the local environment:
`sudo juju destroy-environment -e local -y`
- Run `.venv/bin/python juju-quickstart -i`.
The interactive session should start and this time
it should list your existing environment.
Let's customize the local one: click on it, then
on the "edit" button, and then use the form
to change its name (e.g. replace "local" with "lxc").
Hit page-down and click "save" an then "back" to
return to the index view.
Now let's create an ec2 environment by selecting
"new ec2 environment". Call it "aws", auto-generate
the admin-secret and the control-bucket, fill in your
ec2 credentials and save the new environment.
Also make it the default one, either by using the
checkbox in the form or by clicking "set default"
from the env details view. From the envs detail view,
let's go ahead and bootstrap the "aws" env by clicking "use".
As above, wait for the ec2 environment to be bootstrapped
and the GUI opened. In the meanwhile, you can run
`cat ~/.juju/
ensure the env file looks good. Note that at this time
no backup files are created: this is the goal of my next card.
- Run `juju switch` to check that the default environment
is now "aws".
- Check that command line options validation still work: all the
following command should exit with pertinent errors:
.venv/bin/python juju-quickstart --gui-charm-url invalid
.venv/bin/python juju-quickstart --gui-charm-url http:~juju-
.venv/bin/python juju-quickstart --gui-charm-url cs:precise/
.venv/bin/python juju-quickstart --gui-charm-url cs:saucy/
.venv/bin/python juju-quickstart -e no-such
.venv/bin/python juju-quickstart /no-such-bundle
.venv/bin/python juju-quickstart bundle:
- Let's also try to deploy a bundle:
run `juju unexpose juju-gui` and then
`.venv/bin/python juju-quickstart bundle:
The service should be re-exposed by quickstart and the bundle
deployment should start as usual.
- Destroy the ec2 environment: `juju destroy-environment -e aws -y`.
- Restore your existing juju home:
`rm -rf ~/.juju && mv ~/juju-home-backup .juju`.
- Create a backup copy of your environments file:
`cp ~/.juju/
- Not it's time to be creative. Run quickstart in interactive
mode (`.venv/bin/python juju-quickstart -i`): you should
see your environments listed.
Start changing/
environments. Remove required fields, exit without editing
to check your environments file is not saved by quickstart
if not necessary, try to create two envs with the same name.
In a sentence: try hard to break the application in any
way you can imagine.
- Once done, restore your original environments file:
`mv ~/environments.
- Remember to destroy all the environments you
bootstrapped.
Done! Many thanks for going through all these QA steps.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Benji York (benji) wrote : | # |
LGTM with one tiny suggestion.
https:/
File quickstart/
https:/
quickstart/
This function has gotten a bit big. I wonder if a nice refactoring
would be one that made this function contain no conditionals but instead
called four or five subfunctions.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
code looks good, will start QA.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
QA ok, LGTM
It was cool to use it in interactive mode against my original
environments.yaml file. The only issue from a user POV was I was looking
for some command to save/write the changes made. When I ended up just
quitting I saw the output
changes to the environments file have been saved
but it'd be nice to have some sort of feedback or specific 'save'
command in the UI.
- 57. By Francesco Banconi
-
Changes as per review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Integrate the env management functionality.
This branch enables the environment management
Urwid interactive session feature in quickstart,
but also includes some other improvements, described
below.
Exposed a way to let the user quickly
create and bootstrap a local env if no environments
are already configured. This is implemented
adding a closure (create_
env_index view: this could have been implemented
as a separate function as well (more easily testable,
less surprising), but I decided to stick with the
pattern used by the other views for now.
Changed the env_edit view so that the first created
environment is automatically set as default.
Renamed manage.
the function now does a lot more than validation, e.g.
it lunches the interactive session, handles the
case when the Juju home is not yet configured, etc.
This function also sets up a save_callable to be used
by views. I could have used partial to create such a
function, but I decided to implement a HO
_create_
later in order to enable the backup functionality.
Bumped version up.
The diff is very long, my apologies, but you can safely
ignore deleted code:
- the demo application, no longer required;
- the ensure_environments function + its tests;
- envs.parse_env_file + its tests (
replaced by envs.load + envs.validate).
Moreover, the env_type_db definition in
quickstart.
of the original one: the only difference is that
an ordered dict is used in place of the usual dict.
This change has been introduced so that views
can list the supported environment types in the order
they are defined.
Tests: `make check`.
QA: if you have time, given the amount of code
enabled by this branch, and given the differences
in each one Juju configuration, I'd appreciate both
reviewers to QA the interactive session.
The instructions follow, thank you very much!
- Run `make`.
- Backup your Juju home:
`mv ~/.juju ~/juju-home-backup`
This way we can simulate Juju is not configured at all.
- Run `.venv/bin/python juju-quickstart`.
Quickstart should welcome you and propose, among
other things, to automatically create and bootstrap
a local environment. Do that, provide your sudo
password if requested, wait until the process completes
and the GUI opens in your default browser as usual.
This can take several minutes based on the sanity of your
LXC configuration. Before opening the browser, you should
see instructions on re-running quickstart to reopen
and log in to the GUI later or restarting the interactive
session. Also note that an admin password has been
automatically generated and printed to stdout.
- Run `.venv/bin/python juju-quickstart`.
This time no interactive session is started, the application
recognizes the environment is already bootstrapped and
quickly reopens the browser and logs in to the GUI.
- Run `cat ~/.juju/
Check the generated environment file looks sane.
- Destroy the local environment:
`sudo juju destroy-environment -e local -y`
- Run `.venv/b...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
On 2014/01/10 18:05:45, benji1 wrote:
> LGTM with one tiny suggestion.
> https:/
> File quickstart/
https:/
> quickstart/
> This function has gotten a bit big. I wonder if a nice refactoring
would be one
> that made this function contain no conditionals but instead called
four or five
> subfunctions.
Done. Thank you both!
Preview Diff
1 | === removed file 'cli-app-demo.py' |
2 | --- cli-app-demo.py 2013-12-19 10:04:19 +0000 |
3 | +++ cli-app-demo.py 1970-01-01 00:00:00 +0000 |
4 | @@ -1,57 +0,0 @@ |
5 | -#!.venv/bin/python |
6 | - |
7 | -# This file is part of the Juju Quickstart Plugin, which lets users set up a |
8 | -# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
9 | -# Copyright (C) 2013 Canonical Ltd. |
10 | -# |
11 | -# This program is free software: you can redistribute it and/or modify it under |
12 | -# the terms of the GNU Affero General Public License version 3, as published by |
13 | -# the Free Software Foundation. |
14 | -# |
15 | -# This program is distributed in the hope that it will be useful, but WITHOUT |
16 | -# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
17 | -# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 | -# Affero General Public License for more details. |
19 | -# |
20 | -# You should have received a copy of the GNU Affero General Public License |
21 | -# along with this program. If not, see <http://www.gnu.org/licenses/>. |
22 | - |
23 | -"""Juju Quickstart CLI application demo.""" |
24 | - |
25 | -# XXX frankban (2013-12-16): this file is only for demonstration/QA purposes. |
26 | -# Remove this file when the environment management integration is completed. |
27 | -# Run "make" before running this file. |
28 | - |
29 | -from __future__ import ( |
30 | - print_function, |
31 | - unicode_literals, |
32 | -) |
33 | - |
34 | -import pprint |
35 | - |
36 | -from quickstart.cli import views |
37 | -from quickstart.models import envs |
38 | -from quickstart.tests import helpers |
39 | - |
40 | - |
41 | -def main(): |
42 | - """Start the Quickstart environments list view.""" |
43 | - env_type_db = envs.get_env_type_db() |
44 | - # This simulates an env database as returned by envs.load(). |
45 | - env_db = helpers.make_env_db(default='lxc') |
46 | - save_callable = lambda db: None |
47 | - # Show the view. |
48 | - new_env_db, env_data = views.show( |
49 | - views.env_index, env_type_db, env_db, save_callable) |
50 | - if new_env_db != env_db: |
51 | - print('saved a new env db') |
52 | - pprint.pprint(new_env_db) |
53 | - print('default: {}'.format(new_env_db.get('default'))) |
54 | - if env_data is None: |
55 | - print('no environment selected') |
56 | - else: |
57 | - print('selected: {}'.format(env_data['name'])) |
58 | - |
59 | - |
60 | -if __name__ == '__main__': |
61 | - main() |
62 | |
63 | === modified file 'quickstart/__init__.py' |
64 | --- quickstart/__init__.py 2013-12-06 10:07:40 +0000 |
65 | +++ quickstart/__init__.py 2014-01-13 15:31:13 +0000 |
66 | @@ -22,7 +22,7 @@ |
67 | from __future__ import unicode_literals |
68 | |
69 | |
70 | -VERSION = (0, 5, 0) |
71 | +VERSION = (0, 6, 0) |
72 | |
73 | |
74 | def get_version(): |
75 | |
76 | === modified file 'quickstart/app.py' |
77 | --- quickstart/app.py 2014-01-03 11:38:43 +0000 |
78 | +++ quickstart/app.py 2014-01-13 15:31:13 +0000 |
79 | @@ -208,25 +208,6 @@ |
80 | print('a new ssh key was generated in {}'.format(key_file)) |
81 | |
82 | |
83 | -def ensure_environments(): |
84 | - """Ensure that the environments file exists. |
85 | - |
86 | - Return the env_name argument unchanged if the environment file exists; |
87 | - however, since the other environment types require additional work to |
88 | - set up, return 'local' if juju init is run. |
89 | - """ |
90 | - print('environments file not found; running juju init') |
91 | - retcode, _, error = utils.call('juju', 'init') |
92 | - if retcode: |
93 | - raise ProgramExit(error) |
94 | - print('defaulting to local environment for a new environments file') |
95 | - env_name = 'local' |
96 | - retcode, _, error = utils.call('juju', 'switch', env_name) |
97 | - if retcode: |
98 | - raise ProgramExit(error) |
99 | - return env_name |
100 | - |
101 | - |
102 | def bootstrap(env_name, is_local=False, debug=False): |
103 | """Bootstrap the Juju environment with the given name. |
104 | |
105 | |
106 | === modified file 'quickstart/cli/views.py' |
107 | --- quickstart/cli/views.py 2014-01-07 15:53:57 +0000 |
108 | +++ quickstart/cli/views.py 2014-01-13 15:31:13 +0000 |
109 | @@ -156,13 +156,50 @@ |
110 | envs.get_env_data(env_db, env_name) |
111 | for env_name in env_db['environments'] |
112 | ], key=operator.itemgetter('name')) |
113 | + |
114 | + def create_and_start_local_env(): |
115 | + # Automatically create and use a local environment named "local". |
116 | + # This closure can only be called when there are no environments in the |
117 | + # database. For this reason, the new environment is set as default. |
118 | + # Exit the interactive session selecting the newly created environment. |
119 | + env_data = envs.create_local_env_data( |
120 | + env_type_db, 'local', is_default=True) |
121 | + # Add the new environment to the environments database. |
122 | + envs.set_env_data(env_db, None, env_data) |
123 | + save_callable(env_db) |
124 | + # Use the newly created environment. |
125 | + raise ui.AppExit((env_db, env_data)) |
126 | + |
127 | if environments: |
128 | title = 'Select an existing Juju environment or create a new one' |
129 | + widgets = [] |
130 | else: |
131 | title = 'No Juju environments already set up: please create one' |
132 | + widgets = [ |
133 | + urwid.Text([ |
134 | + ('highlight', 'Welcome to Juju Quickstart!'), |
135 | + '\nYou can use this interactive session to manage your Juju ' |
136 | + 'environments.\nInteractive mode has been automatically ' |
137 | + 'started because no environments have been found. After ' |
138 | + 'creating your first environment you can start Juju ' |
139 | + 'Quickstart in interactive mode again by passing the -i flag, ' |
140 | + 'e.g.:\n', |
141 | + ('highlight', '$ juju quickstart -i'), |
142 | + '\n\nAt the bottom of the page you can find links to manually ' |
143 | + 'create new environments. If you instead prefer to quickly ' |
144 | + 'start your Juju experience in a local environment (LXC), ' |
145 | + 'just click the link below:' |
146 | + ]), |
147 | + ui.MenuButton( |
148 | + '\N{BULLET} automatically create and bootstrap a local ' |
149 | + 'environment', ui.thunk(create_and_start_local_env)), |
150 | + ] |
151 | app.set_title(title) |
152 | # Start creating the page contents: a list of selectable environments. |
153 | - widgets = [] |
154 | + # Wouldn't it be nice if we were able to highlight in some way the |
155 | + # currently running environments? Unfortunately this requires calling |
156 | + # "juju status" for each environment in the list, which is expensive and |
157 | + # time consuming. |
158 | focus_position = None |
159 | errors_found = default_found = False |
160 | for position, env_data in enumerate(environments): |
161 | @@ -376,6 +413,9 @@ |
162 | # Without errors, normalize the new values, update the env_db and save |
163 | # the resulting environments database. |
164 | env_data = envs.normalize(env_metadata, new_env_data) |
165 | + # If this is the only environment in the db, set it as the default one. |
166 | + if not env_db['environments']: |
167 | + env_data['is-default'] = True |
168 | envs.set_env_data(env_db, initial_name, env_data) |
169 | save_callable(env_db) |
170 | verb = 'modified' if exists else 'created' |
171 | |
172 | === modified file 'quickstart/manage.py' |
173 | --- quickstart/manage.py 2014-01-03 11:38:43 +0000 |
174 | +++ quickstart/manage.py 2014-01-13 15:31:13 +0000 |
175 | @@ -34,6 +34,7 @@ |
176 | settings, |
177 | utils, |
178 | ) |
179 | +from quickstart.cli import views |
180 | from quickstart.models import ( |
181 | charms, |
182 | envs, |
183 | @@ -134,35 +135,124 @@ |
184 | 'revision: {}'.format(charm)) |
185 | |
186 | |
187 | -def _validate_env(options, parser): |
188 | - """Validate and process the provided environment related options. |
189 | +def _retrieve_env_db(parser, env_file=None): |
190 | + """Retrieve the environment database (or create an in-memory empty one).""" |
191 | + if env_file is None: |
192 | + return envs.create_empty_env_db() |
193 | + try: |
194 | + return envs.load(env_file) |
195 | + except ValueError as err: |
196 | + return parser.error(bytes(err)) |
197 | + |
198 | + |
199 | +def _create_save_callable(parser, env_file): |
200 | + """Return a function that can be used to save an env_db to the env_file. |
201 | + |
202 | + The returned function is used as save_callable by the environments |
203 | + management views. |
204 | + |
205 | + The resulting function uses the given parser instance to exit the |
206 | + application with an error if an OSError exception is raised while saving |
207 | + the environments database. |
208 | + """ |
209 | + def save_callable(env_db): |
210 | + try: |
211 | + envs.save(env_file, env_db) |
212 | + except OSError as err: |
213 | + return parser.error(bytes(err)) |
214 | + |
215 | + return save_callable |
216 | + |
217 | + |
218 | +def _start_interactive_session(parser, env_type_db, env_db, env_file): |
219 | + """Start the Urwid interactive session. |
220 | + |
221 | + Return the env_data corresponding to the user selected environment. |
222 | + Exit the application if the user exits the interactive session without |
223 | + selecting an environment to start. |
224 | + """ |
225 | + save_callable = _create_save_callable(parser, env_file) |
226 | + new_env_db, env_data = views.show( |
227 | + views.env_index, env_type_db, env_db, save_callable) |
228 | + if new_env_db != env_db: |
229 | + print('changes to the environments file have been saved') |
230 | + if env_data is None: |
231 | + # The user exited the interactive session without selecting an |
232 | + # environment to start: this means this was just an environment |
233 | + # editing session and we can just quit now. |
234 | + return sys.exit('quitting') |
235 | + return env_data |
236 | + |
237 | + |
238 | +def _retrieve_env_data(parser, env_type_db, env_db, env_name): |
239 | + """Retrieve and return the env_data corresponding to the given env_name. |
240 | + |
241 | + Invoke a parser error if the environment does not exist or is not valid. |
242 | + """ |
243 | + try: |
244 | + env_data = envs.get_env_data(env_db, env_name) |
245 | + except ValueError as err: |
246 | + # The specified environment does not exist. |
247 | + return parser.error(bytes(err)) |
248 | + env_metadata = envs.get_env_metadata(env_type_db, env_data) |
249 | + errors = envs.validate(env_metadata, env_data) |
250 | + if errors: |
251 | + msg = 'cannot use the {} environment:\n{}'.format( |
252 | + env_name, '\n'.join(errors.values())) |
253 | + return parser.error(msg.encode('utf-8')) |
254 | + return env_data |
255 | + |
256 | + |
257 | +def _setup_env(options, parser): |
258 | + """Set up, validate and process the provided environment related options. |
259 | + |
260 | + Also start the environments management interactive session if required. |
261 | |
262 | Exit with an error if options are not valid. |
263 | """ |
264 | - logging.debug('ensuring juju environments available') |
265 | + logging.debug('setting up juju environments') |
266 | + env_name = options.env_name |
267 | env_file = os.path.abspath(os.path.expanduser(options.env_file)) |
268 | + interactive = options.interactive |
269 | + env_file_exists = os.path.exists(env_file) |
270 | + if not env_file_exists: |
271 | + # If the Juju home is not set up, force the interactive mode and ignore |
272 | + # the user provided env name. |
273 | + interactive = True |
274 | + env_name = None |
275 | # Validate the environment name. |
276 | - env_name = options.env_name |
277 | - if env_name is None: |
278 | + if env_name is None and not interactive: |
279 | + # The user forced non-interactive mode but a default env name cannot |
280 | + # be retrieved. In this case, just exit with an error. |
281 | return parser.error( |
282 | 'unable to find an environment name to use\n' |
283 | - 'It is possible to specify the environment name by either:\n' |
284 | + 'It is possible to specify the environment to use by either:\n' |
285 | + ' - selecting one from the quickstart interactive session,\n' |
286 | + ' i.e. juju quickstart -i;\n' |
287 | ' - passing the -e or --environment argument;\n' |
288 | ' - setting the JUJU_ENV environment variable;\n' |
289 | ' - using "juju switch" to select the default environment;\n' |
290 | ' - setting the default environment in {}.'.format(env_file) |
291 | ) |
292 | - # Validate the environment file. |
293 | - try: |
294 | - env_type, admin_secret, default_series = ( |
295 | - envs.parse_env_file(env_file, env_name)) |
296 | - except ValueError as err: |
297 | - return parser.error(bytes(err)) |
298 | + # Retrieve the environment database (or create an in-memory empty one). |
299 | + env_db = _retrieve_env_db(parser, env_file if env_file_exists else None) |
300 | + # Validate the environment. |
301 | + env_type_db = envs.get_env_type_db() |
302 | + if interactive: |
303 | + # Start the interactive session. |
304 | + env_data = _start_interactive_session( |
305 | + parser, env_type_db, env_db, env_file) |
306 | + else: |
307 | + # This is a non-interactive session and we need to validate the |
308 | + # selected environment before proceeding. |
309 | + env_data = _retrieve_env_data(parser, env_type_db, env_db, env_name) |
310 | # Update the options namespace with the new values. |
311 | - options.admin_secret = admin_secret |
312 | + options.admin_secret = env_data['admin-secret'] |
313 | options.env_file = env_file |
314 | - options.env_type = env_type |
315 | - options.default_series = default_series |
316 | + options.env_name = env_data['name'] |
317 | + options.env_type = env_data['type'] |
318 | + options.default_series = env_data.get('default-series') |
319 | + options.interactive = interactive |
320 | |
321 | |
322 | def _configure_logging(level): |
323 | @@ -204,6 +294,7 @@ |
324 | - env_file: the absolute path of the Juju environments.yaml file; |
325 | - env_name: the name of the Juju environment to use; |
326 | - env_type: the provider type of the selected Juju environment; |
327 | + - interactive: whether to start the interactive session; |
328 | - open_browser: whether the GUI browser must be opened. |
329 | |
330 | The following attributes will also be included in the namespace if a bundle |
331 | @@ -219,11 +310,6 @@ |
332 | default_env_name = envs.get_default_env_name() |
333 | # Define the help message for the --environment option. |
334 | env_help = 'The name of the Juju environment to use' |
335 | - # XXX 2013-12-02 makyo: |
336 | - # ensure_environments will be removed with more robust environment |
337 | - # management code in the next few weeks. |
338 | - if default_env_name is None: |
339 | - default_env_name = app.ensure_environments() |
340 | if default_env_name is not None: |
341 | env_help = '{} (%(default)s)'.format(env_help) |
342 | # Create and set up the arguments parser. |
343 | @@ -245,6 +331,9 @@ |
344 | 'required if the bundle YAML/JSON only contains one bundle. This ' |
345 | 'option is ignored if the bundle file is not specified') |
346 | parser.add_argument( |
347 | + '-i', '--interactive', action='store_true', dest='interactive', |
348 | + help='Start the environments management interactive session') |
349 | + parser.add_argument( |
350 | '--environments-file', dest='env_file', |
351 | default=os.path.join(settings.JUJU_HOME, 'environments.yaml'), |
352 | help='The path to the Juju environments YAML file (%(default)s)') |
353 | @@ -274,7 +363,7 @@ |
354 | # Convert the provided string arguments to unicode. |
355 | _convert_options_to_unicode(options) |
356 | # Validate and process the provided arguments. |
357 | - _validate_env(options, parser) |
358 | + _setup_env(options, parser) |
359 | if options.bundle is not None: |
360 | _validate_bundle(options, parser) |
361 | if options.charm_url is not None: |
362 | @@ -336,4 +425,11 @@ |
363 | url += '/?authtoken={}'.format(token) |
364 | webbrowser.open(url) |
365 | gui_env.close() |
366 | - print('done!') |
367 | + print( |
368 | + 'done!\n\n' |
369 | + 'Run "juju quickstart -e {}" again if you want\n' |
370 | + 'to reopen and log in to the GUI browser later.\n' |
371 | + 'Run "juju quickstart -i" if you want to manage\n' |
372 | + 'or bootstrap your Juju environments using the\n' |
373 | + 'interactive session.'.format(options.env_name) |
374 | + ) |
375 | |
376 | === modified file 'quickstart/models/envs.py' |
377 | --- quickstart/models/envs.py 2014-01-07 15:41:55 +0000 |
378 | +++ quickstart/models/envs.py 2014-01-13 15:31:13 +0000 |
379 | @@ -74,9 +74,6 @@ |
380 | The set_env_data function, as seen above, needs to be passed the original name |
381 | of the environment being modified. If None is passed, that means we are adding |
382 | a new environment. |
383 | - |
384 | -XXX frankban 13-12-08: remove the parse_env_file function: this can be done |
385 | - when activating models code in manage.py. |
386 | """ |
387 | |
388 | from __future__ import unicode_literals |
389 | @@ -129,6 +126,11 @@ |
390 | return match.groups()[0] |
391 | |
392 | |
393 | +def create_empty_env_db(): |
394 | + """Create and return an empty environments database.""" |
395 | + return {'environments': {}} |
396 | + |
397 | + |
398 | def load(env_file): |
399 | """Load and parse the provided Juju environments.yaml file. |
400 | |
401 | @@ -164,6 +166,8 @@ |
402 | except Exception as err: |
403 | msg = b'unable to parse environments file {}: {}' |
404 | raise ValueError(msg.format(env_file.encode('utf-8'), err)) |
405 | + if contents is None: |
406 | + return create_empty_env_db() |
407 | # Retrieve the environment list. |
408 | try: |
409 | env_contents = contents.get('environments', {}).items() |
410 | @@ -181,7 +185,7 @@ |
411 | default = contents.get('default') |
412 | if default in environments: |
413 | env_db['default'] = default |
414 | - else: |
415 | + elif default is not None: |
416 | logging.warn('excluding invalid default {}'.format(default)) |
417 | return env_db |
418 | |
419 | @@ -198,6 +202,7 @@ |
420 | # and destination files are on different file systems, use for the |
421 | # temporary file the same directory where the env_file is stored. |
422 | dirname = os.path.dirname(env_file) |
423 | + utils.mkdir(dirname) |
424 | banner = utils.get_quickstart_banner() |
425 | try: |
426 | temp_file = tempfile.NamedTemporaryFile( |
427 | @@ -245,7 +250,8 @@ |
428 | try: |
429 | info = env_db['environments'][env_name] |
430 | except KeyError: |
431 | - raise ValueError(b'environment {!r} not found'.format(env_name)) |
432 | + msg = 'environment {} not found'.format(env_name) |
433 | + raise ValueError(msg.encode('utf-8')) |
434 | # Why not just use env_data.copy()? Because this way internal mutable data |
435 | # structures are preserved, even if they are unlikely to be found. |
436 | env_data = copy.deepcopy(info) |
437 | @@ -296,6 +302,25 @@ |
438 | del env_db['default'] |
439 | |
440 | |
441 | +def create_local_env_data(env_type_db, name, is_default=False): |
442 | + """Create and return an local (LXC) env_data. |
443 | + |
444 | + Local environments' fields (except for name and type) are assumed to be |
445 | + either optional or suitable for automatic generation of their values. For |
446 | + this reason local environments can be safely created given just their name. |
447 | + """ |
448 | + env_data = {'type': 'local', 'name': name, 'is-default': is_default} |
449 | + env_metadata = get_env_metadata(env_type_db, env_data) |
450 | + # Retrieve a list of missing required fields. |
451 | + missing_fields = [ |
452 | + field for field in env_metadata['fields'] |
453 | + if field.required and field.name not in env_data |
454 | + ] |
455 | + # Assume all missing fields can be automatically generated. |
456 | + env_data.update((field.name, field.generate()) for field in missing_fields) |
457 | + return env_data |
458 | + |
459 | + |
460 | def remove_env(env_db, env_name): |
461 | """Remove the environment named env_name from the environments database. |
462 | |
463 | @@ -347,80 +372,9 @@ |
464 | ) |
465 | # Define the env_type_db dictionary: this is done inside this function in |
466 | # order to avoid instantiating fields at import time. |
467 | - env_type_db = { |
468 | - 'ec2': { |
469 | - 'description': ( |
470 | - 'The ec2 provider enable you to run Juju on the EC2 cloud. ' |
471 | - 'This process requires you to have an Amazon Web Services ' |
472 | - '(AWS) account. If you have not signed up for one yet, it ' |
473 | - 'can obtained at http://aws.amazon.com. ' |
474 | - 'See https://juju.ubuntu.com/docs/config-aws.html for more ' |
475 | - 'details on the ec2 provider configuration.' |
476 | - ), |
477 | - 'fields': ( |
478 | - provider_field, |
479 | - name_field, |
480 | - admin_secret_field, |
481 | - default_series_field, |
482 | - fields.PasswordField( |
483 | - 'access-key', label='access key', required=True, |
484 | - help='The access key to use to authenticate to AWS. ' |
485 | - 'You can retrieve these values easily from your AWS ' |
486 | - 'Management Console (http://console.aws.amazon.com). ' |
487 | - 'Click on your name in the top-right and then the ' |
488 | - '"Security Credentials" link from the drop down ' |
489 | - 'menu. Under the "Access Keys" heading click the ' |
490 | - '"Create New Root Key" button. You will be prompted ' |
491 | - 'to "Download Key File" which by default is named ' |
492 | - 'rootkey.csv. Open this file to get the access-key ' |
493 | - 'and secret-key for the environments.yaml ' |
494 | - 'configuration file.'), |
495 | - fields.PasswordField( |
496 | - 'secret-key', label='secret key', required=True, |
497 | - help='The secret key to use to authenticate to AWS. ' |
498 | - 'See the access key help above.'), |
499 | - fields.AutoGeneratedStringField( |
500 | - 'control-bucket', label='control bucket', required=True, |
501 | - help='the globally unique S3 bucket name'), |
502 | - fields.ChoiceField( |
503 | - 'region', choices=ec2_regions, default='us-east-1', |
504 | - label='region', required=False, |
505 | - help='the ec2 region to use'), |
506 | - is_default_field, |
507 | - ), |
508 | - }, |
509 | - 'local': { |
510 | - 'description': ( |
511 | - 'The LXC local provider enables you to run Juju on a single ' |
512 | - 'system like your local computer or a single server. ' |
513 | - 'See https://juju.ubuntu.com/docs/config-LXC.html for more ' |
514 | - 'details on the local provider configuration.' |
515 | - ), |
516 | - 'fields': ( |
517 | - provider_field, |
518 | - name_field, |
519 | - admin_secret_field, |
520 | - default_series_field, |
521 | - fields.StringField( |
522 | - 'root-dir', label='root dir', required=False, |
523 | - default='~/.juju/local', |
524 | - help='the directory that is used for the storage files'), |
525 | - fields.IntField( |
526 | - 'storage-port', min_value=1, max_value=65535, |
527 | - label='storage port', required=False, default=8040, |
528 | - help='override if you have multiple local providers, ' |
529 | - 'or if the default port is used by another program'), |
530 | - fields.IntField( |
531 | - 'shared-storage-port', min_value=1, max_value=65535, |
532 | - label='shared storage port', required=False, default=8041, |
533 | - help='override if you have multiple local providers, ' |
534 | - 'or if the default port is used by another program'), |
535 | - fields.StringField( |
536 | - 'network-bridge', label='network bridge', required=False, |
537 | - default='lxcbr0', help='the LXC bridge interface to use'), |
538 | - is_default_field, |
539 | - ), |
540 | - }, |
541 | + # This is an ordered dict so that views can expose options to create new |
542 | + # environments in the order we like. |
543 | + env_type_db = collections.OrderedDict({ |
544 | '__fallback__': { |
545 | 'description': ( |
546 | 'This provider type is not supported by quickstart. ' |
547 | @@ -434,7 +388,78 @@ |
548 | default_series_field, |
549 | is_default_field, |
550 | ), |
551 | - } |
552 | + }, |
553 | + }) |
554 | + env_type_db['ec2'] = { |
555 | + 'description': ( |
556 | + 'The ec2 provider enable you to run Juju on the EC2 cloud. ' |
557 | + 'This process requires you to have an Amazon Web Services (AWS) ' |
558 | + 'account. If you have not signed up for one yet, it can obtained ' |
559 | + 'at http://aws.amazon.com. ' |
560 | + 'See https://juju.ubuntu.com/docs/config-aws.html for more ' |
561 | + 'details on the ec2 provider configuration.' |
562 | + ), |
563 | + 'fields': ( |
564 | + provider_field, |
565 | + name_field, |
566 | + admin_secret_field, |
567 | + default_series_field, |
568 | + fields.PasswordField( |
569 | + 'access-key', label='access key', required=True, |
570 | + help='The access key to use to authenticate to AWS. ' |
571 | + 'You can retrieve these values easily from your AWS ' |
572 | + 'Management Console (http://console.aws.amazon.com). ' |
573 | + 'Click on your name in the top-right and then the ' |
574 | + '"Security Credentials" link from the drop down menu. ' |
575 | + 'Under the "Access Keys" heading click the ' |
576 | + '"Create New Root Key" button. You will be prompted to ' |
577 | + '"Download Key File" which by default is named ' |
578 | + 'rootkey.csv. Open this file to get the access-key and ' |
579 | + 'secret-key for the environments.yaml config file.'), |
580 | + fields.PasswordField( |
581 | + 'secret-key', label='secret key', required=True, |
582 | + help='The secret key to use to authenticate to AWS. ' |
583 | + 'See the access key help above.'), |
584 | + fields.AutoGeneratedStringField( |
585 | + 'control-bucket', label='control bucket', required=True, |
586 | + help='the globally unique S3 bucket name'), |
587 | + fields.ChoiceField( |
588 | + 'region', choices=ec2_regions, default='us-east-1', |
589 | + label='region', required=False, help='the ec2 region to use'), |
590 | + is_default_field, |
591 | + ), |
592 | + } |
593 | + env_type_db['local'] = { |
594 | + 'description': ( |
595 | + 'The LXC local provider enables you to run Juju on a single ' |
596 | + 'system like your local computer or a single server. ' |
597 | + 'See https://juju.ubuntu.com/docs/config-LXC.html for more ' |
598 | + 'details on the local provider configuration.' |
599 | + ), |
600 | + 'fields': ( |
601 | + provider_field, |
602 | + name_field, |
603 | + admin_secret_field, |
604 | + default_series_field, |
605 | + fields.StringField( |
606 | + 'root-dir', label='root dir', required=False, |
607 | + default='~/.juju/local', |
608 | + help='the directory that is used for the storage files'), |
609 | + fields.IntField( |
610 | + 'storage-port', min_value=1, max_value=65535, |
611 | + label='storage port', required=False, default=8040, |
612 | + help='override if you have multiple local providers, ' |
613 | + 'or if the default port is used by another program'), |
614 | + fields.IntField( |
615 | + 'shared-storage-port', min_value=1, max_value=65535, |
616 | + label='shared storage port', required=False, default=8041, |
617 | + help='override if you have multiple local providers, ' |
618 | + 'or if the default port is used by another program'), |
619 | + fields.StringField( |
620 | + 'network-bridge', label='network bridge', required=False, |
621 | + default='lxcbr0', help='the LXC bridge interface to use'), |
622 | + is_default_field, |
623 | + ), |
624 | } |
625 | return env_type_db |
626 | |
627 | @@ -534,54 +559,3 @@ |
628 | if info_parts: |
629 | info = ' ({})'.format(', '.join(info_parts)) |
630 | return env_data['name'] + info |
631 | - |
632 | - |
633 | -def parse_env_file(env_file, env_name): |
634 | - """Parse the provided Juju environments.yaml file. |
635 | - |
636 | - Return a tuple containing: |
637 | - - provider type |
638 | - - admin secret |
639 | - - default series (None if not set) |
640 | - |
641 | - Raise a ValueError if: |
642 | - - the environment file is not found; |
643 | - - the environment file contents are not parsable by YAML; |
644 | - - the YAML contents are not properly structured; |
645 | - - the environment named envname is not found; |
646 | - - the environment does not include the "type" field; |
647 | - - the environment does not include the "admin_secret" field. |
648 | - """ |
649 | - # Load the Juju environments file. |
650 | - try: |
651 | - environments_file = open(env_file.encode('utf-8')) |
652 | - except IOError as err: |
653 | - msg = b'unable to open environments file: {}'.format(err) |
654 | - raise ValueError(msg) |
655 | - # Parse the Juju environments file. |
656 | - try: |
657 | - environments = serializers.yaml_load(environments_file) |
658 | - except Exception as err: |
659 | - msg = b'unable to parse environments file {}: {}' |
660 | - raise ValueError(msg.format(env_file.encode('utf-8'), err)) |
661 | - # Retrieve the information about the current environment. |
662 | - try: |
663 | - environment = environments.get('environments', {}).get(env_name) |
664 | - except AttributeError as err: |
665 | - msg = 'invalid YAML contents in {}: {}'.format(env_file, environments) |
666 | - raise ValueError(msg.encode('utf-8')) |
667 | - if environment is None: |
668 | - msg = 'environment {} not found in {}'.format(env_name, env_file) |
669 | - raise ValueError(msg.encode('utf-8')) |
670 | - # Retrieve the provider type and the admin secret. |
671 | - env_type = environment.get('type') |
672 | - if env_type is None: |
673 | - msg = '{} provider type not found in {}'.format(env_name, env_file) |
674 | - raise ValueError(msg.encode('utf-8')) |
675 | - admin_secret = environment.get('admin-secret') |
676 | - if admin_secret is None: |
677 | - msg = '{} admin secret not found in {}'.format(env_name, env_file) |
678 | - raise ValueError(msg.encode('utf-8')) |
679 | - # The default-series is not required to be set so None is ok. |
680 | - default_series = environment.get('default-series') |
681 | - return env_type, admin_secret, default_series |
682 | |
683 | === modified file 'quickstart/tests/cli/test_views.py' |
684 | --- quickstart/tests/cli/test_views.py 2014-01-07 15:53:57 +0000 |
685 | +++ quickstart/tests/cli/test_views.py 2014-01-13 15:31:13 +0000 |
686 | @@ -216,6 +216,28 @@ |
687 | # loop cycle. |
688 | mock_env_edit.reset_mock() |
689 | |
690 | + def test_create_and_bootstrap_local_environment_clicked(self): |
691 | + # When there are no environments in the env_db the view exposes an |
692 | + # option to automatically create and bootstrap a new local environment. |
693 | + # If that option is clicked, the view quits the application returning |
694 | + # the newly created env_data. |
695 | + env_db = envs.create_empty_env_db() |
696 | + views.env_index(self.app, self.env_type_db, env_db, self.save_callable) |
697 | + buttons = self.get_widgets_in_contents( |
698 | + filter_function=self.is_a(ui.MenuButton)) |
699 | + # The "create and bootstrap" button is the first one in the contents. |
700 | + create_button = buttons[0] |
701 | + self.assertEqual( |
702 | + '\N{BULLET} automatically create and bootstrap a local ' |
703 | + 'environment', cli_helpers.get_button_caption(create_button)) |
704 | + # An AppExit is raised clicking the button. |
705 | + with self.assertRaises(ui.AppExit) as context_manager: |
706 | + cli_helpers.emit(create_button) |
707 | + new_env_db, env_data = context_manager.exception.return_value |
708 | + # The environments database is no longer empty. |
709 | + self.assertIn('local', new_env_db['environments']) |
710 | + self.assertEqual(envs.get_env_data(new_env_db, 'local'), env_data) |
711 | + |
712 | def test_selected_environment(self): |
713 | # The default environment is already selected in the list. |
714 | env_db = helpers.make_env_db(default='lxc') |
715 | @@ -604,6 +626,26 @@ |
716 | self.app, self.env_type_db, self.env_db, self.save_callable, |
717 | new_env_data) |
718 | |
719 | + @mock.patch('quickstart.cli.views.env_detail') |
720 | + def test_save_empty_db(self, mock_env_detail): |
721 | + # If the env_db is empty, the new environment is set as default. |
722 | + self.env_db = envs.create_empty_env_db() |
723 | + changes = { |
724 | + 'name': 'lxc', |
725 | + 'admin-secret': 'Secret!', |
726 | + 'is-default': False, |
727 | + } |
728 | + with self.patch_create_form(changes=changes): |
729 | + self.call_view(env_type='local') |
730 | + save_button = self.get_control_buttons()[0] |
731 | + cli_helpers.emit(save_button) |
732 | + expected_new_env_data = changes.copy() |
733 | + expected_new_env_data.update({'type': 'local', 'is-default': True}) |
734 | + envs.set_env_data(self.env_db, None, expected_new_env_data) |
735 | + mock_env_detail.assert_called_once_with( |
736 | + self.app, self.env_type_db, self.env_db, self.save_callable, |
737 | + expected_new_env_data) |
738 | + |
739 | def test_save_invalid_form_data(self): |
740 | # Errors are displayed if the user tries to save invalid data. |
741 | changes = {'admin-secret': ''} |
742 | |
743 | === modified file 'quickstart/tests/helpers.py' |
744 | --- quickstart/tests/helpers.py 2013-12-19 08:52:34 +0000 |
745 | +++ quickstart/tests/helpers.py 2014-01-13 15:31:13 +0000 |
746 | @@ -109,7 +109,10 @@ |
747 | 'aws': { |
748 | 'admin-secret': 'Secret!', |
749 | 'type': 'ec2', |
750 | - 'default-series': 'edgy', |
751 | + 'default-series': 'saucy', |
752 | + 'access-key': 'AccessKey', |
753 | + 'secret-key': 'SeceretKey', |
754 | + 'control-bucket': 'ControlBucket', |
755 | }, |
756 | }, |
757 | }) |
758 | |
759 | === modified file 'quickstart/tests/models/test_envs.py' |
760 | --- quickstart/tests/models/test_envs.py 2013-12-20 14:47:35 +0000 |
761 | +++ quickstart/tests/models/test_envs.py 2014-01-13 15:31:13 +0000 |
762 | @@ -83,6 +83,14 @@ |
763 | mock_call.assert_called_once_with('juju', 'switch') |
764 | |
765 | |
766 | +class TestCreateEmptyEnvDb(unittest.TestCase): |
767 | + |
768 | + def test_resulting_env_db(self): |
769 | + # The function surprisingly returns an empty environments database. |
770 | + env_db = envs.create_empty_env_db() |
771 | + self.assertEqual({'environments': {}}, env_db) |
772 | + |
773 | + |
774 | class TestLoad( |
775 | helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin, |
776 | unittest.TestCase): |
777 | @@ -104,6 +112,12 @@ |
778 | expected = 'unable to parse environments file {}'.format(env_file) |
779 | self.assertIn(expected, bytes(context_manager.exception)) |
780 | |
781 | + def test_empty_file(self): |
782 | + # An empty environments database is returned if the file is empty. |
783 | + env_file = self.make_env_file('') |
784 | + env_db = envs.load(env_file) |
785 | + self.assertEqual({'environments': {}}, env_db) |
786 | + |
787 | def test_invalid_yaml_contents(self): |
788 | # A ValueError is raised if the environments file is not well formed. |
789 | env_file = self.make_env_file('a-string') |
790 | @@ -258,7 +272,7 @@ |
791 | def test_env_not_found(self): |
792 | # A ValueError is raised if an environment with the given name is not |
793 | # found in the environments dictionary. |
794 | - with self.assert_value_error("environment u'no-such' not found"): |
795 | + with self.assert_value_error("environment no-such not found"): |
796 | envs.get_env_data(self.env_db, 'no-such') |
797 | |
798 | def test_resulting_env_data(self): |
799 | @@ -481,6 +495,35 @@ |
800 | self.assertNotIn('default', self.env_db) |
801 | |
802 | |
803 | +class TestCreateLocalEnvData(unittest.TestCase): |
804 | + |
805 | + def setUp(self): |
806 | + # Store the env_type_db. |
807 | + self.env_type_db = envs.get_env_type_db() |
808 | + |
809 | + def test_not_default(self): |
810 | + # The resulting env_data is correctly structured for non default envs. |
811 | + env_data = envs.create_local_env_data( |
812 | + self.env_type_db, 'my-lxc', is_default=False) |
813 | + # The function is not pure: auto-generated values change each time the |
814 | + # function is called. For local environments, the only auto-generated |
815 | + # value should be the admin-secret. |
816 | + admin_secret = env_data.pop('admin-secret', '') |
817 | + self.assertNotEqual(0, len(admin_secret)) |
818 | + expected = {'type': 'local', 'name': 'my-lxc', 'is-default': False} |
819 | + self.assertEqual(expected, env_data) |
820 | + |
821 | + def test_default(self): |
822 | + # The resulting env_data is correctly structured for default envs. |
823 | + env_data = envs.create_local_env_data( |
824 | + self.env_type_db, 'my-default', is_default=True) |
825 | + # See the comment about auto-generated fields in the test method above. |
826 | + admin_secret = env_data.pop('admin-secret', '') |
827 | + self.assertNotEqual(0, len(admin_secret)) |
828 | + expected = {'type': 'local', 'name': 'my-default', 'is-default': True} |
829 | + self.assertEqual(expected, env_data) |
830 | + |
831 | + |
832 | class TestRemoveEnv( |
833 | EnvDataTestsMixin, helpers.ValueErrorTestsMixin, unittest.TestCase): |
834 | |
835 | @@ -913,81 +956,3 @@ |
836 | env_data = {'name': 'lxc', 'type': None, 'is-default': True} |
837 | description = envs.get_env_short_description(env_data) |
838 | self.assertEqual('lxc (default)', description) |
839 | - |
840 | - |
841 | -class TestParseEnvFile( |
842 | - helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin, |
843 | - unittest.TestCase): |
844 | - |
845 | - def test_no_file(self): |
846 | - # A ValueError is raised if the environments file is not found. |
847 | - expected = ( |
848 | - 'unable to open environments file: ' |
849 | - "[Errno 2] No such file or directory: '/no/such/file.yaml'" |
850 | - ) |
851 | - with self.assert_value_error(expected): |
852 | - envs.parse_env_file('/no/such/file.yaml', 'ec2') |
853 | - |
854 | - def test_invalid_yaml(self): |
855 | - # A ValueError is raised if the environments file is not a valid YAML. |
856 | - env_file = self.make_env_file(':') |
857 | - with self.assertRaises(ValueError) as context_manager: |
858 | - envs.parse_env_file(env_file, 'ec2') |
859 | - expected = 'unable to parse environments file {}'.format(env_file) |
860 | - self.assertIn(expected, bytes(context_manager.exception)) |
861 | - |
862 | - def test_invalid_yaml_contents(self): |
863 | - # A ValueError is raised if the environments file is not well formed. |
864 | - env_file = self.make_env_file('a-string') |
865 | - expected = 'invalid YAML contents in {}: a-string'.format(env_file) |
866 | - with self.assert_value_error(expected): |
867 | - envs.parse_env_file(env_file, 'ec2') |
868 | - |
869 | - def test_no_env(self): |
870 | - # A ValueError is raised if the environment is not found in the YAML. |
871 | - contents = yaml.safe_dump({'environments': {'local': {}}}) |
872 | - env_file = self.make_env_file(contents) |
873 | - expected = 'environment ec2 not found in {}'.format(env_file) |
874 | - with self.assert_value_error(expected): |
875 | - envs.parse_env_file(env_file, 'ec2') |
876 | - |
877 | - def test_no_provider_type(self): |
878 | - # A ValueError is raised if the provider type is not included in the |
879 | - # environment info. |
880 | - contents = yaml.safe_dump({'environments': {'aws': {}}}) |
881 | - env_file = self.make_env_file(contents) |
882 | - expected = 'aws provider type not found in {}'.format(env_file) |
883 | - with self.assert_value_error(expected): |
884 | - envs.parse_env_file(env_file, 'aws') |
885 | - |
886 | - def test_no_admin_secret(self): |
887 | - # A ValueError is raised if the admin secret is not included in the |
888 | - # environment info. |
889 | - contents = yaml.safe_dump({'environments': {'aws': {'type': 'ec2'}}}) |
890 | - env_file = self.make_env_file(contents) |
891 | - expected = 'aws admin secret not found in {}'.format(env_file) |
892 | - with self.assert_value_error(expected): |
893 | - envs.parse_env_file(env_file, 'aws') |
894 | - |
895 | - def test_success_no_default_series(self): |
896 | - # The environment provider type and admin secret are returned. |
897 | - # Default series is None. |
898 | - contents = yaml.safe_dump({ |
899 | - 'environments': {'aws': {'admin-secret': 'Secret!', 'type': 'ec2'}} |
900 | - }) |
901 | - env_file = self.make_env_file(contents) |
902 | - env_type, admin_secret, default_series = ( |
903 | - envs.parse_env_file(env_file, 'aws')) |
904 | - self.assertEqual('ec2', env_type) |
905 | - self.assertEqual('Secret!', admin_secret) |
906 | - self.assertIsNone(default_series) |
907 | - |
908 | - def test_success_with_default_series(self): |
909 | - # The environment provider type and admin secret are returned. |
910 | - # Default series is also returned. |
911 | - env_file = self.make_env_file() |
912 | - env_type, admin_secret, default_series = ( |
913 | - envs.parse_env_file(env_file, 'aws')) |
914 | - self.assertEqual('ec2', env_type) |
915 | - self.assertEqual('Secret!', admin_secret) |
916 | - self.assertEqual('edgy', default_series) |
917 | |
918 | === modified file 'quickstart/tests/test_app.py' |
919 | --- quickstart/tests/test_app.py 2013-12-19 03:29:39 +0000 |
920 | +++ quickstart/tests/test_app.py 2014-01-13 15:31:13 +0000 |
921 | @@ -377,40 +377,6 @@ |
922 | |
923 | |
924 | @helpers.mock_print |
925 | -class TestEnsureEnvironments( |
926 | - helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
927 | - |
928 | - def test_inits(self, mock_print): |
929 | - side_effects = ( |
930 | - (0, '', ''), |
931 | - (0, '', ''), |
932 | - ) |
933 | - with self.patch_multiple_calls(side_effects): |
934 | - self.assertEqual(app.ensure_environments(), 'local') |
935 | - mock_print.assert_has_calls([ |
936 | - mock.call('environments file not found; running juju init'), |
937 | - mock.call('defaulting to local environment for a new ' |
938 | - 'environments file') |
939 | - ]) |
940 | - |
941 | - def test_failure(self, mock_print): |
942 | - side_effects = ( |
943 | - (1, '', 'Error!'), # Fail on init. |
944 | - (0, '', ''), |
945 | - ) |
946 | - with self.assert_program_exit('Error!'): |
947 | - with self.patch_multiple_calls(side_effects): |
948 | - app.ensure_environments() |
949 | - side_effects = ( |
950 | - (0, '', ''), |
951 | - (1, '', 'Error!'), # Fail on switch. |
952 | - ) |
953 | - with self.assert_program_exit('Error!'): |
954 | - with self.patch_multiple_calls(side_effects): |
955 | - app.ensure_environments() |
956 | - |
957 | - |
958 | -@helpers.mock_print |
959 | class TestBootstrap( |
960 | helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
961 | |
962 | |
963 | === modified file 'quickstart/tests/test_manage.py' |
964 | --- quickstart/tests/test_manage.py 2013-12-06 18:16:02 +0000 |
965 | +++ quickstart/tests/test_manage.py 2014-01-13 15:31:13 +0000 |
966 | @@ -19,6 +19,7 @@ |
967 | from __future__ import unicode_literals |
968 | |
969 | import argparse |
970 | +from contextlib import contextmanager |
971 | import logging |
972 | import os |
973 | import shutil |
974 | @@ -34,6 +35,8 @@ |
975 | manage, |
976 | settings, |
977 | ) |
978 | +from quickstart.cli import views |
979 | +from quickstart.models import envs |
980 | from quickstart.tests import helpers |
981 | |
982 | |
983 | @@ -244,24 +247,182 @@ |
984 | self.assertFalse(self.parser.error.called, charm_url) |
985 | |
986 | |
987 | -class TestValidateEnv(helpers.EnvFileTestsMixin, unittest.TestCase): |
988 | - |
989 | - def setUp(self): |
990 | - self.parser = mock.Mock() |
991 | - |
992 | - def make_options(self, env_file, env_name=None): |
993 | +class TestRetrieveEnvDb(helpers.EnvFileTestsMixin, unittest.TestCase): |
994 | + |
995 | + def setUp(self): |
996 | + self.parser = mock.Mock() |
997 | + |
998 | + def test_existing_env_file(self): |
999 | + # The env_db is correctly retrieved from an existing environments file. |
1000 | + env_file = self.make_env_file() |
1001 | + env_db = manage._retrieve_env_db(self.parser, env_file=env_file) |
1002 | + self.assertEqual(yaml.safe_load(self.valid_contents), env_db) |
1003 | + |
1004 | + def test_error_parsing_env_file(self): |
1005 | + # A parser error is invoked if an error occurs parsing the env file. |
1006 | + env_file = self.make_env_file('so-bad') |
1007 | + manage._retrieve_env_db(self.parser, env_file=env_file) |
1008 | + self.parser.error.assert_called_once_with( |
1009 | + 'invalid YAML contents in {}: so-bad'.format(env_file)) |
1010 | + |
1011 | + def test_missing_env_file(self): |
1012 | + # An empty env_db is returned if the environments file does not exist. |
1013 | + env_db = manage._retrieve_env_db(self.parser, env_file=None) |
1014 | + self.assertEqual(envs.create_empty_env_db(), env_db) |
1015 | + |
1016 | + |
1017 | +@mock.patch('quickstart.manage.envs.save') |
1018 | +class TestCreateSaveCallable(unittest.TestCase): |
1019 | + |
1020 | + def setUp(self): |
1021 | + self.parser = mock.Mock() |
1022 | + self.env_file = '/tmp/envfile.yaml' |
1023 | + self.env_db = helpers.make_env_db() |
1024 | + self.save_callable = manage._create_save_callable( |
1025 | + self.parser, self.env_file) |
1026 | + |
1027 | + def test_saved(self, mock_save): |
1028 | + # The returned function correctly saves the new environments database. |
1029 | + self.save_callable(self.env_db) |
1030 | + mock_save.assert_called_once_with(self.env_file, self.env_db) |
1031 | + self.assertFalse(self.parser.error.called) |
1032 | + |
1033 | + def test_error(self, mock_save): |
1034 | + # The returned function uses the parser to exit the program if an error |
1035 | + # occurs while saving the new environments database. |
1036 | + mock_save.side_effect = OSError(b'bad wolf') |
1037 | + self.save_callable(self.env_db) |
1038 | + mock_save.assert_called_once_with(self.env_file, self.env_db) |
1039 | + self.parser.error.assert_called_once_with('bad wolf') |
1040 | + |
1041 | + |
1042 | +class TestStartInteractiveSession( |
1043 | + helpers.EnvFileTestsMixin, unittest.TestCase): |
1044 | + |
1045 | + def setUp(self): |
1046 | + # Set up a parser, the environments metadata, an environments file and |
1047 | + # a testing env_db. |
1048 | + self.parser = mock.Mock() |
1049 | + self.env_type_db = envs.get_env_type_db() |
1050 | + self.env_file = self.make_env_file() |
1051 | + self.env_db = envs.load(self.env_file) |
1052 | + |
1053 | + @contextmanager |
1054 | + def patch_interactive_mode(self, env_db, return_value): |
1055 | + """Patch the quickstart.cli.views.show function. |
1056 | + |
1057 | + Ensure the interactive mode is started by the code in the context block |
1058 | + passing the given env_db. Make the view return the given return_value. |
1059 | + """ |
1060 | + create_save_callable_path = 'quickstart.manage._create_save_callable' |
1061 | + mock_show = mock.Mock(return_value=return_value) |
1062 | + with mock.patch(create_save_callable_path) as mock_save_callable: |
1063 | + with mock.patch('quickstart.manage.views.show', mock_show): |
1064 | + yield |
1065 | + mock_save_callable.assert_called_once_with(self.parser, self.env_file) |
1066 | + mock_show.assert_called_once_with( |
1067 | + views.env_index, self.env_type_db, env_db, |
1068 | + mock_save_callable()) |
1069 | + |
1070 | + def test_resulting_env_data(self): |
1071 | + # The interactive session can be used to select an environment, in |
1072 | + # which case the function returns the corresponding env_data. |
1073 | + env_data = envs.get_env_data(self.env_db, 'aws') |
1074 | + with self.patch_interactive_mode(self.env_db, [self.env_db, env_data]): |
1075 | + obtained_env_data = manage._start_interactive_session( |
1076 | + self.parser, self.env_type_db, self.env_db, self.env_file) |
1077 | + self.assertEqual(env_data, obtained_env_data) |
1078 | + |
1079 | + @helpers.mock_print |
1080 | + def test_modified_environments(self, mock_print): |
1081 | + # The function informs the user that environments have been modified |
1082 | + # during the interactive session. |
1083 | + env_data = envs.get_env_data(self.env_db, 'aws') |
1084 | + new_env_db = helpers.make_env_db() |
1085 | + with self.patch_interactive_mode(self.env_db, [new_env_db, env_data]): |
1086 | + manage._start_interactive_session( |
1087 | + self.parser, self.env_type_db, self.env_db, self.env_file) |
1088 | + mock_print.assert_called_once_with( |
1089 | + 'changes to the environments file have been saved') |
1090 | + |
1091 | + @mock.patch('sys.exit') |
1092 | + def test_interactive_mode_quit(self, mock_exit): |
1093 | + # If the user explicitly quits the interactive mode, the program exits |
1094 | + # without proceeding with the environment bootstrapping. |
1095 | + with self.patch_interactive_mode(self.env_db, [self.env_db, None]): |
1096 | + manage._start_interactive_session( |
1097 | + self.parser, self.env_type_db, self.env_db, self.env_file) |
1098 | + mock_exit.assert_called_once_with('quitting') |
1099 | + |
1100 | + |
1101 | +class TestRetrieveEnvData(unittest.TestCase): |
1102 | + |
1103 | + def setUp(self): |
1104 | + # Set up a parser, the environments metadata and a testing env_db. |
1105 | + self.parser = mock.Mock() |
1106 | + self.env_type_db = envs.get_env_type_db() |
1107 | + self.env_db = helpers.make_env_db() |
1108 | + |
1109 | + def test_resulting_env_data(self): |
1110 | + # The env_data is correctly validated and returned. |
1111 | + expected_env_data = envs.get_env_data(self.env_db, 'lxc') |
1112 | + env_data = manage._retrieve_env_data( |
1113 | + self.parser, self.env_type_db, self.env_db, 'lxc') |
1114 | + self.assertEqual(expected_env_data, env_data) |
1115 | + |
1116 | + def test_error_environment_not_found(self): |
1117 | + # A parser error is invoked if the provided environment is not included |
1118 | + # in the environments database. |
1119 | + manage._retrieve_env_data( |
1120 | + self.parser, self.env_type_db, self.env_db, 'no-such') |
1121 | + self.parser.error.assert_called_once_with( |
1122 | + 'environment no-such not found') |
1123 | + |
1124 | + def test_error_environment_not_valid(self): |
1125 | + # A parser error is invoked if the selected environment is not valid. |
1126 | + manage._retrieve_env_data( |
1127 | + self.parser, self.env_type_db, self.env_db, 'local-with-errors') |
1128 | + self.parser.error.assert_called_once_with( |
1129 | + 'cannot use the local-with-errors environment:\n' |
1130 | + 'the storage port field requires an integer value\n' |
1131 | + 'a value is required for the admin secret field') |
1132 | + |
1133 | + |
1134 | +class TestSetupEnv(helpers.EnvFileTestsMixin, unittest.TestCase): |
1135 | + |
1136 | + def setUp(self): |
1137 | + self.parser = mock.Mock() |
1138 | + |
1139 | + def make_options(self, env_file, env_name=None, interactive=False): |
1140 | """Return a mock options object which includes the passed arguments.""" |
1141 | - return mock.Mock(env_file=env_file, env_name=env_name) |
1142 | + return mock.Mock( |
1143 | + env_file=env_file, |
1144 | + env_name=env_name, |
1145 | + interactive=interactive, |
1146 | + ) |
1147 | + |
1148 | + def patch_interactive_mode(self, return_value): |
1149 | + """Patch the quickstart.manage._start_interactive_session function. |
1150 | + |
1151 | + Make the mocked function return the given return_value. |
1152 | + """ |
1153 | + mock_start_interactive_session = mock.Mock(return_value=return_value) |
1154 | + return mock.patch( |
1155 | + 'quickstart.manage._start_interactive_session', |
1156 | + mock_start_interactive_session) |
1157 | |
1158 | def test_resulting_options(self): |
1159 | # The options object is correctly set up. |
1160 | env_file = self.make_env_file() |
1161 | - options = self.make_options(env_file, env_name='aws') |
1162 | - manage._validate_env(options, self.parser) |
1163 | + options = self.make_options( |
1164 | + env_file, env_name='aws', interactive=False) |
1165 | + manage._setup_env(options, self.parser) |
1166 | self.assertEqual('Secret!', options.admin_secret) |
1167 | self.assertEqual(env_file, options.env_file) |
1168 | self.assertEqual('aws', options.env_name) |
1169 | self.assertEqual('ec2', options.env_type) |
1170 | + self.assertEqual('saucy', options.default_series) |
1171 | + self.assertFalse(options.interactive) |
1172 | |
1173 | def test_expand_user(self): |
1174 | # The ~ construct is correctly expanded in the validation process. |
1175 | @@ -274,40 +435,74 @@ |
1176 | path = '~/{}'.format(filename) |
1177 | options = self.make_options(env_file=path, env_name='aws') |
1178 | with mock.patch('os.environ', {'HOME': base_path}): |
1179 | - manage._validate_env(options, self.parser) |
1180 | + manage._setup_env(options, self.parser) |
1181 | self.assertEqual(env_file, options.env_file) |
1182 | |
1183 | def test_no_env_name(self): |
1184 | - # A parser error is invoked if the environment name is missing. |
1185 | - options = self.make_options(self.make_env_file()) |
1186 | - manage._validate_env(options, self.parser) |
1187 | + # A parser error is invoked if the environment name is missing and |
1188 | + # interactive mode is disabled. |
1189 | + options = self.make_options(self.make_env_file(), interactive=False) |
1190 | + manage._setup_env(options, self.parser) |
1191 | self.assertTrue(self.parser.error.called) |
1192 | message = self.parser.error.call_args[0][0] |
1193 | self.assertIn('unable to find an environment name to use', message) |
1194 | |
1195 | - def test_error_parsing_env_file(self): |
1196 | - # A parser error is invoked if an error occurs parsing the env file. |
1197 | - env_file = self.make_env_file() |
1198 | - options = self.make_options(env_file, env_name='no-such') |
1199 | - manage._validate_env(options, self.parser) |
1200 | - expected = 'environment no-such not found in {}'.format(env_file) |
1201 | - self.parser.error.assert_called_once_with(expected) |
1202 | - |
1203 | def test_local_provider(self): |
1204 | - # The parser exits with an error if the provided environment name |
1205 | - # refers to a local provider type. |
1206 | + # Local environments are correctly handled. |
1207 | contents = yaml.safe_dump({ |
1208 | 'environments': { |
1209 | 'lxc': {'admin-secret': 'Secret!', 'type': 'local'}, |
1210 | }, |
1211 | }) |
1212 | env_file = self.make_env_file(contents) |
1213 | - options = self.make_options(env_file, env_name='lxc') |
1214 | - manage._validate_env(options, self.parser) |
1215 | + options = self.make_options( |
1216 | + env_file, env_name='lxc', interactive=False) |
1217 | + manage._setup_env(options, self.parser) |
1218 | self.assertEqual('Secret!', options.admin_secret) |
1219 | self.assertEqual(env_file, options.env_file) |
1220 | self.assertEqual('lxc', options.env_name) |
1221 | self.assertEqual('local', options.env_type) |
1222 | + self.assertIsNone(options.default_series) |
1223 | + self.assertFalse(options.interactive) |
1224 | + |
1225 | + def test_interactive_mode(self): |
1226 | + # The interactive mode is started properly if the corresponding option |
1227 | + # flag is set. |
1228 | + env_file = self.make_env_file() |
1229 | + options = self.make_options(env_file, interactive=True) |
1230 | + # Simulate the user did not make any changes to the env_db from the |
1231 | + # interactive session. |
1232 | + env_db = yaml.load(self.valid_contents) |
1233 | + # Simulate the aws environment has been selected and started from the |
1234 | + # interactive session. |
1235 | + env_data = envs.get_env_data(env_db, 'aws') |
1236 | + get_env_type_db_path = 'quickstart.models.envs.get_env_type_db' |
1237 | + with mock.patch(get_env_type_db_path) as mock_get_env_type_db: |
1238 | + with self.patch_interactive_mode(env_data) as mock_interactive: |
1239 | + manage._setup_env(options, self.parser) |
1240 | + mock_interactive.assert_called_once_with( |
1241 | + self.parser, mock_get_env_type_db(), env_db, env_file) |
1242 | + # The options is updated with data from the selected environment. |
1243 | + self.assertEqual('Secret!', options.admin_secret) |
1244 | + self.assertEqual(env_file, options.env_file) |
1245 | + self.assertEqual('aws', options.env_name) |
1246 | + self.assertEqual('ec2', options.env_type) |
1247 | + self.assertEqual('saucy', options.default_series) |
1248 | + self.assertTrue(options.interactive) |
1249 | + |
1250 | + @helpers.mock_print |
1251 | + def test_missing_env_file(self, mock_print): |
1252 | + # If the environments file does not exist, an empty env_db is created |
1253 | + # in memory and interactive mode is forced. |
1254 | + new_env_db = helpers.make_env_db() |
1255 | + env_data = envs.get_env_data(new_env_db, 'lxc') |
1256 | + options = self.make_options('__no_such_env_file__', interactive=False) |
1257 | + # In this case, we expect the interactive mode to be started and the |
1258 | + # env_db passed to the view to be an empty one. |
1259 | + with self.patch_interactive_mode(env_data) as mock_interactive: |
1260 | + manage._setup_env(options, self.parser) |
1261 | + self.assertTrue(mock_interactive.called) |
1262 | + self.assertTrue(options.interactive) |
1263 | |
1264 | |
1265 | class TestConvertOptionsToUnicode(unittest.TestCase): |
1266 | @@ -334,7 +529,7 @@ |
1267 | self.assertIsNone(options.opt2) |
1268 | |
1269 | |
1270 | -@mock.patch('quickstart.manage._validate_env', mock.Mock()) |
1271 | +@mock.patch('quickstart.manage._setup_env', mock.Mock()) |
1272 | class TestSetup(unittest.TestCase): |
1273 | |
1274 | def patch_get_default_env_name(self, env_name=None): |
1275 | @@ -388,13 +583,6 @@ |
1276 | output = stdout_write.call_args[0][0] |
1277 | self.assertIn('The name of the Juju environment to use (hp)\n', output) |
1278 | |
1279 | - def test_init_environment(self): |
1280 | - # The program calls ensure_environments if env_name is None. |
1281 | - with mock.patch('quickstart.app.ensure_environments', |
1282 | - return_value='local') as ensure_environments: |
1283 | - self.call_setup([], env_name=None, exit_called=False) |
1284 | - self.assertTrue(ensure_environments.called) |
1285 | - |
1286 | def test_description(self): |
1287 | # The program description is properly printed out as required by juju. |
1288 | with helpers.mock_print as mock_print: |
1289 | |
1290 | === modified file 'quickstart/tests/test_utils.py' |
1291 | --- quickstart/tests/test_utils.py 2013-12-19 08:47:57 +0000 |
1292 | +++ quickstart/tests/test_utils.py 2014-01-13 15:31:13 +0000 |
1293 | @@ -21,7 +21,10 @@ |
1294 | import datetime |
1295 | import httplib |
1296 | import json |
1297 | +import os |
1298 | +import shutil |
1299 | import socket |
1300 | +import tempfile |
1301 | import unittest |
1302 | import urllib2 |
1303 | |
1304 | @@ -378,6 +381,55 @@ |
1305 | mock_call.assert_called_once_with('lsb_release', '-cs') |
1306 | |
1307 | |
1308 | +class TestMkdir(unittest.TestCase): |
1309 | + |
1310 | + def setUp(self): |
1311 | + # Set up a playground directory. |
1312 | + self.playground = tempfile.mkdtemp() |
1313 | + self.addCleanup(shutil.rmtree, self.playground) |
1314 | + |
1315 | + def test_create_dir(self): |
1316 | + # A directory is correctly created. |
1317 | + path = os.path.join(self.playground, 'foo') |
1318 | + utils.mkdir(path) |
1319 | + self.assertTrue(os.path.isdir(path)) |
1320 | + |
1321 | + def test_intermediate_dirs(self): |
1322 | + # All intermediate directories are created. |
1323 | + path = os.path.join(self.playground, 'foo', 'bar', 'leaf') |
1324 | + utils.mkdir(path) |
1325 | + self.assertTrue(os.path.isdir(path)) |
1326 | + |
1327 | + def test_expand_user(self): |
1328 | + # The ~ construction is expanded. |
1329 | + with mock.patch('os.environ', {'HOME': self.playground}): |
1330 | + utils.mkdir('~/in/my/home') |
1331 | + path = os.path.join(self.playground, 'in', 'my', 'home') |
1332 | + self.assertTrue(os.path.isdir(path)) |
1333 | + |
1334 | + def test_existing_dir(self): |
1335 | + # The function exits without errors if the target directory exists. |
1336 | + path = os.path.join(self.playground, 'foo') |
1337 | + os.mkdir(path) |
1338 | + utils.mkdir(path) |
1339 | + |
1340 | + def test_existing_file(self): |
1341 | + # An OSError is raised if a file already exists in the target path. |
1342 | + path = os.path.join(self.playground, 'foo') |
1343 | + with open(path, 'w'): |
1344 | + with self.assertRaises(OSError): |
1345 | + utils.mkdir(path) |
1346 | + |
1347 | + def test_failure(self): |
1348 | + # Errors are correctly re-raised. |
1349 | + path = os.path.join(self.playground, 'foo') |
1350 | + os.chmod(self.playground, 0000) |
1351 | + self.addCleanup(os.chmod, self.playground, 0700) |
1352 | + with self.assertRaises(OSError): |
1353 | + utils.mkdir(os.path.join(path)) |
1354 | + self.assertFalse(os.path.exists(path)) |
1355 | + |
1356 | + |
1357 | class TestParseBundle( |
1358 | helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin, |
1359 | unittest.TestCase): |
1360 | |
1361 | === modified file 'quickstart/utils.py' |
1362 | --- quickstart/utils.py 2013-12-19 08:47:57 +0000 |
1363 | +++ quickstart/utils.py 2014-01-13 15:31:13 +0000 |
1364 | @@ -23,6 +23,7 @@ |
1365 | |
1366 | import collections |
1367 | import datetime |
1368 | +import errno |
1369 | import httplib |
1370 | import json |
1371 | import logging |
1372 | @@ -204,6 +205,21 @@ |
1373 | return output.strip() |
1374 | |
1375 | |
1376 | +def mkdir(path): |
1377 | + """Create a leaf directory and all intermediate ones. |
1378 | + |
1379 | + Also expand ~ and ~user constructions. |
1380 | + If path exists and it's a directory, return without errors. |
1381 | + """ |
1382 | + path = os.path.expanduser(path) |
1383 | + try: |
1384 | + os.makedirs(path) |
1385 | + except OSError as err: |
1386 | + # Re-raise the error if the target path exists but it is not a dir. |
1387 | + if (err.errno != errno.EEXIST) or (not os.path.isdir(path)): |
1388 | + raise |
1389 | + |
1390 | + |
1391 | def parse_bundle(bundle_yaml, bundle_name=None): |
1392 | """Parse the provided bundle YAML encoded contents. |
1393 |
Reviewers: mp+201163_ code.launchpad. net,
Message:
Please take a look.
Description:
Integrate the env management functionality.
This branch enables the environment management
Urwid interactive session feature in quickstart,
but also includes some other improvements, described
below.
Exposed a way to let the user quickly and_start_ local_env) to the
create and bootstrap a local env if no environments
are already configured. This is implemented
adding a closure (create_
env_index view: this could have been implemented
as a separate function as well (more easily testable,
less surprising), but I decided to stick with the
pattern used by the other views for now.
Changed the env_edit view so that the first created
environment is automatically set as default.
Renamed manage. _validate_ env to manage._setup_env: save_callable that can be easily be extended
the function now does a lot more than validation, e.g.
it lunches the interactive session, handles the
case when the Juju home is not yet configured, etc.
This function also sets up a save_callable to be used
by views. I could have used partial to create such a
function, but I decided to implement a HO
_create_
later in order to enable the backup functionality.
Bumped version up.
The diff is very long, my apologies, but you can safely models. envs is just a copy/paste replacement
ignore deleted code:
- the demo application, no longer required;
- the ensure_environments function + its tests;
- envs.parse_env_file + its tests (
replaced by envs.load + envs.validate).
Moreover, the env_type_db definition in
quickstart.
of the original one: the only difference is that
an ordered dict is used in place of the usual dict.
This change has been introduced so that views
can list the supported environment types in the order
they are defined.
Tests: `make check`.
QA: if you have time, given the amount of code
enabled by this branch, and given the differences
in each one Juju configuration, I'd appreciate both
reviewers to QA the interactive session.
The instructions follow, thank you very much!
- Run `make`.
- Backup your Juju home:
`mv ~/.juju ~/juju-home-backup`
This way we can simulate Juju is not configured at all.
- Run `.venv/bin/python juju-quickstart`.
Quickstart should welcome you and propose, among
other things, to automatically create and bootstrap
a local environment. Do that, provide your sudo
password if requested, wait until the process completes
and the GUI opens in your default browser as usual.
This can take several minutes based on the sanity of your
LXC configuration. Before opening the browser, you should
see instructions on re-running quickstart to reopen
and log in to the GUI later or restarting the interactive
session. Also note that an admin password has been
automatically generated and printed to stdout.
- Run `.venv/bin/python juju-quickstart`.
This time no interactive session is started, the application
recognizes the environment is already bootstrapped and
quickly reopens the browser and logs in to the GUI.
- Run `cat ~/.juju/ environments. yaml`.
Check the generated environment file looks sane.
- Destroy the local enviro...