Merge lp:~frankban/juju-quickstart/support-cache-yaml into lp:juju-quickstart

Proposed by Francesco Banconi
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
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

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/bin/juju-quickstart`) to set up envs and
deploy bundles both with jes enabled (`export JUJU_DEV_FEATURE_FLAGS=jes`)
and disabled. Check that everything works as usual.

To post a comment you must log in.
Revision history for this message
Martin Hilton (martin-hilton) wrote :

LGTM no QA

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

Thanks for the review Martin!

150. By Francesco Banconi

Fix typo.

151. By Francesco Banconi

Fix functional tests.

Revision history for this message
Madison Scott-Clary (makyo) wrote :

QA okay with one minor comment below, thanks!

review: Approve (code+qa)
152. By Francesco Banconi

Update jujuclient and jujubundlelib dependencies.

Revision history for this message
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

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches