Merge lp:~frankban/juju-quickstart/support-cache-yaml into lp:juju-quickstart
- support-cache-yaml
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 142 |
Proposed branch: | lp:~frankban/juju-quickstart/support-cache-yaml |
Merge into: | lp:juju-quickstart |
Diff against target: |
2301 lines (+663/-522) 19 files modified
quickstart/__init__.py (+1/-1) quickstart/app.py (+8/-63) quickstart/cli/params.py (+2/-4) quickstart/cli/views.py (+19/-46) quickstart/manage.py (+43/-39) quickstart/models/apiinfo.py (+110/-0) quickstart/models/jenv.py (+8/-0) quickstart/platform_support.py (+5/-11) quickstart/settings.py (+1/-1) quickstart/tests/cli/test_params.py (+6/-13) quickstart/tests/cli/test_views.py (+68/-122) quickstart/tests/functional/test_functional.py (+1/-1) quickstart/tests/helpers.py (+25/-6) quickstart/tests/models/test_apiinfo.py (+242/-0) quickstart/tests/models/test_bundles.py (+6/-6) quickstart/tests/test_app.py (+22/-114) quickstart/tests/test_manage.py (+86/-85) quickstart/tests/test_platform_support.py (+5/-5) tox.ini (+5/-5) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/support-cache-yaml |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Madison Scott-Clary (community) | code+qa | Approve | |
Martin Hilton (community) | Approve | ||
Review via email: mp+276540@code.launchpad.net |
Commit message
Description of the change
Use API info to connect to existing environments.
Do not rely on jenv files for handling already bootstrapped
environments. Instead, use the `juju api-info` command in order
to retrieve the existing environment's credentials, API addresses
and UUID.
As a consequence, drop support to Juju 1.18: the minimum supported
version is now Juju 1.22.1 (present in trusty-updates).
Also update to test against jujubundlelib 0.2.0 and jujuclient 0.50.2.
Apologies for the long diff, but a lot of code has been removed and
simplified.
Tests: `make check`.
QA: run quickstart (`devenv/
deploy bundles both with jes enabled (`export JUJU_DEV_
and disabled. Check that everything works as usual.
Francesco Banconi (frankban) wrote : | # |
Thanks for the review Martin!
- 150. By Francesco Banconi
-
Fix typo.
- 151. By Francesco Banconi
-
Fix functional tests.
Madison Scott-Clary (makyo) wrote : | # |
QA okay with one minor comment below, thanks!
- 152. By Francesco Banconi
-
Update jujuclient and jujubundlelib dependencies.
Francesco Banconi (frankban) wrote : | # |
Thanks for the review Madison, I fixed the test.
- 153. By Francesco Banconi
-
Fix tests intermittently failing due to sorting issues.
Preview Diff
1 | === modified file 'quickstart/__init__.py' |
2 | --- quickstart/__init__.py 2015-08-07 10:24:11 +0000 |
3 | +++ quickstart/__init__.py 2015-11-05 10:58:41 +0000 |
4 | @@ -45,7 +45,7 @@ |
5 | Once Juju has been installed, the command can also be run as a juju plugin, |
6 | without the hyphen ("juju quickstart"). |
7 | """ |
8 | -VERSION = (2, 2, 1) |
9 | +VERSION = (2, 2, 2) |
10 | |
11 | |
12 | def get_version(): |
13 | |
14 | === modified file 'quickstart/app.py' |
15 | --- quickstart/app.py 2015-08-11 09:06:56 +0000 |
16 | +++ quickstart/app.py 2015-11-05 10:58:41 +0000 |
17 | @@ -21,7 +21,6 @@ |
18 | unicode_literals, |
19 | ) |
20 | |
21 | -import json |
22 | import logging |
23 | import sys |
24 | import time |
25 | @@ -40,7 +39,6 @@ |
26 | utils, |
27 | watchers, |
28 | ) |
29 | -from quickstart.models import jenv |
30 | |
31 | |
32 | class ProgramExit(Exception): |
33 | @@ -179,25 +177,18 @@ |
34 | raise ProgramExit(bytes(err)) |
35 | |
36 | |
37 | -def check_bootstrapped(env_name): |
38 | - """Check if the environment named env_name is already bootstrapped. |
39 | +def get_api_address(env_info): |
40 | + """Return the API address for the environment described by the given info. |
41 | |
42 | - If so, return the environment API address to be used to connect to the Juju |
43 | - API server. If not already bootstrapped, or if the API address cannot be |
44 | - retrieved, return None. |
45 | + Return None if the address cannot be found or the environment is not |
46 | + bootstrapped. |
47 | """ |
48 | - if not jenv.exists(env_name): |
49 | - return None |
50 | - # Retrieve the Juju API addresses from the jenv file. |
51 | - try: |
52 | - candidates = jenv.get_value(env_name, 'state-servers') |
53 | - except ValueError as err: |
54 | - logging.warn(b'cannot retrieve the Juju API address: {}'.format(err)) |
55 | + if not env_info: |
56 | return None |
57 | # Look for a reachable API address. |
58 | + candidates = env_info['state-servers'] |
59 | if not candidates: |
60 | - logging.warn( |
61 | - 'cannot retrieve the Juju API address: no addresses found') |
62 | + logging.warn('cannot retrieve the API address: no addresses found') |
63 | return None |
64 | for candidate in candidates: |
65 | error = netutils.check_listening(candidate) |
66 | @@ -206,7 +197,7 @@ |
67 | return candidate |
68 | logging.debug(error) |
69 | logging.warn( |
70 | - 'cannot retrieve the Juju API address: cannot connect to any of the ' |
71 | + 'cannot retrieve the API address: cannot connect to any of the ' |
72 | 'following addresses: {}'.format(', '.join(candidates))) |
73 | return None |
74 | |
75 | @@ -282,52 +273,6 @@ |
76 | raise ProgramExit('the state server is not ready:\n{}'.format(details)) |
77 | |
78 | |
79 | -def get_env_uuid_or_none(env_name): |
80 | - """Return the Juju environment unique id for the given environment name. |
81 | - |
82 | - Parse the jenv file to retrieve the environment UUID. |
83 | - |
84 | - Return None if the environment UUID is not present in the jenv file. |
85 | - Raise a ProgramExit if the jenv file is not valid. |
86 | - """ |
87 | - try: |
88 | - return jenv.get_env_uuid(env_name) |
89 | - except ValueError as err: |
90 | - msg = b'cannot retrieve environment unique identifier: {}'.format(err) |
91 | - raise ProgramExit(msg) |
92 | - |
93 | - |
94 | -def get_credentials(env_name): |
95 | - """Return the Juju credentials for the given environment name. |
96 | - |
97 | - Parse the jenv file to retrieve the environment user name and password. |
98 | - Raise a ProgramExit if the credentials cannot be retrieved. |
99 | - """ |
100 | - try: |
101 | - return jenv.get_credentials(env_name) |
102 | - except ValueError as err: |
103 | - msg = b'cannot retrieve environment credentials: {}'.format(err) |
104 | - raise ProgramExit(msg) |
105 | - |
106 | - |
107 | -def get_api_address(env_name, juju_command): |
108 | - """Return a Juju API address for the given environment name. |
109 | - |
110 | - Only the address is returned, without the schema or the path. For instance: |
111 | - "api.example.com:17070". |
112 | - |
113 | - Use the Juju CLI in a subprocess in order to retrieve the API addresses. |
114 | - Raise a ProgramExit if any error occurs. |
115 | - """ |
116 | - retcode, output, error = utils.call( |
117 | - juju_command, 'api-endpoints', '-e', env_name, '--format', 'json') |
118 | - if retcode: |
119 | - raise ProgramExit(error) |
120 | - # Assuming there is always at least one API address, grab the first one |
121 | - # from the JSON output. |
122 | - return json.loads(output)[0] |
123 | - |
124 | - |
125 | def connect(api_url, username, password): |
126 | """Connect to the Juju API server and log in using the given credentials. |
127 | |
128 | |
129 | === modified file 'quickstart/cli/params.py' |
130 | --- quickstart/cli/params.py 2015-01-12 15:39:20 +0000 |
131 | +++ quickstart/cli/params.py 2015-11-05 10:58:41 +0000 |
132 | @@ -33,9 +33,8 @@ |
133 | _PARAMS = ( |
134 | 'env_type_db', |
135 | 'env_db', |
136 | - 'jenv_db', |
137 | + 'active_db', |
138 | 'save_callable', |
139 | - 'remove_jenv_callable', |
140 | ) |
141 | |
142 | |
143 | @@ -52,7 +51,6 @@ |
144 | return self.__class__( |
145 | env_type_db=self.env_type_db, |
146 | env_db=copy.deepcopy(self.env_db), |
147 | - jenv_db=copy.deepcopy(self.jenv_db), |
148 | + active_db=copy.deepcopy(self.active_db), |
149 | save_callable=self.save_callable, |
150 | - remove_jenv_callable=self.remove_jenv_callable, |
151 | ) |
152 | |
153 | === modified file 'quickstart/cli/views.py' |
154 | --- quickstart/cli/views.py 2015-01-12 15:39:20 +0000 |
155 | +++ quickstart/cli/views.py 2015-11-05 10:58:41 +0000 |
156 | @@ -116,8 +116,8 @@ |
157 | ui, |
158 | ) |
159 | from quickstart.models import ( |
160 | + apiinfo, |
161 | envs, |
162 | - jenv, |
163 | ) |
164 | |
165 | |
166 | @@ -165,7 +165,7 @@ |
167 | Receives a params namedtuple-like object including the following fields: |
168 | - env_type_db: the environments meta information; |
169 | - env_db: the environments database; |
170 | - - jenv_db: the jenv files database; |
171 | + - active_db: the active environments database; |
172 | - save_callable: a function called to save a new environment database. |
173 | See quickstart.cli.params.Params. |
174 | """ |
175 | @@ -176,7 +176,7 @@ |
176 | # without selecting an environment to use. |
177 | app.set_return_value_on_exit((params.env_db, None)) |
178 | detail_view = functools.partial(env_detail, app, params) |
179 | - jenv_view = functools.partial(jenv_detail, app, params) |
180 | + active_view = functools.partial(active_detail, app, params) |
181 | edit_view = functools.partial(env_edit, app, params) |
182 | # Alphabetically sort the existing environments. |
183 | environments = sorted([ |
184 | @@ -262,7 +262,7 @@ |
185 | focus_position = None |
186 | active_found = default_found = errors_found = False |
187 | existing_widgets_num = len(widgets) |
188 | - remaining_jenv_db = copy.deepcopy(params.jenv_db) |
189 | + remaining_active_db = copy.deepcopy(params.active_db) |
190 | for position, env_data in enumerate(environments): |
191 | bullet = '\N{BULLET}' |
192 | # Is this environment the default one? |
193 | @@ -271,7 +271,7 @@ |
194 | # The first two positions are the section header and the divider. |
195 | focus_position = position + existing_widgets_num |
196 | bullet = '\N{CHECK MARK}' |
197 | - if remaining_jenv_db['environments'].pop(env_data['name'], None): |
198 | + if remaining_active_db['environments'].pop(env_data['name'], None): |
199 | # This is an active environment. Is it running? Who knows... |
200 | active_found = True |
201 | bullet = ('active', bullet) |
202 | @@ -290,8 +290,8 @@ |
203 | # Alphabetically sort the remaining environments not included in the |
204 | # environments.yaml file. |
205 | environments = list(sorted([ |
206 | - envs.get_env_data(remaining_jenv_db, env_name) |
207 | - for env_name in remaining_jenv_db['environments'] |
208 | + envs.get_env_data(remaining_active_db, env_name) |
209 | + for env_name in remaining_active_db['environments'] |
210 | ], key=operator.itemgetter('name'))) |
211 | |
212 | if environments: |
213 | @@ -308,9 +308,10 @@ |
214 | ]) |
215 | bullet = ('active', '\N{BULLET}') |
216 | for env_data in environments: |
217 | - env_short_description = jenv.get_env_short_description(env_data) |
218 | + env_short_description = envs.get_env_short_description(env_data) |
219 | text = [bullet, ' {}'.format(env_short_description)] |
220 | - widgets.append(ui.MenuButton(text, ui.thunk(jenv_view, env_data))) |
221 | + widgets.append( |
222 | + ui.MenuButton(text, ui.thunk(active_view, env_data))) |
223 | |
224 | # Set up the "create a new environment" section. |
225 | widgets.extend([ |
226 | @@ -389,7 +390,7 @@ |
227 | Receives a params namedtuple-like object including the following fields: |
228 | - env_type_db: the environments meta information; |
229 | - env_db: the environments database; |
230 | - - jenv_db: the jenv files database; |
231 | + - active_db: the active environments database; |
232 | - save_callable: a function called to save a new environment database. |
233 | See quickstart.cli.params.Params. |
234 | Also receives the current environment data env_data. |
235 | @@ -494,17 +495,17 @@ |
236 | redirect_view() |
237 | |
238 | |
239 | -def jenv_detail(app, params, env_data): |
240 | - """Show details on a Juju imported environment. |
241 | +def active_detail(app, params, env_data): |
242 | + """Show details on a Juju active/imported environment. |
243 | |
244 | The environment is not included in the environments.yaml file, but just |
245 | - found in the jenv database. |
246 | + found in the active database. |
247 | From this view it is possible to start the environment. |
248 | |
249 | Receives a params namedtuple-like object including the following fields: |
250 | - env_type_db: the environments meta information; |
251 | - env_db: the environments database; |
252 | - - jenv_db: the jenv files database; |
253 | + - active_db: the active environments database; |
254 | - save_callable: a function called to save a new environment database; |
255 | See quickstart.cli.params.Params. |
256 | Also receives the current environment data env_data. |
257 | @@ -516,9 +517,9 @@ |
258 | app.set_return_value_on_exit((params.env_db, None)) |
259 | index_view = functools.partial(env_index, app, params) |
260 | |
261 | - app.set_title(jenv.get_env_short_description(env_data)) |
262 | + app.set_title(envs.get_env_short_description(env_data)) |
263 | widgets = [] |
264 | - for key, value in jenv.get_env_details(env_data): |
265 | + for key, value in apiinfo.get_env_details(env_data): |
266 | widgets.append(urwid.Text(['{}: '.format(key), ('highlight', value)])) |
267 | widgets.extend([ |
268 | urwid.Divider(), |
269 | @@ -528,23 +529,13 @@ |
270 | '\nFor this reason, it is not possible to edit it.\n' |
271 | 'However, you can use the link below to ', |
272 | ('highlight', 'use Juju Quickstart'), |
273 | - ' with this environment or ', |
274 | - ('highlight', 'remove the corresponding jenv file'), |
275 | - '.\n' |
276 | - 'Note that removing the Juju generated environment file does not ' |
277 | - 'destroy the corresponding active environment.' |
278 | + ' with this environment', |
279 | ]), |
280 | ]) |
281 | |
282 | - remove_callback = ui.thunk( |
283 | - _remove_jenv, params.jenv_db, env_data, params.remove_jenv_callable, |
284 | - app.set_message, index_view) |
285 | - confirm_removal_callback = ui.thunk( |
286 | - _confirm_removal, app, env_data, remove_callback) |
287 | controls = [ |
288 | ui.MenuButton('back', ui.thunk(index_view)), |
289 | ui.MenuButton('use', ui.thunk(_use, params.env_db, env_data)), |
290 | - ui.MenuButton(('control alert', 'remove'), confirm_removal_callback), |
291 | ] |
292 | widgets.append(ui.create_controls(*controls)) |
293 | listbox = urwid.ListBox(urwid.SimpleFocusListWalker(widgets)) |
294 | @@ -552,24 +543,6 @@ |
295 | app.set_status([' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate ']) |
296 | |
297 | |
298 | -def _remove_jenv( |
299 | - jenv_db, env_data, remove_jenv_callable, set_message, redirect_view): |
300 | - """Remove the jenv file corresonding to the env_data environment. |
301 | - |
302 | - Update the provided jenv_db and return to the given view. |
303 | - Also output a notification using the given set_message callable. |
304 | - """ |
305 | - env_name = env_data['name'] |
306 | - # Remove the jenv file. |
307 | - msg = remove_jenv_callable(env_name) |
308 | - if msg is None: |
309 | - msg = '{} successfully removed'.format(env_name) |
310 | - # Also remove the environments from the jenv database. |
311 | - del jenv_db['environments'][env_name] |
312 | - set_message(msg) |
313 | - redirect_view() |
314 | - |
315 | - |
316 | def env_edit(app, params, env_data): |
317 | """Create or modify a Juju environment. |
318 | |
319 | @@ -580,7 +553,7 @@ |
320 | Receives a params namedtuple-like object including the following fields: |
321 | - env_type_db: the environments meta information; |
322 | - env_db: the environments database; |
323 | - - jenv_db: the jenv files database; |
324 | + - active_db: the active environments database; |
325 | - save_callable: a function called to save a new environment database; |
326 | See quickstart.cli.params.Params. |
327 | Also receives the current environment data env_data. |
328 | |
329 | === modified file 'quickstart/manage.py' |
330 | --- quickstart/manage.py 2015-05-29 15:03:27 +0000 |
331 | +++ quickstart/manage.py 2015-11-05 10:58:41 +0000 |
332 | @@ -45,9 +45,9 @@ |
333 | views, |
334 | ) |
335 | from quickstart.models import ( |
336 | + apiinfo, |
337 | bundles, |
338 | envs, |
339 | - jenv, |
340 | ) |
341 | |
342 | |
343 | @@ -162,9 +162,14 @@ |
344 | options.gui_source = (user, branch) |
345 | |
346 | |
347 | -def _validate_platform(platform, parser): |
348 | +def _validate_platform(platform, options, parser): |
349 | """Validate the platform. |
350 | |
351 | + Populate the options namespace with the following names: |
352 | + - info: and object that can be used to retrieve API info on envs; |
353 | + - juju_command: the path to the Juju command on the given platform; |
354 | + - platform: the current OS platform as defined in the settings module. |
355 | + |
356 | Exit with an error if platform is not supported by quickstart or is |
357 | missing files. |
358 | """ |
359 | @@ -172,6 +177,9 @@ |
360 | platform_support.validate_platform(platform) |
361 | except ValueError as err: |
362 | return parser.error(bytes(err)) |
363 | + options.juju_command = platform_support.get_juju_command(platform) |
364 | + options.platform = platform |
365 | + options.info = apiinfo.Info(options.juju_command) |
366 | |
367 | |
368 | def _validate_port(port, parser): |
369 | @@ -214,7 +222,8 @@ |
370 | return save_callable |
371 | |
372 | |
373 | -def _start_interactive_session(parser, env_type_db, env_db, jenv_db, env_file): |
374 | +def _start_interactive_session( |
375 | + parser, env_type_db, env_db, active_db, env_file): |
376 | """Start the Urwid interactive session. |
377 | |
378 | Return the env_data corresponding to the user selected environment. |
379 | @@ -224,9 +233,8 @@ |
380 | parameters = params.Params( |
381 | env_type_db=env_type_db, |
382 | env_db=env_db, |
383 | - jenv_db=jenv_db, |
384 | + active_db=active_db, |
385 | save_callable=_create_save_callable(parser, env_file), |
386 | - remove_jenv_callable=jenv.remove, |
387 | ) |
388 | new_env_db, env_data = views.show(views.env_index, parameters) |
389 | if new_env_db != env_db: |
390 | @@ -239,7 +247,7 @@ |
391 | return env_data |
392 | |
393 | |
394 | -def _retrieve_env_data(parser, env_type_db, env_db, jenv_db, env_name): |
395 | +def _retrieve_env_data(parser, env_type_db, env_db, active_db, env_name): |
396 | """Retrieve and return the env_data corresponding to the given env_name. |
397 | |
398 | Invoke a parser error if the environment does not exist or is not valid. |
399 | @@ -250,7 +258,7 @@ |
400 | # The specified environment does not exist in the environments file. |
401 | # Check if this is an imported environment. |
402 | try: |
403 | - return envs.get_env_data(jenv_db, env_name) |
404 | + return envs.get_env_data(active_db, env_name) |
405 | except ValueError as err: |
406 | # The environment cannot be found anywhere, exit with an error. |
407 | return parser.error(bytes(err)) |
408 | @@ -299,21 +307,21 @@ |
409 | |
410 | # Retrieve the environment database (or create an in-memory empty one). |
411 | env_db = _retrieve_env_db(parser, env_file if env_file_exists else None) |
412 | - jenv_db = jenv.get_env_db() |
413 | + active_db = options.info.all() |
414 | |
415 | # Validate the environment. |
416 | env_type_db = envs.get_env_type_db() |
417 | if interactive: |
418 | # Start the interactive session. |
419 | env_data = _start_interactive_session( |
420 | - parser, env_type_db, env_db, jenv_db, env_file) |
421 | + parser, env_type_db, env_db, active_db, env_file) |
422 | else: |
423 | # This is a non-interactive session and we need to validate the |
424 | # selected environment before proceeding. |
425 | env_data = _retrieve_env_data( |
426 | - parser, env_type_db, env_db, jenv_db, env_name) |
427 | + parser, env_type_db, env_db, active_db, env_name) |
428 | # Check for local support, if requested. |
429 | - options.env_type = env_data['type'] |
430 | + options.env_type = env_data.get('type') |
431 | no_local_support = not platform_support.supports_local(options.platform) |
432 | if options.env_type == 'local' and no_local_support: |
433 | return parser.error( |
434 | @@ -368,11 +376,15 @@ |
435 | - distro_only: install Juju only using the distribution packages; |
436 | - env_file: the absolute path of the Juju environments.yaml file; |
437 | - env_name: the name of the Juju environment to use; |
438 | - - env_type: the provider type of the selected Juju environment; |
439 | + - env_type: the provider type of the selected Juju environment, or None |
440 | + if the environment type cannot be retrieved at setup time; |
441 | - gui_source: the optional Juju GUI branch source on github, or None; |
442 | + - info: an instance of "quickstart.models.apiinfo.Info", used to |
443 | + retrieve information about the current environment in use; |
444 | - interactive: whether to start the interactive session; |
445 | + - juju_command: the path to the Juju command on the current platform; |
446 | - open_browser: whether the GUI browser must be opened; |
447 | - - platform: The host platform; |
448 | + - platform: the host platform; |
449 | - port: the optional Juju GUI port, or None; |
450 | - uncommitted: whether the provided bundle must be deployed using the |
451 | Juju GUI uncommitted state; |
452 | @@ -520,10 +532,8 @@ |
453 | # Set up logging. |
454 | _configure_logging(logging.DEBUG if options.debug else logging.INFO) |
455 | |
456 | - # Validate and add in the platform for convenience. |
457 | - _validate_platform(platform, parser) |
458 | - options.platform = platform |
459 | - |
460 | + # Validate the platform and add in platform info for convenience. |
461 | + _validate_platform(platform, options, parser) |
462 | # Convert the provided string arguments to unicode. |
463 | _convert_options_to_unicode(options) |
464 | # Validate and process the provided arguments. |
465 | @@ -544,12 +554,9 @@ |
466 | print('contents loaded for {} (services: {})'.format( |
467 | options.bundle, len(options.bundle.services()))) |
468 | |
469 | - juju_command, custom_juju = platform_support.get_juju_command( |
470 | - options.platform) |
471 | - |
472 | logging.debug('ensuring juju and dependencies are installed') |
473 | juju_version = app.ensure_dependencies( |
474 | - options.distro_only, options.platform, juju_command) |
475 | + options.distro_only, options.platform, options.juju_command) |
476 | app.check_juju_supported(juju_version) |
477 | |
478 | logging.debug('ensuring SSH keys are available') |
479 | @@ -558,7 +565,8 @@ |
480 | # Bootstrap the Juju environment or reuse an already bootstrapped one. |
481 | already_bootstrapped = True |
482 | env_type = options.env_type |
483 | - api_address = app.check_bootstrapped(options.env_name) |
484 | + env_info = options.info.get(options.env_name) |
485 | + api_address = app.get_api_address(env_info) |
486 | if api_address is None: |
487 | print('bootstrapping the {} environment'.format(options.env_name)) |
488 | if env_type == 'local': |
489 | @@ -567,7 +575,8 @@ |
490 | print('sudo privileges will be required to bootstrap the ' |
491 | 'environment') |
492 | already_bootstrapped = app.bootstrap( |
493 | - options.env_name, juju_command, |
494 | + options.env_name, |
495 | + options.juju_command, |
496 | debug=options.debug, |
497 | upload_tools=options.upload_tools, |
498 | upload_series=options.upload_series, |
499 | @@ -579,26 +588,21 @@ |
500 | # Retrieve the environment status, ensure it is in a ready state and |
501 | # contextually fetch the bootstrap node series. |
502 | print('retrieving the environment status') |
503 | - bootstrap_node_series = app.status(options.env_name, juju_command) |
504 | + bootstrap_node_series = app.status(options.env_name, options.juju_command) |
505 | |
506 | # If the environment was not already bootstrapped, we need to retrieve |
507 | # the API address. |
508 | if api_address is None: |
509 | - print('retrieving the Juju API address') |
510 | - api_address = app.get_api_address(options.env_name, juju_command) |
511 | - |
512 | - # Retrieve the Juju environment unique identifier. |
513 | - env_uuid = app.get_env_uuid_or_none(options.env_name) |
514 | + print('retrieving the Juju API address and credentials') |
515 | + env_info = options.info.get(options.env_name) |
516 | + api_address = app.get_api_address(env_info) |
517 | |
518 | # Build the Juju API endpoint based on the Juju version and environment id. |
519 | - api_url = jujutools.get_api_url(api_address, juju_version, env_uuid) |
520 | - |
521 | - # Retrieve the admin-secret for the current environment. |
522 | - print('retrieving the Juju environment credentials') |
523 | - username, password = app.get_credentials(options.env_name) |
524 | + api_url = jujutools.get_api_url( |
525 | + api_address, juju_version, env_info['uuid']) |
526 | |
527 | print('connecting to {}'.format(api_url)) |
528 | - env = app.connect(api_url, username, password) |
529 | + env = app.connect(api_url, env_info['user'], env_info['password']) |
530 | |
531 | if already_bootstrapped: |
532 | # Retrieve the environment type from the live environment: it may be |
533 | @@ -629,19 +633,19 @@ |
534 | url = utils.build_web_url( |
535 | address, gui_config['port'], gui_config['secure']) |
536 | print('\nJuju GUI URL: {}\nusername: {}\npassword: {}\n'.format( |
537 | - url, username, password)) |
538 | + url, env_info['user'], env_info['password'])) |
539 | |
540 | # Connect to the GUI server WebSocket API. |
541 | print('connecting to the Juju GUI server') |
542 | gui_api_url = jujutools.get_api_url( |
543 | - '{}:{}'.format(address, gui_config['port']), juju_version, env_uuid, |
544 | - path_prefix='ws', charm_ref=charm_ref, |
545 | + '{}:{}'.format(address, gui_config['port']), juju_version, |
546 | + env_info['uuid'], path_prefix='ws', charm_ref=charm_ref, |
547 | insecure=not gui_config['secure']) |
548 | |
549 | # We need to connect to an API WebSocket server supporting bundle |
550 | # deployments and automatic auth-token login. The GUI builtin server, |
551 | # listening on the Juju GUI address, exposes a suitable API. |
552 | - gui_env = app.connect(gui_api_url, username, password) |
553 | + gui_env = app.connect(gui_api_url, env_info['user'], env_info['password']) |
554 | changes_token = None |
555 | # Handle bundle deployment. |
556 | if options.bundle_source is not None: |
557 | |
558 | === added file 'quickstart/models/apiinfo.py' |
559 | --- quickstart/models/apiinfo.py 1970-01-01 00:00:00 +0000 |
560 | +++ quickstart/models/apiinfo.py 2015-11-05 10:58:41 +0000 |
561 | @@ -0,0 +1,110 @@ |
562 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
563 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
564 | +# Copyright (C) 2015 Canonical Ltd. |
565 | +# |
566 | +# This program is free software: you can redistribute it and/or modify it under |
567 | +# the terms of the GNU Affero General Public License version 3, as published by |
568 | +# the Free Software Foundation. |
569 | +# |
570 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
571 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
572 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
573 | +# Affero General Public License for more details. |
574 | +# |
575 | +# You should have received a copy of the GNU Affero General Public License |
576 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
577 | + |
578 | +"""Juju Quickstart API info handling. |
579 | + |
580 | +This module is used to collect information about already bootstrapped |
581 | +environments. The Juju command line is used to retrieve data useful when |
582 | +connecting to these environments. |
583 | +""" |
584 | + |
585 | +from __future__ import unicode_literals |
586 | + |
587 | +import json |
588 | +import logging |
589 | +import os |
590 | + |
591 | +from quickstart import ( |
592 | + settings, |
593 | + utils, |
594 | +) |
595 | + |
596 | + |
597 | +class Info(object): |
598 | + """Collect methods used for retrieving environment information.""" |
599 | + |
600 | + def __init__(self, juju_command): |
601 | + """Initialize the object with the path to the Juju command.""" |
602 | + self._juju_command = juju_command |
603 | + |
604 | + def get(self, env_name): |
605 | + """Return info on the given bootstrapped environment. |
606 | + |
607 | + Return an empty dict if the environment is not found, is not |
608 | + bootstrapped or it is not possible to retrieve information, for |
609 | + instance because Juju is not installed. |
610 | + """ |
611 | + retcode, output, error = utils.call( |
612 | + self._juju_command, 'api-info', |
613 | + '-e', env_name, '--password', '--format', 'json') |
614 | + if retcode: |
615 | + logging.debug( |
616 | + 'unable to get API info for {}: {}'.format(env_name, error)) |
617 | + return {} |
618 | + |
619 | + data = json.loads(output) |
620 | + return { |
621 | + 'name': env_name, |
622 | + 'user': data['user'], |
623 | + 'password': data['password'], |
624 | + 'uuid': data['environ-uuid'], |
625 | + 'state-servers': data['state-servers'], |
626 | + } |
627 | + |
628 | + def all(self): |
629 | + """Return information about all bootstrapped environments. |
630 | + |
631 | + The returned dict is similar to what is returned by models.envs.load(). |
632 | + |
633 | + The environment database returned by this method does not contain the |
634 | + usual fields used as bootstrap options. The included fields are: |
635 | + "name", "user", "password", "uuid" and "state-servers". |
636 | + """ |
637 | + retcode, output, _ = utils.call(self._juju_command, 'system', 'list') |
638 | + names = _env_names_from_jenvs() if retcode else output.splitlines() |
639 | + return {'environments': dict((name, self.get(name)) for name in names)} |
640 | + |
641 | + |
642 | +def _env_names_from_jenvs(): |
643 | + """Return active environment names by searching for jenv files.""" |
644 | + names = [] |
645 | + path = os.path.expanduser(os.path.join(settings.JUJU_HOME, 'environments')) |
646 | + if not os.path.isdir(path): |
647 | + logging.debug('environments directory not found in the Juju home') |
648 | + return names |
649 | + for filename in sorted(os.listdir(path)): |
650 | + fullpath = os.path.join(path, filename) |
651 | + # Check that the current file is a jenv file. |
652 | + if not os.path.isfile(fullpath): |
653 | + continue |
654 | + name, ext = os.path.splitext(filename) |
655 | + if ext == '.jenv': |
656 | + names.append(name) |
657 | + return names |
658 | + |
659 | + |
660 | +def get_env_details(env_data): |
661 | + """Return the environment details as a sequence of tuples (label, value). |
662 | + |
663 | + In each tuple, the label is the field name, and the value is the |
664 | + corresponding value in the given env_data. |
665 | + """ |
666 | + return [ |
667 | + ('name', env_data['name']), |
668 | + ('user', env_data['user']), |
669 | + ('uuid', env_data['uuid']), |
670 | + ('state servers', ', '.join(env_data['state-servers'])), |
671 | + ] |
672 | |
673 | === modified file 'quickstart/models/jenv.py' |
674 | --- quickstart/models/jenv.py 2015-02-09 11:22:44 +0000 |
675 | +++ quickstart/models/jenv.py 2015-11-05 10:58:41 +0000 |
676 | @@ -18,6 +18,11 @@ |
677 | |
678 | At bootstrap, Juju writes a generated file (jenv) located in JUJU_HOME. |
679 | This module includes functions to load and parse those jenv file contents. |
680 | + |
681 | +NOTE: this module is deprecated and still here for API compatibility only. |
682 | +Do not import this module on quickstart code. External programs using this |
683 | +module should move to more reliable ways to retrieve information on already |
684 | +bootstrapped environments. For instance, see "quickstart.models.apiinfo". |
685 | """ |
686 | |
687 | from __future__ import unicode_literals |
688 | @@ -31,6 +36,9 @@ |
689 | ) |
690 | |
691 | |
692 | +# This module is deprecated. |
693 | +logging.warn('using deprecated module "quickstart.models.jenv"') |
694 | + |
695 | # Define the default Juju user when an environment is initially bootstrapped. |
696 | JUJU_DEFAULT_USER = 'admin' |
697 | # Define an env type to use when the real type is not included in the jenv. |
698 | |
699 | === modified file 'quickstart/platform_support.py' |
700 | --- quickstart/platform_support.py 2015-05-22 11:33:00 +0000 |
701 | +++ quickstart/platform_support.py 2015-11-05 10:58:41 +0000 |
702 | @@ -117,23 +117,17 @@ |
703 | def get_juju_command(platform): |
704 | """Return the path to the Juju command on the given platform. |
705 | |
706 | - Also return a flag indicating whether the user requested to customize |
707 | - the Juju command by providing a JUJU environment variable. |
708 | - |
709 | - If the platform does not have a novel location, the default will be |
710 | - returned. |
711 | - |
712 | - If the environment variable JUJU is set, then its value will be |
713 | - returned. |
714 | + If the platform does not have a novel location, the default is returned. |
715 | + If the environment variable JUJU is set, then its value is returned. |
716 | """ |
717 | juju_command = os.getenv('JUJU', '').strip() |
718 | platform_command = settings.JUJU_CMD_PATHS.get( |
719 | platform, |
720 | settings.JUJU_CMD_PATHS['default']) |
721 | if juju_command and juju_command != platform_command: |
722 | - logging.warn("a customized juju is being used") |
723 | - return juju_command, True |
724 | - return platform_command, False |
725 | + logging.warn('a customized juju is being used') |
726 | + return juju_command |
727 | + return platform_command |
728 | |
729 | |
730 | def get_juju_installer(platform): |
731 | |
732 | === modified file 'quickstart/settings.py' |
733 | --- quickstart/settings.py 2015-08-12 12:04:34 +0000 |
734 | +++ quickstart/settings.py 2015-11-05 10:58:41 +0000 |
735 | @@ -77,7 +77,7 @@ |
736 | JUJU_GUI_SUPPORTED_SERIES = tuple(DEFAULT_CHARM_URLS.keys()) |
737 | |
738 | # The minimum Juju version supported by Juju Quickstart, |
739 | -JUJU_SUPPORTED_VERSION = (1, 18, 1) |
740 | +JUJU_SUPPORTED_VERSION = (1, 22, 1) |
741 | |
742 | # The path to the MAAS command line interface. |
743 | MAAS_CMD = '/usr/bin/maas' |
744 | |
745 | === modified file 'quickstart/tests/cli/test_params.py' |
746 | --- quickstart/tests/cli/test_params.py 2015-01-12 12:10:38 +0000 |
747 | +++ quickstart/tests/cli/test_params.py 2015-11-05 10:58:41 +0000 |
748 | @@ -31,35 +31,30 @@ |
749 | # Store parameters. |
750 | self.env_type_db = envs.get_env_type_db() |
751 | self.env_db = helpers.make_env_db() |
752 | - self.jenv_db = helpers.make_jenv_db() |
753 | + self.active_db = helpers.make_active_db() |
754 | self.save_callable = lambda env_db: None |
755 | - self.remove_jenv_callable = lambda env_db: None |
756 | # Set up a params object used in tests. |
757 | self.params = params.Params( |
758 | env_type_db=self.env_type_db, |
759 | env_db=self.env_db, |
760 | - jenv_db=self.jenv_db, |
761 | + active_db=self.active_db, |
762 | save_callable=self.save_callable, |
763 | - remove_jenv_callable=self.remove_jenv_callable, |
764 | ) |
765 | |
766 | def test_tuple(self): |
767 | # The params object can be used as a tuple. |
768 | - env_type_db, env_db, jenv_db, save, remove = self.params |
769 | + env_type_db, env_db, active_db, save = self.params |
770 | self.assertIs(self.env_type_db, env_type_db) |
771 | self.assertIs(self.env_db, env_db) |
772 | - self.assertIs(self.jenv_db, jenv_db) |
773 | + self.assertIs(self.active_db, active_db) |
774 | self.assertIs(self.save_callable, save) |
775 | - self.assertIs(self.remove_jenv_callable, remove) |
776 | |
777 | def test_attributes(self): |
778 | # Parameters can be accessed as attributes. |
779 | self.assertIs(self.env_type_db, self.params.env_type_db) |
780 | self.assertIs(self.env_db, self.params.env_db) |
781 | - self.assertIs(self.jenv_db, self.params.jenv_db) |
782 | + self.assertIs(self.active_db, self.params.active_db) |
783 | self.assertIs(self.save_callable, self.params.save_callable) |
784 | - self.assertIs( |
785 | - self.remove_jenv_callable, self.params.remove_jenv_callable) |
786 | |
787 | def test_immutable(self): |
788 | # It is not possible to replace a stored parameter. |
789 | @@ -72,10 +67,8 @@ |
790 | # The original object is not mutated by the copy. |
791 | self.assertIs(self.env_type_db, self.params.env_type_db) |
792 | self.assertIs(self.env_db, self.params.env_db) |
793 | - self.assertIs(self.jenv_db, self.params.jenv_db) |
794 | + self.assertIs(self.active_db, self.params.active_db) |
795 | self.assertIs(self.save_callable, self.params.save_callable) |
796 | - self.assertIs( |
797 | - self.remove_jenv_callable, self.params.remove_jenv_callable) |
798 | # The new params object stores the same data. |
799 | self.assertEqual(self.params, params) |
800 | # But they do not refer to the same object. |
801 | |
802 | === modified file 'quickstart/tests/cli/test_views.py' |
803 | --- quickstart/tests/cli/test_views.py 2015-01-12 13:59:45 +0000 |
804 | +++ quickstart/tests/cli/test_views.py 2015-11-05 10:58:41 +0000 |
805 | @@ -36,8 +36,8 @@ |
806 | views, |
807 | ) |
808 | from quickstart.models import ( |
809 | + apiinfo, |
810 | envs, |
811 | - jenv, |
812 | ) |
813 | from quickstart.tests import helpers |
814 | from quickstart.tests.cli import helpers as cli_helpers |
815 | @@ -137,7 +137,6 @@ |
816 | # Set up the base Urwid application. |
817 | self.loop, self.app = base.setup_urwid_app() |
818 | self.save_callable = mock.Mock() |
819 | - self.remove_jenv_callable = mock.Mock(return_value=None) |
820 | |
821 | def get_widgets_in_contents(self, filter_function=None): |
822 | """Return a list of widgets included in the app contents. |
823 | @@ -168,14 +167,13 @@ |
824 | """ |
825 | return lambda arg: isinstance(arg, cls) |
826 | |
827 | - def make_params(self, env_db, jenv_db): |
828 | + def make_params(self, env_db, active_db): |
829 | """Create and return view parameters using the given env databases.""" |
830 | return params.Params( |
831 | env_type_db=self.env_type_db, |
832 | env_db=env_db, |
833 | - jenv_db=jenv_db, |
834 | + active_db=active_db, |
835 | save_callable=self.save_callable, |
836 | - remove_jenv_callable=self.remove_jenv_callable, |
837 | ) |
838 | |
839 | def click_remove_button(self, env_name): |
840 | @@ -230,8 +228,8 @@ |
841 | # a tuple including a copy of the given env_db and None, the latter |
842 | # meaning no environment has been selected. |
843 | env_db = helpers.make_env_db() |
844 | - jenv_db = helpers.make_jenv_db() |
845 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
846 | + active_db = helpers.make_active_db() |
847 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
848 | new_env_db, env_data = self.get_on_exit_return_value(self.loop) |
849 | self.assertEqual(env_db, new_env_db) |
850 | self.assertIsNot(env_db, new_env_db) |
851 | @@ -240,8 +238,8 @@ |
852 | def test_view_title(self): |
853 | # The application title is correctly set up. |
854 | env_db = helpers.make_env_db() |
855 | - jenv_db = helpers.make_jenv_db() |
856 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
857 | + active_db = helpers.make_active_db() |
858 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
859 | self.assertEqual( |
860 | 'Select an existing Juju environment or create a new one', |
861 | self.app.get_title()) |
862 | @@ -249,8 +247,8 @@ |
863 | def test_view_title_no_environments(self): |
864 | # The application title changes if the env_db has no environments. |
865 | env_db = {'environments': {}} |
866 | - jenv_db = helpers.make_jenv_db() |
867 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
868 | + active_db = helpers.make_active_db() |
869 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
870 | self.assertEqual( |
871 | 'No Juju environments already set up: please create one', |
872 | self.app.get_title()) |
873 | @@ -259,9 +257,9 @@ |
874 | # The view displays a list of the environments in env_db, and buttons |
875 | # to create new environments. |
876 | env_db = helpers.make_env_db() |
877 | - jenv_db = {'environments': {}} |
878 | + active_db = {'environments': {}} |
879 | with local_envs_supported(True): |
880 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
881 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
882 | buttons = self.get_widgets_in_contents( |
883 | filter_function=self.is_a(ui.MenuButton)) |
884 | # A button is created for each existing environment (see details) and |
885 | @@ -274,9 +272,9 @@ |
886 | # The view displays a list of active imported environments, and buttons |
887 | # to create new environments. |
888 | env_db = {'environments': {}} |
889 | - jenv_db = helpers.make_jenv_db() |
890 | + active_db = helpers.make_active_db() |
891 | with local_envs_supported(True): |
892 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
893 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
894 | buttons = self.get_widgets_in_contents( |
895 | filter_function=self.is_a(ui.MenuButton)) |
896 | # A button is created for each existing environment (see details) and |
897 | @@ -284,7 +282,7 @@ |
898 | env_types = envs.get_supported_env_types(self.env_type_db) |
899 | expected_buttons_number = ( |
900 | # The number of active environments. |
901 | - len(jenv_db['environments']) + |
902 | + len(active_db['environments']) + |
903 | # The buttons to create new environments. |
904 | len(env_types) + |
905 | # The button to automatically create a new local environment. |
906 | @@ -299,8 +297,8 @@ |
907 | def test_view_contents_without_imported_envs(self): |
908 | # If there are no active imported environments the corresponding |
909 | # header text is not displayed. |
910 | - env_db = jenv_db = {'environments': {}} |
911 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
912 | + env_db = active_db = {'environments': {}} |
913 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
914 | widgets = self.get_widgets_in_contents( |
915 | filter_function=self.is_a(urwid.Text)) |
916 | texts = [widget.text for widget in widgets] |
917 | @@ -310,9 +308,9 @@ |
918 | # The option to create a new local environment is not present if they |
919 | # are not supported in the current platform. |
920 | env_db = helpers.make_env_db() |
921 | - jenv_db = helpers.make_jenv_db() |
922 | + active_db = helpers.make_active_db() |
923 | with local_envs_supported(False): |
924 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
925 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
926 | buttons = self.get_widgets_in_contents( |
927 | filter_function=self.is_a(ui.MenuButton)) |
928 | captions = map(cli_helpers.get_button_caption, buttons) |
929 | @@ -326,8 +324,8 @@ |
930 | def test_environment_clicked(self, mock_env_detail): |
931 | # The environment detail view is called when clicking an environment. |
932 | env_db = helpers.make_env_db() |
933 | - jenv_db = {'environments': {}} |
934 | - params = self.make_params(env_db, jenv_db) |
935 | + active_db = {'environments': {}} |
936 | + params = self.make_params(env_db, active_db) |
937 | views.env_index(self.app, params) |
938 | buttons = self.get_widgets_in_contents( |
939 | filter_function=self.is_a(ui.MenuButton)) |
940 | @@ -347,40 +345,41 @@ |
941 | # loop cycle. |
942 | mock_env_detail.reset_mock() |
943 | |
944 | - @mock.patch('quickstart.cli.views.jenv_detail') |
945 | - def test_imported_environment_clicked(self, mock_jenv_detail): |
946 | - # The jenv detail view is called when clicking an imported environment. |
947 | + @mock.patch('quickstart.cli.views.active_detail') |
948 | + def test_imported_environment_clicked(self, mock_active_detail): |
949 | + # The active detail view is called when clicking an imported |
950 | + # environment. |
951 | env_db = {'environments': {}} |
952 | - jenv_db = helpers.make_jenv_db() |
953 | - params = self.make_params(env_db, jenv_db) |
954 | + active_db = helpers.make_active_db() |
955 | + params = self.make_params(env_db, active_db) |
956 | with local_envs_supported(False): |
957 | views.env_index(self.app, params) |
958 | buttons = self.get_widgets_in_contents( |
959 | filter_function=self.is_a(ui.MenuButton)) |
960 | # The environments are listed in alphabetical order. |
961 | - environments = sorted(jenv_db['environments']) |
962 | + environments = sorted(active_db['environments']) |
963 | for env_name, button in zip(environments, buttons): |
964 | - env_data = envs.get_env_data(jenv_db, env_name) |
965 | + env_data = envs.get_env_data(active_db, env_name) |
966 | # The caption includes the environment description. |
967 | - env_description = jenv.get_env_short_description(env_data) |
968 | + env_description = envs.get_env_short_description(env_data) |
969 | self.assertIn( |
970 | env_description, cli_helpers.get_button_caption(button)) |
971 | - # When the button is clicked, the jenv detail view is called |
972 | + # When the button is clicked, the active detail view is called |
973 | # passing the corresponding environment data. |
974 | cli_helpers.emit(button) |
975 | - mock_jenv_detail.assert_called_once_with( |
976 | + mock_active_detail.assert_called_once_with( |
977 | self.app, params, env_data) |
978 | # Reset the mock so that it does not include any calls on the next |
979 | # loop cycle. |
980 | - mock_jenv_detail.reset_mock() |
981 | + mock_active_detail.reset_mock() |
982 | |
983 | @mock.patch('quickstart.cli.views.env_edit') |
984 | def test_create_new_environment_clicked(self, mock_env_edit): |
985 | # The environment edit view is called when clicking to create a new |
986 | # environment. |
987 | env_db = helpers.make_env_db() |
988 | - jenv_db = {'environments': {}} |
989 | - params = self.make_params(env_db, jenv_db) |
990 | + active_db = {'environments': {}} |
991 | + params = self.make_params(env_db, active_db) |
992 | with local_envs_supported(True): |
993 | views.env_index(self.app, params) |
994 | buttons = self.get_widgets_in_contents( |
995 | @@ -410,10 +409,10 @@ |
996 | # If that option is clicked, the view quits the application returning |
997 | # the newly created env_data. |
998 | env_db = envs.create_empty_env_db() |
999 | - jenv_db = helpers.make_jenv_db() |
1000 | + active_db = helpers.make_active_db() |
1001 | with maas_env_detected(False): |
1002 | with local_envs_supported(True): |
1003 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1004 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1005 | buttons = self.get_widgets_in_contents( |
1006 | filter_function=self.is_a(ui.MenuButton)) |
1007 | # The "create and bootstrap" button is the first one in the contents. |
1008 | @@ -434,9 +433,9 @@ |
1009 | # environment is not displayed if the current platform does not support |
1010 | # local environments. |
1011 | env_db = envs.create_empty_env_db() |
1012 | - jenv_db = helpers.make_jenv_db() |
1013 | + active_db = helpers.make_active_db() |
1014 | with local_envs_supported(False): |
1015 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1016 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1017 | buttons = self.get_widgets_in_contents( |
1018 | filter_function=self.is_a(ui.MenuButton)) |
1019 | # No "create and bootstrap local" buttons are present. |
1020 | @@ -450,9 +449,9 @@ |
1021 | # If that option is clicked, the view quits the application returning |
1022 | # the newly created env_data. |
1023 | env_db = envs.create_empty_env_db() |
1024 | - jenv_db = helpers.make_jenv_db() |
1025 | + active_db = helpers.make_active_db() |
1026 | with maas_env_detected(True): |
1027 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1028 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1029 | buttons = self.get_widgets_in_contents( |
1030 | filter_function=self.is_a(ui.MenuButton)) |
1031 | # The "create and bootstrap" button is the first one in the contents. |
1032 | @@ -473,9 +472,9 @@ |
1033 | # environment is not displayed if no MAAS API endpoints are |
1034 | # available on the system |
1035 | env_db = envs.create_empty_env_db() |
1036 | - jenv_db = helpers.make_jenv_db() |
1037 | + active_db = helpers.make_active_db() |
1038 | with maas_env_detected(False): |
1039 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1040 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1041 | buttons = self.get_widgets_in_contents( |
1042 | filter_function=self.is_a(ui.MenuButton)) |
1043 | # No "create and bootstrap MAAS" buttons are present. |
1044 | @@ -485,8 +484,8 @@ |
1045 | def test_selected_environment(self): |
1046 | # The default environment is already selected in the list. |
1047 | env_db = helpers.make_env_db(default='lxc') |
1048 | - jenv_db = helpers.make_jenv_db() |
1049 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1050 | + active_db = helpers.make_active_db() |
1051 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1052 | env_data = envs.get_env_data(env_db, 'lxc') |
1053 | env_description = envs.get_env_short_description(env_data) |
1054 | contents = self.app.get_contents() |
1055 | @@ -498,32 +497,32 @@ |
1056 | def test_status_with_errors(self): |
1057 | # The status message explains how errors are displayed. |
1058 | env_db = helpers.make_env_db() |
1059 | - jenv_db = {'environments': {}} |
1060 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1061 | + active_db = {'environments': {}} |
1062 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1063 | status = self.app.get_status() |
1064 | self.assertEqual(self.base_status + ' \N{BULLET} has errors ', status) |
1065 | |
1066 | def test_status_with_default(self): |
1067 | # The status message explains how default environment is represented. |
1068 | env_db = helpers.make_env_db(default='lxc', exclude_invalid=True) |
1069 | - jenv_db = {'environments': {}} |
1070 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1071 | + active_db = {'environments': {}} |
1072 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1073 | status = self.app.get_status() |
1074 | self.assertEqual(self.base_status + ' \N{CHECK MARK} default ', status) |
1075 | |
1076 | def test_status_with_active(self): |
1077 | # The status message explains how active environments are displayed. |
1078 | env_db = helpers.make_env_db(exclude_invalid=True) |
1079 | - jenv_db = helpers.make_jenv_db() |
1080 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1081 | + active_db = helpers.make_active_db() |
1082 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1083 | status = self.app.get_status() |
1084 | self.assertEqual(self.base_status + ' \N{BULLET} active ', status) |
1085 | |
1086 | def test_complete_status(self): |
1087 | # The status message includes default, active and errors explanations. |
1088 | env_db = helpers.make_env_db(default='lxc') |
1089 | - jenv_db = helpers.make_jenv_db() |
1090 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1091 | + active_db = helpers.make_active_db() |
1092 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1093 | status = self.app.get_status() |
1094 | self.assertEqual( |
1095 | self.base_status + |
1096 | @@ -535,8 +534,8 @@ |
1097 | def test_base_status(self): |
1098 | # The status only includes navigation info if there are no errors. |
1099 | env_db = helpers.make_env_db(exclude_invalid=True) |
1100 | - jenv_db = {'environments': {}} |
1101 | - views.env_index(self.app, self.make_params(env_db, jenv_db)) |
1102 | + active_db = {'environments': {}} |
1103 | + views.env_index(self.app, self.make_params(env_db, active_db)) |
1104 | status = self.app.get_status() |
1105 | self.assertEqual(self.base_status, status) |
1106 | |
1107 | @@ -545,12 +544,12 @@ |
1108 | |
1109 | base_status = ' \N{RIGHTWARDS ARROW OVER LEFTWARDS ARROW} navigate ' |
1110 | env_db = helpers.make_env_db(default='lxc') |
1111 | - jenv_db = helpers.make_jenv_db() |
1112 | + active_db = helpers.make_active_db() |
1113 | |
1114 | def call_view(self, env_name='lxc'): |
1115 | """Call the view passing the env_data corresponding to env_name.""" |
1116 | self.env_data = envs.get_env_data(self.env_db, env_name) |
1117 | - self.params = self.make_params(self.env_db, self.jenv_db) |
1118 | + self.params = self.make_params(self.env_db, self.active_db) |
1119 | return views.env_detail(self.app, self.params, self.env_data) |
1120 | |
1121 | def test_view_default_return_value_on_exit(self): |
1122 | @@ -707,16 +706,16 @@ |
1123 | self.assertEqual(self.base_status, status) |
1124 | |
1125 | |
1126 | -class TestJenvDetail(EnvViewTestsMixin, unittest.TestCase): |
1127 | +class TestActiveDetail(EnvViewTestsMixin, unittest.TestCase): |
1128 | |
1129 | env_db = helpers.make_env_db(default='lxc') |
1130 | - jenv_db = helpers.make_jenv_db() |
1131 | + active_db = helpers.make_active_db() |
1132 | |
1133 | def call_view(self, env_name='lxc'): |
1134 | """Call the view passing the env_data corresponding to env_name.""" |
1135 | - self.env_data = envs.get_env_data(self.jenv_db, env_name) |
1136 | - self.params = self.make_params(self.env_db, self.jenv_db) |
1137 | - return views.jenv_detail(self.app, self.params, self.env_data) |
1138 | + self.env_data = envs.get_env_data(self.active_db, env_name) |
1139 | + self.params = self.make_params(self.env_db, self.active_db) |
1140 | + return views.active_detail(self.app, self.params, self.env_data) |
1141 | |
1142 | def test_view_default_return_value_on_exit(self): |
1143 | # The view configures the app so that the return value on user exit is |
1144 | @@ -730,19 +729,19 @@ |
1145 | |
1146 | def test_view_title(self): |
1147 | # The application title is correctly set up: it shows the description |
1148 | - # of the current jenv environment. |
1149 | + # of the current active environment. |
1150 | self.call_view() |
1151 | - env_description = jenv.get_env_short_description(self.env_data) |
1152 | + env_description = envs.get_env_short_description(self.env_data) |
1153 | self.assertEqual(env_description, self.app.get_title()) |
1154 | |
1155 | def test_view_contents(self): |
1156 | - # The view displays the jenv details. |
1157 | + # The view displays the active details. |
1158 | self.call_view() |
1159 | widgets = self.get_widgets_in_contents( |
1160 | filter_function=self.is_a(urwid.Text)) |
1161 | expected_texts = [ |
1162 | '{}: {}'.format(label, value) for label, value |
1163 | - in jenv.get_env_details(self.env_data) |
1164 | + in apiinfo.get_env_details(self.env_data) |
1165 | ] |
1166 | for expected_text, widget in zip(expected_texts, widgets): |
1167 | self.assertEqual(expected_text, widget.text) |
1168 | @@ -752,7 +751,7 @@ |
1169 | self.call_view(env_name='ec2-west') |
1170 | buttons = self.get_control_buttons() |
1171 | captions = map(cli_helpers.get_button_caption, buttons) |
1172 | - self.assertEqual(['back', 'use', 'remove'], captions) |
1173 | + self.assertEqual(['back', 'use'], captions) |
1174 | |
1175 | @mock.patch('quickstart.cli.views.env_index') |
1176 | def test_back_button(self, mock_env_index): |
1177 | @@ -775,64 +774,11 @@ |
1178 | self.assertEqual( |
1179 | expected_return_value, context_manager.exception.return_value) |
1180 | |
1181 | - def test_remove_button(self): |
1182 | - # A confirmation dialog is displayed if the "remove" button is clicked. |
1183 | - self.call_view(env_name='test-jenv') |
1184 | - buttons, _ = self.click_remove_button('test-jenv') |
1185 | - # The dialog includes the "cancel" and "confirm" buttons. |
1186 | - self.assertEqual(2, len(buttons)) |
1187 | - captions = map(cli_helpers.get_button_caption, buttons) |
1188 | - self.assertEqual(['cancel', 'confirm'], captions) |
1189 | - |
1190 | - def test_remove_cancelled(self): |
1191 | - # The "remove" confirmation dialog can be safely dismissed. |
1192 | - self.call_view(env_name='test-jenv') |
1193 | - original_contents = self.cancel_removal('test-jenv') |
1194 | - # The original contents have been restored. |
1195 | - self.assertIs(original_contents, self.app.get_contents()) |
1196 | - |
1197 | - @mock.patch('quickstart.cli.views.env_index') |
1198 | - def test_remove_confirmed(self, mock_env_index): |
1199 | - # The jenv file is removed if the "remove" button is clicked and then |
1200 | - # then the deletion is confirmed. Subsequently the application switches |
1201 | - # to the index view. |
1202 | - env_name = 'test-jenv' |
1203 | - self.call_view(env_name=env_name) |
1204 | - self.confirm_removal(env_name) |
1205 | - # A message notifies the environment has been removed. |
1206 | - self.assertEqual( |
1207 | - '{} successfully removed'.format(env_name), self.app.get_message()) |
1208 | - # The index view has been called passing the modified jenv_db params. |
1209 | - self.assertTrue(mock_env_index.called) |
1210 | - params = mock_env_index.call_args[0][1] |
1211 | - # The new jenv_db no longer includes the "test-jenv" environment. |
1212 | - self.assertNotIn(env_name, params.jenv_db['environments']) |
1213 | - # The corresponding jenv file has been removed. |
1214 | - self.remove_jenv_callable.assert_called_once_with(env_name) |
1215 | - self.assertEqual( |
1216 | - 'test-jenv successfully removed', self.app.get_message()) |
1217 | - |
1218 | - @mock.patch('quickstart.cli.views.env_index') |
1219 | - def test_remove_confirmed_error(self, mock_env_index): |
1220 | - # Errors occurred while trying to remove the jenv files are notified. |
1221 | - env_name = 'test-jenv' |
1222 | - self.call_view(env_name=env_name) |
1223 | - # Simulate an error removing the jenv file. |
1224 | - self.remove_jenv_callable.return_value = 'bad wolf' |
1225 | - self.confirm_removal(env_name) |
1226 | - # The error is notified. |
1227 | - self.assertEqual('bad wolf'.format(env_name), self.app.get_message()) |
1228 | - # The index view has been called passing the original jenv_db params. |
1229 | - self.assertTrue(mock_env_index.called) |
1230 | - params = mock_env_index.call_args[0][1] |
1231 | - # The jenv_db still includes the "test_jenv" environment. |
1232 | - self.assertIn(env_name, params.jenv_db['environments']) |
1233 | - |
1234 | |
1235 | class TestEnvEdit(EnvViewTestsMixin, unittest.TestCase): |
1236 | |
1237 | env_db = helpers.make_env_db(default='lxc') |
1238 | - jenv_db = helpers.make_jenv_db() |
1239 | + active_db = helpers.make_active_db() |
1240 | |
1241 | def call_view(self, env_name='lxc', env_type=None): |
1242 | """Call the view passing the env_data corresponding to env_name. |
1243 | @@ -844,7 +790,7 @@ |
1244 | self.env_data = envs.get_env_data(self.env_db, env_name) |
1245 | else: |
1246 | self.env_data = {'type': env_type} |
1247 | - self.params = self.make_params(self.env_db, self.jenv_db) |
1248 | + self.params = self.make_params(self.env_db, self.active_db) |
1249 | return views.env_edit(self.app, self.params, self.env_data) |
1250 | |
1251 | def get_form_contents(self): |
1252 | |
1253 | === modified file 'quickstart/tests/functional/test_functional.py' |
1254 | --- quickstart/tests/functional/test_functional.py 2015-08-12 12:36:32 +0000 |
1255 | +++ quickstart/tests/functional/test_functional.py 2015-11-05 10:58:41 +0000 |
1256 | @@ -91,7 +91,7 @@ |
1257 | Return a tuple including the command exit code, its output and error. |
1258 | """ |
1259 | platform = platform_support.get_platform() |
1260 | - cmd, _ = platform_support.get_juju_command(platform) |
1261 | + cmd = platform_support.get_juju_command(platform) |
1262 | return utils.call(cmd, *args) |
1263 | |
1264 | |
1265 | |
1266 | === modified file 'quickstart/tests/helpers.py' |
1267 | --- quickstart/tests/helpers.py 2015-04-23 12:16:20 +0000 |
1268 | +++ quickstart/tests/helpers.py 2015-11-05 10:58:41 +0000 |
1269 | @@ -148,6 +148,22 @@ |
1270 | return env_file.name |
1271 | |
1272 | |
1273 | +class FakeApiInfo(object): |
1274 | + """Implement the "quickstart.models.apiinfo.Info" interface for tests.""" |
1275 | + |
1276 | + def __init__(self, envs): |
1277 | + """Initialize the fake object with the given envs.""" |
1278 | + self.envs = collections.OrderedDict((env['name'], env) for env in envs) |
1279 | + |
1280 | + def get(self, env_name): |
1281 | + """See "quickstart.models.apiinfo.Info.get.""" |
1282 | + return self.envs.get(env_name, {}) |
1283 | + |
1284 | + def all(self): |
1285 | + """See "quickstart.models.apiinfo.Info.all.""" |
1286 | + return {'environments': self.envs} |
1287 | + |
1288 | + |
1289 | class JenvFileTestsMixin(object): |
1290 | """Shared methods for testing Juju generated environment files (jenv).""" |
1291 | |
1292 | @@ -271,22 +287,25 @@ |
1293 | return env_db |
1294 | |
1295 | |
1296 | -def make_jenv_db(): |
1297 | - """Create and return a jenv files database.""" |
1298 | +def make_active_db(): |
1299 | + """Create and return a active environments database.""" |
1300 | environments = { |
1301 | 'ec2-west': { |
1302 | - 'type': '__unknown__', |
1303 | 'user': 'who', |
1304 | + 'password': 'geronimo', |
1305 | + 'uuid': 'ec2-uuid', |
1306 | 'state-servers': ('1.2.3.4:42', '1.2.3.4:47'), |
1307 | }, |
1308 | 'lxc': { |
1309 | - 'type': 'local', |
1310 | 'user': 'dalek', |
1311 | + 'password': 'exterminate', |
1312 | + 'uuid': 'lxc-uuid', |
1313 | 'state-servers': ('localhost:17070', '10.0.3.1:17070'), |
1314 | }, |
1315 | - 'test-jenv': { |
1316 | - 'type': '__unknown__', |
1317 | + 'test-env': { |
1318 | 'user': 'my-user', |
1319 | + 'password': 'my-password', |
1320 | + 'uuid': 'test-uuid', |
1321 | 'state-servers': ('10.0.3.1:17070',), |
1322 | }, |
1323 | } |
1324 | |
1325 | === added file 'quickstart/tests/models/test_apiinfo.py' |
1326 | --- quickstart/tests/models/test_apiinfo.py 1970-01-01 00:00:00 +0000 |
1327 | +++ quickstart/tests/models/test_apiinfo.py 2015-11-05 10:58:41 +0000 |
1328 | @@ -0,0 +1,242 @@ |
1329 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
1330 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
1331 | +# Copyright (C) 2015 Canonical Ltd. |
1332 | +# |
1333 | +# This program is free software: you can redistribute it and/or modify it under |
1334 | +# the terms of the GNU Affero General Public License version 3, as published by |
1335 | +# the Free Software Foundation. |
1336 | +# |
1337 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
1338 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
1339 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
1340 | +# Affero General Public License for more details. |
1341 | +# |
1342 | +# You should have received a copy of the GNU Affero General Public License |
1343 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1344 | + |
1345 | +"""Tests for the Juju Quickstart jenv generated files handling.""" |
1346 | + |
1347 | +from __future__ import unicode_literals |
1348 | + |
1349 | +from contextlib import contextmanager |
1350 | +import json |
1351 | +import os |
1352 | +import shutil |
1353 | +import tempfile |
1354 | +import unittest |
1355 | + |
1356 | +import mock |
1357 | + |
1358 | +from quickstart.models import apiinfo |
1359 | +from quickstart.tests import helpers |
1360 | + |
1361 | + |
1362 | +class TestInfo( |
1363 | + unittest.TestCase, helpers.CallTestsMixin, helpers.JenvFileTestsMixin): |
1364 | + |
1365 | + def setUp(self): |
1366 | + # Instantiate an Info object. |
1367 | + self.juju_command = '/path/to/juju' |
1368 | + self.info = apiinfo.Info(self.juju_command) |
1369 | + |
1370 | + @contextmanager |
1371 | + def make_juju_home(self, envs=()): |
1372 | + """Create a Juju home with the given optional environments. |
1373 | + |
1374 | + An empty jenv file is created for each environment name in envs. |
1375 | + The "environments" subdir of the Juju home is provided in the context |
1376 | + block. |
1377 | + """ |
1378 | + home = tempfile.mkdtemp() |
1379 | + self.addCleanup(shutil.rmtree, home) |
1380 | + envs_dir = os.path.join(home, 'environments') |
1381 | + os.mkdir(envs_dir) |
1382 | + for env in envs: |
1383 | + jenv = os.path.join(envs_dir, env + '.jenv') |
1384 | + open(jenv, 'a').close() |
1385 | + with mock.patch('quickstart.settings.JUJU_HOME', home): |
1386 | + yield envs_dir |
1387 | + |
1388 | + def test_get(self): |
1389 | + # The environment info is returned correctly. |
1390 | + output = json.dumps({ |
1391 | + 'user': 'who', |
1392 | + 'password': 'Secret!', |
1393 | + 'environ-uuid': 'env-uuid', |
1394 | + 'state-servers': ['localhost:17070', '10.0.3.1:17070'], |
1395 | + }) |
1396 | + with self.patch_call(0, output, '') as mock_call: |
1397 | + info = self.info.get('ec2') |
1398 | + mock_call.assert_called_once_with( |
1399 | + self.juju_command, 'api-info', '-e', 'ec2', '--password', |
1400 | + '--format', u'json') |
1401 | + expected_info = { |
1402 | + 'name': 'ec2', |
1403 | + 'user': 'who', |
1404 | + 'password': 'Secret!', |
1405 | + 'uuid': 'env-uuid', |
1406 | + 'state-servers': ['localhost:17070', '10.0.3.1:17070'], |
1407 | + } |
1408 | + self.assertEqual(expected_info, info) |
1409 | + |
1410 | + def test_get_error(self): |
1411 | + # An empty dict is returned if the environment info cannot be |
1412 | + # retrieved. |
1413 | + expected_message = 'unable to get API info for ec2: bad wolf' |
1414 | + with self.patch_call(1, '', 'bad wolf'): |
1415 | + with helpers.assert_logs([expected_message], level='debug'): |
1416 | + info = self.info.get('ec2') |
1417 | + self.assertEqual({}, info) |
1418 | + |
1419 | + def test_all(self): |
1420 | + # The active environments database is properly returned. |
1421 | + output1 = '\n'.join(['local', 'ec2']) |
1422 | + output2 = json.dumps({ |
1423 | + 'user': 'local-user', |
1424 | + 'password': 'pswd1', |
1425 | + 'environ-uuid': 'local-uuid', |
1426 | + 'state-servers': ['localhost:17070', '10.0.3.1:17070'], |
1427 | + }) |
1428 | + output3 = json.dumps({ |
1429 | + 'user': 'ec2-user', |
1430 | + 'password': 'pswd2', |
1431 | + 'environ-uuid': 'ec2-uuid', |
1432 | + 'state-servers': ['1.2.3.4:17070'], |
1433 | + }) |
1434 | + side_effect = [ |
1435 | + # First call to retrieve the list of environments. |
1436 | + (0, output1, ''), |
1437 | + # Second call to retrieve info on the local environment. |
1438 | + (0, output2, ''), |
1439 | + # Third call to retrieve info on the ec2 environment. |
1440 | + (0, output3, ''), |
1441 | + ] |
1442 | + with self.patch_multiple_calls(side_effect) as mock_call: |
1443 | + db = self.info.all() |
1444 | + self.assertEqual(3, mock_call.call_count) |
1445 | + mock_call.assert_has_calls([ |
1446 | + mock.call(self.juju_command, 'system', 'list'), |
1447 | + mock.call(self.juju_command, 'api-info', '-e', 'local', |
1448 | + '--password', '--format', u'json'), |
1449 | + mock.call(self.juju_command, 'api-info', '-e', 'ec2', '--password', |
1450 | + '--format', u'json'), |
1451 | + ]) |
1452 | + expected_db = {'environments': { |
1453 | + 'ec2': { |
1454 | + 'name': 'ec2', |
1455 | + 'user': 'ec2-user', |
1456 | + 'password': 'pswd2', |
1457 | + 'uuid': 'ec2-uuid', |
1458 | + 'state-servers': ['1.2.3.4:17070'], |
1459 | + }, |
1460 | + 'local': { |
1461 | + 'name': 'local', |
1462 | + 'user': 'local-user', |
1463 | + 'password': 'pswd1', |
1464 | + 'uuid': 'local-uuid', |
1465 | + 'state-servers': ['localhost:17070', '10.0.3.1:17070'], |
1466 | + }, |
1467 | + }} |
1468 | + self.assertEqual(expected_db, db) |
1469 | + |
1470 | + def test_all_empty(self): |
1471 | + # An empty active environments database is returned when there are no |
1472 | + # active environments. |
1473 | + with self.patch_call(0, '', '') as mock_call: |
1474 | + db = self.info.all() |
1475 | + mock_call.assert_called_once_with(self.juju_command, 'system', 'list') |
1476 | + self.assertEqual({'environments': {}}, db) |
1477 | + |
1478 | + def test_all_legacy(self): |
1479 | + # Active environments are detected even when using an old version of |
1480 | + # Juju not supporting controllers. |
1481 | + output1 = json.dumps({ |
1482 | + 'user': 'ec2-user', |
1483 | + 'password': 'pswd2', |
1484 | + 'environ-uuid': 'ec2-uuid', |
1485 | + 'state-servers': ['1.2.3.4:17070'], |
1486 | + }) |
1487 | + output2 = json.dumps({ |
1488 | + 'user': 'local-user', |
1489 | + 'password': 'pswd1', |
1490 | + 'environ-uuid': 'local-uuid', |
1491 | + 'state-servers': ['localhost:17070', '10.0.3.1:17070'], |
1492 | + }) |
1493 | + side_effect = [ |
1494 | + # First call to retrieve the list of environments. |
1495 | + (1, '', 'not implemented'), |
1496 | + # Second call to retrieve info on the local environment. |
1497 | + (0, output1, ''), |
1498 | + # Third call to retrieve info on the ec2 environment. |
1499 | + (0, output2, ''), |
1500 | + ] |
1501 | + with self.make_juju_home(envs=('local', 'ec2')): |
1502 | + with self.patch_multiple_calls(side_effect) as mock_call: |
1503 | + db = self.info.all() |
1504 | + self.assertEqual(3, mock_call.call_count) |
1505 | + mock_call.assert_has_calls([ |
1506 | + mock.call(self.juju_command, 'system', 'list'), |
1507 | + mock.call(self.juju_command, 'api-info', '-e', 'ec2', '--password', |
1508 | + '--format', u'json'), |
1509 | + mock.call(self.juju_command, 'api-info', '-e', 'local', |
1510 | + '--password', '--format', u'json'), |
1511 | + ]) |
1512 | + expected_db = {'environments': { |
1513 | + 'ec2': { |
1514 | + 'name': 'ec2', |
1515 | + 'user': 'ec2-user', |
1516 | + 'password': 'pswd2', |
1517 | + 'uuid': 'ec2-uuid', |
1518 | + 'state-servers': ['1.2.3.4:17070'], |
1519 | + }, |
1520 | + 'local': { |
1521 | + 'name': 'local', |
1522 | + 'user': 'local-user', |
1523 | + 'password': 'pswd1', |
1524 | + 'uuid': 'local-uuid', |
1525 | + 'state-servers': ['localhost:17070', '10.0.3.1:17070'], |
1526 | + }, |
1527 | + }} |
1528 | + self.assertEqual(expected_db, db) |
1529 | + |
1530 | + def test_all_legacy_empty(self): |
1531 | + # An empty active environments database is returned when there are no |
1532 | + # active environments and an old version of Juju is in use. |
1533 | + with self.make_juju_home() as envs_dir: |
1534 | + # Directories and non-jenv files are ignored. |
1535 | + os.mkdir(os.path.join(envs_dir, 'dir')) |
1536 | + open(os.path.join(envs_dir, 'ec2.ext'), 'a').close() |
1537 | + with self.patch_call(2, '', 'not implemented'): |
1538 | + db = self.info.all() |
1539 | + self.assertEqual({'environments': {}}, db) |
1540 | + |
1541 | + def test_all_legacy_no_juju_home(self): |
1542 | + # An empty active environments database is returned when the Juju home |
1543 | + # does not exist and an old version of Juju is in use. |
1544 | + with self.make_juju_home() as envs_dir: |
1545 | + # Remove the environments directory. |
1546 | + os.rmdir(envs_dir) |
1547 | + with self.patch_call(2, '', 'not implemented'): |
1548 | + db = self.info.all() |
1549 | + self.assertEqual({'environments': {}}, db) |
1550 | + |
1551 | + |
1552 | +class TestGetEnvDetails(unittest.TestCase): |
1553 | + |
1554 | + def test_details(self): |
1555 | + # The environment details are properly returned. |
1556 | + env_data = { |
1557 | + 'name': 'lxc', |
1558 | + 'user': 'who', |
1559 | + 'password': 'pswd', |
1560 | + 'uuid': 'env-uuid', |
1561 | + 'state-servers': ('1.2.3.4:17060', 'localhost:17070'), |
1562 | + } |
1563 | + expected_details = [ |
1564 | + ('name', 'lxc'), |
1565 | + ('user', 'who'), |
1566 | + ('uuid', 'env-uuid'), |
1567 | + ('state servers', '1.2.3.4:17060, localhost:17070'), |
1568 | + ] |
1569 | + details = apiinfo.get_env_details(env_data) |
1570 | + self.assertEqual(expected_details, details) |
1571 | |
1572 | === modified file 'quickstart/tests/models/test_bundles.py' |
1573 | --- quickstart/tests/models/test_bundles.py 2015-08-11 09:42:32 +0000 |
1574 | +++ quickstart/tests/models/test_bundles.py 2015-11-05 10:58:41 +0000 |
1575 | @@ -80,22 +80,22 @@ |
1576 | 'method': 'addCharm', |
1577 | 'args': ['cs:vivid/mysql-47'], |
1578 | 'requires': []}, |
1579 | - {'id': 'addService-1', |
1580 | + {'id': 'deploy-1', |
1581 | 'method': 'deploy', |
1582 | - 'args': ['cs:vivid/mysql-47', 'mysql', {}], |
1583 | + 'args': ['$addCharm-0', 'mysql', {}, ''], |
1584 | 'requires': ['addCharm-0']}, |
1585 | {'id': 'addCharm-2', |
1586 | 'method': 'addCharm', |
1587 | 'args': ['cs:trusty/wordpress-42'], |
1588 | 'requires': []}, |
1589 | - {'id': 'addService-3', |
1590 | + {'id': 'deploy-3', |
1591 | 'method': 'deploy', |
1592 | - 'args': ['cs:trusty/wordpress-42', 'wordpress', {}], |
1593 | + 'args': ['$addCharm-2', 'wordpress', {}, ''], |
1594 | 'requires': ['addCharm-2']}, |
1595 | {'id': 'addUnit-4', |
1596 | 'method': 'addUnit', |
1597 | - 'args': ['$addService-3', 1, None], |
1598 | - 'requires': ['addService-3']}, |
1599 | + 'args': ['$deploy-3', None], |
1600 | + 'requires': ['deploy-3']}, |
1601 | ) |
1602 | bundle = helpers.make_ordered_bundle(self.bundle) |
1603 | self.assertEqual(expected_changeset, tuple(bundle.get_changeset())) |
1604 | |
1605 | === modified file 'quickstart/tests/test_app.py' |
1606 | --- quickstart/tests/test_app.py 2015-08-12 16:24:35 +0000 |
1607 | +++ quickstart/tests/test_app.py 2015-11-05 10:58:41 +0000 |
1608 | @@ -19,8 +19,6 @@ |
1609 | from __future__ import unicode_literals |
1610 | |
1611 | from contextlib import contextmanager |
1612 | -import json |
1613 | -import os |
1614 | import unittest |
1615 | |
1616 | from jujubundlelib import references |
1617 | @@ -325,8 +323,8 @@ |
1618 | |
1619 | class TestCheckJujuSupported(ProgramExitTestsMixin, unittest.TestCase): |
1620 | |
1621 | - supported_versions = [(1, 18, 1), (1, 19, 0), (1, 42, 47), (2, 0, 0)] |
1622 | - unsupported_versions = [(1, 18, 0), (1, 17, 42), (1, 0, 0), (0, 20, 47)] |
1623 | + supported_versions = [(1, 22, 1), (1, 22, 2), (1, 42, 47), (2, 0, 0)] |
1624 | + unsupported_versions = [(1, 22, 0), (1, 17, 42), (1, 0, 0), (0, 20, 47)] |
1625 | |
1626 | def test_supported(self): |
1627 | # No exceptions are raised if the Juju version is supported. |
1628 | @@ -479,57 +477,43 @@ |
1629 | self.assertTrue(mock_create_keys.called) |
1630 | |
1631 | |
1632 | -class TestCheckBootstrapped(helpers.JenvFileTestsMixin, unittest.TestCase): |
1633 | - |
1634 | - def test_no_jenv_file(self): |
1635 | - # A None API address is returned if the jenv file is not present. |
1636 | - with self.make_jenv('ec2', ''): |
1637 | - with helpers.assert_logs([], level='warn'): |
1638 | - api_address = app.check_bootstrapped('hp') |
1639 | - self.assertIsNone(api_address) |
1640 | - |
1641 | - def test_invalid_jenv_file(self): |
1642 | - # A None API address is returned if the list of API addresses cannot be |
1643 | - # retrieved from the jenv file. |
1644 | - with self.make_jenv('ec2', '') as path: |
1645 | - logs = [ |
1646 | - 'cannot retrieve the Juju API address: ' |
1647 | - 'cannot read {}: invalid YAML contents: ' |
1648 | - 'state-servers key not found in the root section'.format(path) |
1649 | - ] |
1650 | - with helpers.assert_logs(logs, level='warn'): |
1651 | - api_address = app.check_bootstrapped('ec2') |
1652 | +class TestGetApiAddress(unittest.TestCase): |
1653 | + |
1654 | + def test_no_env_info(self): |
1655 | + # A None API address is returned if no environment info is provided. |
1656 | + env_info = {} |
1657 | + with helpers.assert_logs([], level='warn'): |
1658 | + api_address = app.get_api_address(env_info) |
1659 | self.assertIsNone(api_address) |
1660 | |
1661 | def test_no_api_addresses(self): |
1662 | # A None API address is returned if the list of API addresses is empty. |
1663 | - jenv_data = {'state-servers': []} |
1664 | - logs = ['cannot retrieve the Juju API address: no addresses found'] |
1665 | - with self.make_jenv('local', yaml.safe_dump(jenv_data)): |
1666 | - with helpers.assert_logs(logs, level='warn'): |
1667 | - api_address = app.check_bootstrapped('local') |
1668 | + env_info = {'state-servers': []} |
1669 | + logs = ['cannot retrieve the API address: no addresses found'] |
1670 | + with helpers.assert_logs(logs, level='warn'): |
1671 | + api_address = app.get_api_address(env_info) |
1672 | self.assertIsNone(api_address) |
1673 | |
1674 | def test_api_address_not_listening(self): |
1675 | # A None API address is returned if there is no reachable API address. |
1676 | + env_info = {'state-servers': ['localhost:17070', '10.0.3.1:17070']} |
1677 | logs = [ |
1678 | - 'cannot retrieve the Juju API address: ' |
1679 | + 'cannot retrieve the API address: ' |
1680 | 'cannot connect to any of the following addresses: ' |
1681 | 'localhost:17070, 10.0.3.1:17070' |
1682 | ] |
1683 | - with self.make_jenv('local', yaml.safe_dump(self.jenv_data)): |
1684 | - with helpers.assert_logs(logs, level='warn'): |
1685 | - with helpers.patch_socket_create_connection('bad wolf'): |
1686 | - api_address = app.check_bootstrapped('local') |
1687 | + with helpers.assert_logs(logs, level='warn'): |
1688 | + with helpers.patch_socket_create_connection('bad wolf'): |
1689 | + api_address = app.get_api_address(env_info) |
1690 | self.assertIsNone(api_address) |
1691 | |
1692 | def test_bootstrapped(self): |
1693 | # The first listening API address is returned if the environment is |
1694 | # already bootstrapped. |
1695 | - with self.make_jenv('hp', yaml.safe_dump(self.jenv_data)): |
1696 | - with helpers.assert_logs([], level='warn'): |
1697 | - with helpers.patch_socket_create_connection(): |
1698 | - api_address = app.check_bootstrapped('hp') |
1699 | + env_info = {'state-servers': ['localhost:17070', '10.0.3.1:17070']} |
1700 | + with helpers.assert_logs([], level='warn'): |
1701 | + with helpers.patch_socket_create_connection(): |
1702 | + api_address = app.get_api_address(env_info) |
1703 | # The first API address is returned. |
1704 | self.assertEqual('localhost:17070', api_address) |
1705 | |
1706 | @@ -725,82 +709,6 @@ |
1707 | mock_call.assert_has_calls(expected_calls) |
1708 | |
1709 | |
1710 | -class TestGetEnvUuidOrNone( |
1711 | - helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
1712 | - |
1713 | - def test_success(self): |
1714 | - # The environment UUID is successfully retrieved. |
1715 | - with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)): |
1716 | - env_uuid = app.get_env_uuid_or_none('ec2') |
1717 | - self.assertEqual('__unique_identifier__', env_uuid) |
1718 | - |
1719 | - def test_no_uuid(self): |
1720 | - # None is returned if the environment UUID is not found. |
1721 | - data = {'user': 'jean-luc', 'password': 'Secret!'} |
1722 | - with self.make_jenv('ec2', yaml.safe_dump(data)): |
1723 | - env_uuid = app.get_env_uuid_or_none('ec2') |
1724 | - self.assertIsNone(env_uuid) |
1725 | - |
1726 | - def test_error(self): |
1727 | - # A ProgramExit is raised if the environment UUID cannot be retrieved. |
1728 | - with self.make_jenv('ec2', '') as path: |
1729 | - os.remove(path) |
1730 | - expected_error = ( |
1731 | - 'cannot retrieve environment unique identifier: unable to ' |
1732 | - "open file {}: [Errno 2] No such file or directory: '{}'" |
1733 | - ''.format(path, path)) |
1734 | - with self.assert_program_exit(expected_error): |
1735 | - app.get_env_uuid_or_none('ec2') |
1736 | - |
1737 | - |
1738 | -class TestGetCredentials( |
1739 | - helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
1740 | - |
1741 | - def test_success(self): |
1742 | - # The user name and password are successfully retrieved. |
1743 | - with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)): |
1744 | - username, password = app.get_credentials('ec2') |
1745 | - self.assertEqual('admin', username) |
1746 | - self.assertEqual('Secret!', password) |
1747 | - |
1748 | - def test_error(self): |
1749 | - # A ProgramExit is raised if the credentials cannot be retrieved. |
1750 | - with self.make_jenv('ec2', '') as path: |
1751 | - expected_error = ( |
1752 | - 'cannot retrieve environment credentials: cannot parse {}: ' |
1753 | - 'cannot retrieve the password: invalid YAML contents: ' |
1754 | - 'bootstrap-config key not found in the root section' |
1755 | - ''.format(path)) |
1756 | - with self.assert_program_exit(expected_error): |
1757 | - app.get_credentials('ec2') |
1758 | - |
1759 | - |
1760 | -class TestGetApiAddress( |
1761 | - helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
1762 | - |
1763 | - env_name = 'ec2' |
1764 | - juju_command = settings.JUJU_CMD_PATHS['default'] |
1765 | - |
1766 | - def test_success(self): |
1767 | - # The API address is correctly returned. |
1768 | - api_addresses = json.dumps(['api.example.com:17070', 'not-today']) |
1769 | - with self.patch_call(retcode=0, output=api_addresses) as mock_call: |
1770 | - api_address = app.get_api_address(self.env_name, self.juju_command) |
1771 | - self.assertEqual('api.example.com:17070', api_address) |
1772 | - mock_call.assert_called_once_with( |
1773 | - self.juju_command, 'api-endpoints', '-e', self.env_name, |
1774 | - '--format', 'json') |
1775 | - |
1776 | - def test_failure(self): |
1777 | - # A ProgramExit is raised if an error occurs retrieving the address. |
1778 | - with self.patch_call(retcode=1, error='bad wolf') as mock_call: |
1779 | - with self.assert_program_exit('bad wolf'): |
1780 | - app.get_api_address(self.env_name, self.juju_command) |
1781 | - mock_call.assert_called_once_with( |
1782 | - self.juju_command, 'api-endpoints', '-e', self.env_name, |
1783 | - '--format', 'json') |
1784 | - |
1785 | - |
1786 | class TestConnect(ProgramExitTestsMixin, unittest.TestCase): |
1787 | |
1788 | username = 'MyUser' |
1789 | |
1790 | === modified file 'quickstart/tests/test_manage.py' |
1791 | --- quickstart/tests/test_manage.py 2015-05-29 15:03:27 +0000 |
1792 | +++ quickstart/tests/test_manage.py 2015-11-05 10:58:41 +0000 |
1793 | @@ -40,9 +40,9 @@ |
1794 | views, |
1795 | ) |
1796 | from quickstart.models import ( |
1797 | + apiinfo, |
1798 | bundles, |
1799 | envs, |
1800 | - jenv, |
1801 | ) |
1802 | from quickstart.tests import helpers |
1803 | |
1804 | @@ -229,14 +229,14 @@ |
1805 | self.env_type_db = envs.get_env_type_db() |
1806 | self.env_file = self.make_env_file() |
1807 | self.env_db = envs.load(self.env_file) |
1808 | - self.jenv_db = helpers.make_jenv_db() |
1809 | + self.active_db = helpers.make_active_db() |
1810 | |
1811 | @contextmanager |
1812 | - def patch_interactive_mode(self, env_db, jenv_db, return_value): |
1813 | + def patch_interactive_mode(self, env_db, active_db, return_value): |
1814 | """Patch the quickstart.cli.views.show function. |
1815 | |
1816 | Ensure the interactive mode is started by the code in the context block |
1817 | - passing the given env_db and jenv_db. |
1818 | + passing the given env_db and active_db. |
1819 | Make the view return the given return_value. |
1820 | """ |
1821 | create_save_callable_path = 'quickstart.manage._create_save_callable' |
1822 | @@ -248,9 +248,8 @@ |
1823 | expected_params = params.Params( |
1824 | env_type_db=self.env_type_db, |
1825 | env_db=env_db, |
1826 | - jenv_db=jenv_db, |
1827 | + active_db=active_db, |
1828 | save_callable=mock_save_callable(), |
1829 | - remove_jenv_callable=jenv.remove, |
1830 | ) |
1831 | mock_show.assert_called_once_with(views.env_index, expected_params) |
1832 | |
1833 | @@ -259,9 +258,9 @@ |
1834 | # which case the function returns the corresponding env_data. |
1835 | env_data = envs.get_env_data(self.env_db, 'aws') |
1836 | with self.patch_interactive_mode( |
1837 | - self.env_db, self.jenv_db, [self.env_db, env_data]): |
1838 | + self.env_db, self.active_db, [self.env_db, env_data]): |
1839 | obtained_env_data = manage._start_interactive_session( |
1840 | - self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1841 | + self.parser, self.env_type_db, self.env_db, self.active_db, |
1842 | self.env_file) |
1843 | self.assertEqual(env_data, obtained_env_data) |
1844 | |
1845 | @@ -272,9 +271,9 @@ |
1846 | env_data = envs.get_env_data(self.env_db, 'aws') |
1847 | new_env_db = helpers.make_env_db() |
1848 | with self.patch_interactive_mode( |
1849 | - self.env_db, self.jenv_db, [new_env_db, env_data]): |
1850 | + self.env_db, self.active_db, [new_env_db, env_data]): |
1851 | manage._start_interactive_session( |
1852 | - self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1853 | + self.parser, self.env_type_db, self.env_db, self.active_db, |
1854 | self.env_file) |
1855 | mock_print.assert_called_once_with( |
1856 | 'changes to the environments file have been saved') |
1857 | @@ -284,9 +283,9 @@ |
1858 | # If the user explicitly quits the interactive mode, the program exits |
1859 | # without proceeding with the environment bootstrapping. |
1860 | with self.patch_interactive_mode( |
1861 | - self.env_db, self.jenv_db, [self.env_db, None]): |
1862 | + self.env_db, self.active_db, [self.env_db, None]): |
1863 | manage._start_interactive_session( |
1864 | - self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1865 | + self.parser, self.env_type_db, self.env_db, self.active_db, |
1866 | self.env_file) |
1867 | mock_exit.assert_called_once_with('quitting') |
1868 | |
1869 | @@ -298,28 +297,28 @@ |
1870 | self.parser = mock.Mock() |
1871 | self.env_type_db = envs.get_env_type_db() |
1872 | self.env_db = helpers.make_env_db() |
1873 | - self.jenv_db = helpers.make_jenv_db() |
1874 | + self.active_db = helpers.make_active_db() |
1875 | |
1876 | def test_resulting_env_data(self): |
1877 | # The env_data is correctly validated and returned. |
1878 | expected_env_data = envs.get_env_data(self.env_db, 'lxc') |
1879 | env_data = manage._retrieve_env_data( |
1880 | - self.parser, self.env_type_db, self.env_db, self.jenv_db, 'lxc') |
1881 | + self.parser, self.env_type_db, self.env_db, self.active_db, 'lxc') |
1882 | self.assertEqual(expected_env_data, env_data) |
1883 | |
1884 | def test_jenv_data(self): |
1885 | # The env_data is correctly retrieved from the jenv database. |
1886 | - expected_env_data = envs.get_env_data(self.jenv_db, 'test-jenv') |
1887 | + expected_env_data = envs.get_env_data(self.active_db, 'test-env') |
1888 | env_data = manage._retrieve_env_data( |
1889 | - self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1890 | - 'test-jenv') |
1891 | + self.parser, self.env_type_db, self.env_db, self.active_db, |
1892 | + 'test-env') |
1893 | self.assertEqual(expected_env_data, env_data) |
1894 | |
1895 | def test_error_environment_not_found(self): |
1896 | # A parser error is invoked if the provided environment is not included |
1897 | # in the environments database. |
1898 | manage._retrieve_env_data( |
1899 | - self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1900 | + self.parser, self.env_type_db, self.env_db, self.active_db, |
1901 | 'no-such') |
1902 | self.parser.error.assert_called_once_with( |
1903 | 'environment no-such not found') |
1904 | @@ -327,7 +326,7 @@ |
1905 | def test_error_environment_not_valid(self): |
1906 | # A parser error is invoked if the selected environment is not valid. |
1907 | manage._retrieve_env_data( |
1908 | - self.parser, self.env_type_db, self.env_db, self.jenv_db, |
1909 | + self.parser, self.env_type_db, self.env_db, self.active_db, |
1910 | 'local-with-errors') |
1911 | self.parser.error.assert_called_once_with( |
1912 | 'cannot use the local-with-errors environment:\n' |
1913 | @@ -367,14 +366,16 @@ |
1914 | class TestValidatePlatform(unittest.TestCase): |
1915 | |
1916 | def setUp(self): |
1917 | - # Set up a parser. |
1918 | + # Set up options and a parser. |
1919 | + self.options = argparse.Namespace() |
1920 | self.parser = mock.Mock() |
1921 | |
1922 | def test_platform_validation_fails(self): |
1923 | # If the platform validation fails a parser error is given. |
1924 | path = 'quickstart.manage.platform_support.validate_platform' |
1925 | with mock.patch(path, side_effect=ValueError('Bad platform, yo')): |
1926 | - manage._validate_platform(settings.LINUX_RPM, self.parser) |
1927 | + manage._validate_platform( |
1928 | + settings.LINUX_RPM, self.options, self.parser) |
1929 | self.parser.error.assert_called_once_with( |
1930 | 'Bad platform, yo') |
1931 | |
1932 | @@ -382,8 +383,13 @@ |
1933 | # If the platform validation passes it returns None. |
1934 | path = 'quickstart.platform_support.validate_platform' |
1935 | with mock.patch(path, side_effect=None): |
1936 | - result = manage._validate_platform(settings.LINUX_RPM, self.parser) |
1937 | + with mock.patch('os.environ', {'JUJU': '/tmp/juju'}): |
1938 | + result = manage._validate_platform( |
1939 | + settings.LINUX_RPM, self.options, self.parser) |
1940 | self.assertIsNone(result) |
1941 | + self.assertEqual('/tmp/juju', self.options.juju_command) |
1942 | + self.assertEqual(settings.LINUX_RPM, self.options.platform) |
1943 | + self.assertIsInstance(self.options.info, apiinfo.Info) |
1944 | |
1945 | |
1946 | class TestValidatePort(unittest.TestCase): |
1947 | @@ -409,9 +415,7 @@ |
1948 | 'invalid Juju GUI port: not in range 1-65535') |
1949 | |
1950 | |
1951 | -class TestSetupEnv( |
1952 | - helpers.EnvFileTestsMixin, helpers.JenvFileTestsMixin, |
1953 | - unittest.TestCase): |
1954 | +class TestSetupEnv(helpers.EnvFileTestsMixin, unittest.TestCase): |
1955 | |
1956 | def setUp(self): |
1957 | self.parser = mock.Mock() |
1958 | @@ -422,6 +426,13 @@ |
1959 | return mock.Mock( |
1960 | env_file=env_file, |
1961 | env_name=env_name, |
1962 | + info=helpers.FakeApiInfo([{ |
1963 | + 'name': 'ec2', |
1964 | + 'user': 'the-doctor', |
1965 | + 'password': 'in-the-tardis', |
1966 | + 'uuid': 'who', |
1967 | + 'state-servers': ['1.2.3.4:17070'], |
1968 | + }]), |
1969 | interactive=interactive, |
1970 | platform=platform, |
1971 | ) |
1972 | @@ -514,17 +525,16 @@ |
1973 | # Simulate the user did not make any changes to the env_db from the |
1974 | # interactive session. |
1975 | env_db = yaml.load(self.valid_contents) |
1976 | + active_db = options.info.all() |
1977 | # Simulate the aws environment has been selected and started from the |
1978 | # interactive session. |
1979 | env_data = envs.get_env_data(env_db, 'aws') |
1980 | get_env_type_db_path = 'quickstart.models.envs.get_env_type_db' |
1981 | with mock.patch(get_env_type_db_path) as mock_get_env_type_db: |
1982 | - with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)): |
1983 | - jenv_db = jenv.get_env_db() |
1984 | - with self.patch_interactive_mode(env_data) as mock_interactive: |
1985 | - manage._setup_env(options, self.parser) |
1986 | + with self.patch_interactive_mode(env_data) as mock_interactive: |
1987 | + manage._setup_env(options, self.parser) |
1988 | mock_interactive.assert_called_once_with( |
1989 | - self.parser, mock_get_env_type_db(), env_db, jenv_db, env_file) |
1990 | + self.parser, mock_get_env_type_db(), env_db, active_db, env_file) |
1991 | # The options is updated with data from the selected environment. |
1992 | self.assertEqual(env_file, options.env_file) |
1993 | self.assertEqual('aws', options.env_name) |
1994 | @@ -688,7 +698,13 @@ |
1995 | @mock.patch('__builtin__.print', mock.Mock()) |
1996 | class TestRun(helpers.BundleFileTestsMixin, unittest.TestCase): |
1997 | |
1998 | - juju_command = '/sbin/juju' |
1999 | + env_info = { |
2000 | + 'name': 'aws', |
2001 | + 'user': 'MyUser', |
2002 | + 'password': 'Secret!', |
2003 | + 'uuid': 'env-uuid', |
2004 | + 'state-servers': ['1.2.3.4:17070', 'localhost:1234'], |
2005 | + } |
2006 | |
2007 | def make_options(self, **kwargs): |
2008 | """Set up the options to be passed to the run function.""" |
2009 | @@ -700,6 +716,8 @@ |
2010 | 'env_name': 'aws', |
2011 | 'env_type': 'ec2', |
2012 | 'gui_source': None, |
2013 | + 'info': helpers.FakeApiInfo([self.env_info]), |
2014 | + 'juju_command': '/sbin/juju', |
2015 | 'open_browser': True, |
2016 | 'port': None, |
2017 | 'uncommitted': False, |
2018 | @@ -724,18 +742,12 @@ |
2019 | 'check_juju_supported': None, |
2020 | # Ensure the SSH keys are properly configured. |
2021 | 'ensure_ssh_keys': None, |
2022 | - # The environment is not already bootstrapped. |
2023 | - 'check_bootstrapped': None, |
2024 | + # The environment is not already bootstrapped: the |
2025 | + # "app.get_api_address" call is handled below. |
2026 | # This is also confirmed by the bootstrap function. |
2027 | 'bootstrap': False, |
2028 | # Status is then called, returning the bootstrap node series. |
2029 | 'status': 'trusty', |
2030 | - # The API address must be retrieved (the environment is not ready). |
2031 | - 'get_api_address': '1.2.3.4:17070', |
2032 | - # Retrieve the environment unique identifier. |
2033 | - 'get_env_uuid_or_none': 'env-uuid', |
2034 | - # Retrieve the environment credentials. |
2035 | - 'get_credentials': ('MyUser', 'Secret!'), |
2036 | # Connect to the Juju Environment API endpoint. |
2037 | 'connect': env, |
2038 | # The environment is then checked. |
2039 | @@ -760,38 +772,38 @@ |
2040 | defaults.update(kwargs) |
2041 | for attr, return_value in defaults.items(): |
2042 | getattr(mock_app, attr).return_value = return_value |
2043 | + # The "app.get_api_address" function is called twice. |
2044 | + mock_app.get_api_address.side_effect = [ |
2045 | + None, self.env_info['state-servers'][0]] |
2046 | return env |
2047 | |
2048 | - def patch_get_juju_command(self): |
2049 | - """Patch the platform_support.get_juju_command function.""" |
2050 | - path = 'quickstart.manage.platform_support.get_juju_command' |
2051 | - return mock.patch(path, return_value=(self.juju_command, False)) |
2052 | - |
2053 | def test_run(self, mock_app, mock_open): |
2054 | # The application runs correctly if no bundle is provided. |
2055 | env = self.configure_app(mock_app) |
2056 | # Run the application. |
2057 | options = self.make_options() |
2058 | - with self.patch_get_juju_command(): |
2059 | - manage.run(options) |
2060 | + manage.run(options) |
2061 | # Ensure the functions have been used correctly. |
2062 | mock_app.ensure_dependencies.assert_called_once_with( |
2063 | - options.distro_only, options.platform, self.juju_command) |
2064 | + options.distro_only, options.platform, options.juju_command) |
2065 | mock_app.check_juju_supported.assert_called_once_with((1, 22, 0)) |
2066 | mock_app.ensure_ssh_keys.assert_called_once_with() |
2067 | - mock_app.check_bootstrapped.assert_called_once_with(options.env_name) |
2068 | + # The "app.get_api_address" function has been called twice: the first |
2069 | + # time to check if the environment is already bootstrapped, the second |
2070 | + # time after the environment has been effectively bootstrapped. |
2071 | + self.assertEqual(2, mock_app.get_api_address.call_count) |
2072 | + mock_app.get_api_address.assert_has_calls([ |
2073 | + mock.call(self.env_info), |
2074 | + mock.call(self.env_info), |
2075 | + ]) |
2076 | mock_app.bootstrap.assert_called_once_with( |
2077 | - options.env_name, self.juju_command, |
2078 | + options.env_name, options.juju_command, |
2079 | debug=options.debug, |
2080 | upload_tools=options.upload_tools, |
2081 | upload_series=options.upload_series, |
2082 | constraints=options.constraints) |
2083 | mock_app.status.assert_called_once_with( |
2084 | - options.env_name, self.juju_command) |
2085 | - mock_app.get_api_address.assert_called_once_with( |
2086 | - options.env_name, self.juju_command) |
2087 | - mock_app.get_env_uuid_or_none.assert_called_once_with(options.env_name) |
2088 | - mock_app.get_credentials.assert_called_once_with(options.env_name) |
2089 | + options.env_name, options.juju_command) |
2090 | mock_app.connect.assert_has_calls([ |
2091 | mock.call( |
2092 | 'wss://1.2.3.4:17070/environment/env-uuid/api', |
2093 | @@ -823,8 +835,7 @@ |
2094 | self.configure_app(mock_app, ensure_dependencies=(1, 19, 0)) |
2095 | # Run the application. |
2096 | options = self.make_options() |
2097 | - with self.patch_get_juju_command(): |
2098 | - manage.run(options) |
2099 | + manage.run(options) |
2100 | mock_app.connect.assert_has_calls([ |
2101 | mock.call('wss://1.2.3.4:17070', 'MyUser', 'Secret!'), |
2102 | mock.call().close(), |
2103 | @@ -844,8 +855,7 @@ |
2104 | )) |
2105 | # Run the application. |
2106 | options = self.make_options() |
2107 | - with self.patch_get_juju_command(): |
2108 | - manage.run(options) |
2109 | + manage.run(options) |
2110 | mock_app.connect.assert_has_calls([ |
2111 | mock.call( |
2112 | 'wss://1.2.3.4:17070/environment/env-uuid/api', |
2113 | @@ -863,8 +873,7 @@ |
2114 | mock_app, get_service_config={'port': 8080, 'secure': True}) |
2115 | # Run the application. |
2116 | options = self.make_options() |
2117 | - with self.patch_get_juju_command(): |
2118 | - manage.run(options) |
2119 | + manage.run(options) |
2120 | mock_app.connect.assert_has_calls([ |
2121 | mock.call( |
2122 | 'wss://1.2.3.4:17070/environment/env-uuid/api', |
2123 | @@ -887,8 +896,7 @@ |
2124 | mock_app, get_service_config={'port': 443, 'secure': False}) |
2125 | # Run the application. |
2126 | options = self.make_options() |
2127 | - with self.patch_get_juju_command(): |
2128 | - manage.run(options) |
2129 | + manage.run(options) |
2130 | mock_app.connect.assert_has_calls([ |
2131 | mock.call( |
2132 | 'wss://1.2.3.4:17070/environment/env-uuid/api', |
2133 | @@ -906,16 +914,18 @@ |
2134 | |
2135 | def test_already_bootstrapped(self, mock_app, mock_open): |
2136 | # The application correctly reuses an already bootstrapped environment. |
2137 | - env = self.configure_app(mock_app, check_bootstrapped='example.com') |
2138 | + env = self.configure_app(mock_app) |
2139 | + mock_app.get_api_address.side_effect = ['example.com'] |
2140 | # Run the application. |
2141 | options = self.make_options() |
2142 | - with self.patch_get_juju_command(): |
2143 | - manage.run(options) |
2144 | + manage.run(options) |
2145 | # The environment type is retrieved from the jenv. |
2146 | mock_app.get_env_type.assert_called_once_with(env) |
2147 | - # No reason to call bootstrap or get_api_address functions. |
2148 | + # No reason to call bootstrap. |
2149 | self.assertFalse(mock_app.bootstrap.called) |
2150 | - self.assertFalse(mock_app.get_api_address.called) |
2151 | + # The API address is only retrieved once at the beginning of the |
2152 | + # program execution. |
2153 | + mock_app.get_api_address.assert_called_once_with(self.env_info) |
2154 | |
2155 | def test_already_bootstrapped_race(self, mock_app, mock_open): |
2156 | # The application correctly reuses an already bootstrapped environment. |
2157 | @@ -924,19 +934,16 @@ |
2158 | env = self.configure_app(mock_app, bootstrap=True) |
2159 | # Run the application. |
2160 | options = self.make_options() |
2161 | - with self.patch_get_juju_command(): |
2162 | - manage.run(options) |
2163 | + manage.run(options) |
2164 | # The bootstrap and get_api_address functions are still called, but |
2165 | # this time also get_env_type is required. |
2166 | - # The environment type is retrieved from the jenv. |
2167 | mock_app.bootstrap.assert_called_once_with( |
2168 | - options.env_name, self.juju_command, |
2169 | + options.env_name, options.juju_command, |
2170 | debug=options.debug, |
2171 | upload_tools=options.upload_tools, |
2172 | upload_series=options.upload_series, |
2173 | constraints=options.constraints) |
2174 | - mock_app.get_api_address.assert_called_once_with( |
2175 | - options.env_name, self.juju_command) |
2176 | + self.assertEqual(2, mock_app.get_api_address.call_count) |
2177 | mock_app.get_env_type.assert_called_once_with(env) |
2178 | |
2179 | def test_no_token(self, mock_app, mock_open): |
2180 | @@ -945,8 +952,7 @@ |
2181 | env = self.configure_app(mock_app, create_auth_token=None) |
2182 | # Run the application. |
2183 | options = self.make_options() |
2184 | - with self.patch_get_juju_command(): |
2185 | - manage.run(options) |
2186 | + manage.run(options) |
2187 | # Ensure the browser is still open without an auth token. |
2188 | mock_app.create_auth_token.assert_called_once_with(env) |
2189 | mock_open.assert_called_once_with('https://1.2.3.5') |
2190 | @@ -956,8 +962,7 @@ |
2191 | env = self.configure_app(mock_app) |
2192 | # Run the application. |
2193 | options = self.make_options(gui_source=('juju', 'develop'), port=4242) |
2194 | - with self.patch_get_juju_command(): |
2195 | - manage.run(options) |
2196 | + manage.run(options) |
2197 | expected_config = { |
2198 | 'port': 4242, |
2199 | 'juju-gui-source': u'https://github.com/juju/juju-gui.git develop', |
2200 | @@ -974,8 +979,7 @@ |
2201 | bundle = bundles.Bundle(self.bundle_data, reference=reference) |
2202 | # Run the application. |
2203 | options = self.make_options(bundle_source=bundle_source, bundle=bundle) |
2204 | - with self.patch_get_juju_command(): |
2205 | - manage.run(options) |
2206 | + manage.run(options) |
2207 | # Ensure the bundle is correctly deployed. |
2208 | ref = references.Reference.from_string('cs:trusty/juju-gui-42') |
2209 | mock_app.deploy_bundle.assert_called_once_with(env, bundle, False, ref) |
2210 | @@ -989,8 +993,7 @@ |
2211 | # Run the application. |
2212 | options = self.make_options( |
2213 | bundle_source=bundle_source, bundle=bundle, uncommitted=True) |
2214 | - with self.patch_get_juju_command(): |
2215 | - manage.run(options) |
2216 | + manage.run(options) |
2217 | # Ensure the bundle is correctly deployed. |
2218 | ref = references.Reference.from_string('cs:trusty/juju-gui-42') |
2219 | mock_app.deploy_bundle.assert_called_once_with(env, bundle, True, ref) |
2220 | @@ -1003,15 +1006,13 @@ |
2221 | self.configure_app(mock_app, create_auth_token=None) |
2222 | # Run the application. |
2223 | options = self.make_options(env_type='local') |
2224 | - with self.patch_get_juju_command(): |
2225 | - manage.run(options) |
2226 | + manage.run(options) |
2227 | |
2228 | def test_no_browser(self, mock_app, mock_open): |
2229 | # It is possible to avoid opening the GUI in the browser. |
2230 | self.configure_app(mock_app, create_auth_token=None) |
2231 | # Run the application. |
2232 | options = self.make_options(open_browser=False) |
2233 | - with self.patch_get_juju_command(): |
2234 | - manage.run(options) |
2235 | + manage.run(options) |
2236 | # The browser is not opened. |
2237 | self.assertFalse(mock_open.called) |
2238 | |
2239 | === modified file 'quickstart/tests/test_platform_support.py' |
2240 | --- quickstart/tests/test_platform_support.py 2014-08-25 14:45:50 +0000 |
2241 | +++ quickstart/tests/test_platform_support.py 2015-11-05 10:58:41 +0000 |
2242 | @@ -170,13 +170,13 @@ |
2243 | class TestGetJujuCommand(unittest.TestCase): |
2244 | |
2245 | def test_getenv_succeeds(self): |
2246 | + # The Juju path is taken from the JUJU environment variable. |
2247 | expected_command = '/custom/juju' |
2248 | with mock.patch('os.environ', {'JUJU': expected_command}): |
2249 | - command, customized = platform_support.get_juju_command(None) |
2250 | + command = platform_support.get_juju_command(None) |
2251 | self.assertEqual(expected_command, command) |
2252 | - self.assertTrue(customized) |
2253 | |
2254 | def test_without_env_var(self): |
2255 | - expected = settings.JUJU_CMD_PATHS['default'], False |
2256 | - actual = platform_support.get_juju_command('default') |
2257 | - self.assertEqual(expected, actual) |
2258 | + # The Juju path is declared by quickstart itself. |
2259 | + command = platform_support.get_juju_command('default') |
2260 | + self.assertEqual(settings.JUJU_CMD_PATHS['default'], command) |
2261 | |
2262 | === modified file 'tox.ini' |
2263 | --- tox.ini 2015-08-11 09:57:18 +0000 |
2264 | +++ tox.ini 2015-11-05 10:58:41 +0000 |
2265 | @@ -71,8 +71,8 @@ |
2266 | # Dependencies present in ppa:juju/stable. |
2267 | # See https://launchpad.net/~juju/+archive/ubuntu/stable. |
2268 | websocket-client==0.18.0 |
2269 | - jujuclient==0.50.1 |
2270 | - jujubundlelib==0.1.9 |
2271 | + jujuclient==0.50.3 |
2272 | + jujubundlelib==0.2.1 |
2273 | urwid==1.2.1 |
2274 | # The distribution PyYAML requirement is used in this case. |
2275 | |
2276 | @@ -82,7 +82,7 @@ |
2277 | # Ubuntu 14.04 (trusty) distro dependencies. |
2278 | websocket-client==0.12.0 |
2279 | jujuclient==0.17.5 |
2280 | - jujubundlelib==0.1.9 |
2281 | + jujubundlelib==0.2.0 |
2282 | PyYAML==3.10 |
2283 | urwid==1.1.1 |
2284 | |
2285 | @@ -92,7 +92,7 @@ |
2286 | # Ubuntu 15.04 (vivid) distro dependencies. |
2287 | websocket-client==0.18.0 |
2288 | jujuclient==0.18.5 |
2289 | - jujubundlelib==0.1.9 |
2290 | + jujubundlelib==0.2.0 |
2291 | PyYAML==3.11 |
2292 | urwid==1.2.1 |
2293 | |
2294 | @@ -102,7 +102,7 @@ |
2295 | # Ubuntu 15.10 (wily) distro dependencies. |
2296 | websocket-client==0.18.0 |
2297 | jujuclient==0.50.1 |
2298 | - jujubundlelib==0.1.9 |
2299 | + jujubundlelib==0.2.0 |
2300 | PyYAML==3.11 |
2301 | urwid==1.2.1 |
2302 |
LGTM no QA