Merge lp:~frankban/juju-quickstart/new-auth-api-endpoint into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 121
Proposed branch: lp:~frankban/juju-quickstart/new-auth-api-endpoint
Merge into: lp:juju-quickstart
Diff against target: 1044 lines (+441/-91)
12 files modified
quickstart/app.py (+36/-19)
quickstart/jujutools.py (+42/-1)
quickstart/manage.py (+19/-7)
quickstart/models/charms.py (+4/-0)
quickstart/models/jenv.py (+17/-0)
quickstart/settings.py (+8/-2)
quickstart/tests/helpers.py (+1/-0)
quickstart/tests/models/test_charms.py (+28/-0)
quickstart/tests/models/test_jenv.py (+24/-0)
quickstart/tests/test_app.py (+79/-44)
quickstart/tests/test_jujutools.py (+118/-0)
quickstart/tests/test_manage.py (+65/-18)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/new-auth-api-endpoint
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+249102@code.launchpad.net

Description of the change

Add support for new Juju WebSocket API endpoints.

Recent Juju versions introduced a new API endpoint
path. In essence, instead of the usual
"wss://<address>:17070", the new
"wss://<address>:17070/environment/<env-uuid>/api"
is used to connect to the API.
This allows for connecting to a specific environment
in a multi-environment state server scenario.

In this branch the new API endpoint is used if a recent
Juju version is in use, and if it is possible to retrieve
the environment UUID from the jenv file.

Also, when connecting to the GUI server (for creating
the auth token or for deploying bundles), use the new
GUI server API endpoints when possible, i.e. when the
charm is recent enough to support redirecting requests
to the new Juju endpoints.
Note that this feature is assumed to land in the next
juju-gui charm release (see settings.py). If that's
not the case, we'll need to increase the charm revisions
in settings.MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT
before releasing the new Quickstart.

Tests: `make check`

QA:
- bootstrap quickstart as usual: `devenv/bin/juju-quickstart`;
- check that, if you are using juju devel (1.22beta), quickstart
  properly connect to the new API endpoint;
- run quickstart again to deploy a bundle, e.g.:
  `devenv/bin/juju-quickstart bundle:mediawiki/single`;
- ensure that the deployment request succeeds;
- if possible, do the above with and older version of Juju,
  to ensure backward compatibility.

Done, thank you!

https://codereview.appspot.com/199490043/

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

Reviewers: mp+249102_code.launchpad.net,

Message:
Please take a look.

Description:
Add support for new Juju WebSocket API endpoints.

Recent Juju versions introduced a new API endpoint
path. In essence, instead of the usual
"wss://<address>:17070", the new
"wss://<address>:17070/environment/<env-uuid>/api"
is used to connect to the API.
This allows for connecting to a specific environment
in a multi-environment state server scenario.

In this branch the new API endpoint is used if a recent
Juju version is in use, and if it is possible to retrieve
the environment UUID from the jenv file.

Also, when connecting to the GUI server (for creating
the auth token or for deploying bundles), use the new
GUI server API endpoints when possible, i.e. when the
charm is recent enough to support redirecting requests
to the new Juju endpoints.
Note that this feature is assumed to land in the next
juju-gui charm release (see settings.py). If that's
not the case, we'll need to increase the charm revisions
in settings.MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT
before releasing the new Quickstart.

Tests: `make check`

QA:
- bootstrap quickstart as usual: `devenv/bin/juju-quickstart`;
- check that, if you are using juju devel (1.22beta), quickstart
   properly connect to the new API endpoint;
- run quickstart again to deploy a bundle, e.g.:
   `devenv/bin/juju-quickstart bundle:mediawiki/single`;
- ensure that the deployment request succeeds;
- if possible, do the above with and older version of Juju,
   to ensure backward compatibility.

Done, thank you!

https://code.launchpad.net/~frankban/juju-quickstart/new-auth-api-endpoint/+merge/249102

(do not edit description out of merge proposal)

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

Affected files (+443, -91 lines):
   A [revision details]
   M quickstart/app.py
   M quickstart/jujutools.py
   M quickstart/manage.py
   M quickstart/models/charms.py
   M quickstart/models/jenv.py
   M quickstart/settings.py
   M quickstart/tests/helpers.py
   M quickstart/tests/models/test_charms.py
   M quickstart/tests/models/test_jenv.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_jujutools.py
   M quickstart/tests/test_manage.py

Revision history for this message
Martin Hilton (martin-hilton) wrote :

LGTM: No QA

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

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py#newcode283
quickstart/app.py:283: def get_env_uuid_or_none(env_name):
Is it really necessary to have or_none in the name of this function?

https://codereview.appspot.com/199490043/

Revision history for this message
Jeff Pihach (hatch) wrote :

LGTM with some possible cleanups.
QA OK!

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

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py#newcode283
quickstart/app.py:283: def get_env_uuid_or_none(env_name):
On 2015/02/10 10:47:32, martin.hilton wrote:
> Is it really necessary to have or_none in the name of this function?

+1

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

https://codereview.appspot.com/199490043/diff/1/quickstart/models/jenv.py#newcode134
quickstart/models/jenv.py:134: data =
serializers.yaml_load_from_path(jenv_path)
I believe there are other places in the code which require information
from the jenv file so I figured that this would already be abstracted
out into a utility method already so you could just fetch the value.

https://codereview.appspot.com/199490043/

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

Thanks for the reviews!

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

https://codereview.appspot.com/199490043/diff/1/quickstart/app.py#newcode283
quickstart/app.py:283: def get_env_uuid_or_none(env_name):
On 2015/02/10 10:47:32, martin.hilton wrote:
> Is it really necessary to have or_none in the name of this function?

Not strictly necessary, but in the caller context this can help: while
most of the times app functions return values, this can also return
none, i.e. do not rely on the fact the env uuid is always known.

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

https://codereview.appspot.com/199490043/diff/1/quickstart/models/jenv.py#newcode134
quickstart/models/jenv.py:134: data =
serializers.yaml_load_from_path(jenv_path)
On 2015/02/10 15:12:09, jeff.pihach wrote:
> I believe there are other places in the code which require information
from the
> jenv file so I figured that this would already be abstracted out into
a utility
> method already so you could just fetch the value.

A slight refactor of the code in the jenv models can help, I agree.
On the other hand, the repeated code is still just two lines, so perhaps
not something for this branch.

https://codereview.appspot.com/199490043/

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

*** Submitted:

Add support for new Juju WebSocket API endpoints.

Recent Juju versions introduced a new API endpoint
path. In essence, instead of the usual
"wss://<address>:17070", the new
"wss://<address>:17070/environment/<env-uuid>/api"
is used to connect to the API.
This allows for connecting to a specific environment
in a multi-environment state server scenario.

In this branch the new API endpoint is used if a recent
Juju version is in use, and if it is possible to retrieve
the environment UUID from the jenv file.

Also, when connecting to the GUI server (for creating
the auth token or for deploying bundles), use the new
GUI server API endpoints when possible, i.e. when the
charm is recent enough to support redirecting requests
to the new Juju endpoints.
Note that this feature is assumed to land in the next
juju-gui charm release (see settings.py). If that's
not the case, we'll need to increase the charm revisions
in settings.MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT
before releasing the new Quickstart.

Tests: `make check`

QA:
- bootstrap quickstart as usual: `devenv/bin/juju-quickstart`;
- check that, if you are using juju devel (1.22beta), quickstart
   properly connect to the new API endpoint;
- run quickstart again to deploy a bundle, e.g.:
   `devenv/bin/juju-quickstart bundle:mediawiki/single`;
- ensure that the deployment request succeeds;
- if possible, do the above with and older version of Juju,
   to ensure backward compatibility.

Done, thank you!

R=martin.hilton, jeff.pihach
CC=
https://codereview.appspot.com/199490043

https://codereview.appspot.com/199490043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/app.py'
2--- quickstart/app.py 2015-02-09 12:34:33 +0000
3+++ quickstart/app.py 2015-02-09 18:28:10 +0000
4@@ -180,9 +180,9 @@
5 def check_bootstrapped(env_name):
6 """Check if the environment named env_name is already bootstrapped.
7
8- If so, return the environment API URL to be used to connect to the Juju API
9- server. If not already bootstrapped, or if the API URL cannot be retrieved,
10- return None.
11+ If so, return the environment API address to be used to connect to the Juju
12+ API server. If not already bootstrapped, or if the API address cannot be
13+ retrieved, return None.
14 """
15 if not jenv.exists(env_name):
16 return None
17@@ -190,20 +190,21 @@
18 try:
19 candidates = jenv.get_value(env_name, 'state-servers')
20 except ValueError as err:
21- logging.warn(b'cannot retrieve the Juju API URL: {}'.format(err))
22+ logging.warn(b'cannot retrieve the Juju API address: {}'.format(err))
23 return None
24- # Look for a reachable API URL.
25+ # Look for a reachable API address.
26 if not candidates:
27- logging.warn('cannot retrieve the Juju API URL: no addresses found')
28+ logging.warn(
29+ 'cannot retrieve the Juju API address: no addresses found')
30 return None
31 for candidate in candidates:
32 error = netutils.check_listening(candidate)
33 if error is None:
34- # Juju API URL found.
35- return 'wss://{}'.format(candidate)
36+ # Juju API address found.
37+ return candidate
38 logging.debug(error)
39 logging.warn(
40- 'cannot retrieve the Juju API URL: cannot connect to any of the '
41+ 'cannot retrieve the Juju API address: cannot connect to any of the '
42 'following addresses: {}'.format(', '.join(candidates)))
43 return None
44
45@@ -279,6 +280,21 @@
46 raise ProgramExit('the state server is not ready:\n{}'.format(details))
47
48
49+def get_env_uuid_or_none(env_name):
50+ """Return the Juju environment unique id for the given environment name.
51+
52+ Parse the jenv file to retrieve the environment UUID.
53+
54+ Return None if the environment UUID is not present in the jenv file.
55+ Raise a ProgramExit if the jenv file is not valid.
56+ """
57+ try:
58+ return jenv.get_env_uuid(env_name)
59+ except ValueError as err:
60+ msg = b'cannot retrieve environment unique identifier: {}'.format(err)
61+ raise ProgramExit(msg)
62+
63+
64 def get_credentials(env_name):
65 """Return the Juju credentials for the given environment name.
66
67@@ -292,11 +308,13 @@
68 raise ProgramExit(msg)
69
70
71-def get_api_url(env_name, juju_command):
72- """Return a Juju API URL for the given environment name.
73+def get_api_address(env_name, juju_command):
74+ """Return a Juju API address for the given environment name.
75+
76+ Only the address is returned, without the schema or the path. For instance:
77+ "api.example.com:17070".
78
79 Use the Juju CLI in a subprocess in order to retrieve the API addresses.
80- Return the complete URL, e.g. "wss://api.example.com:17070".
81 Raise a ProgramExit if any error occurs.
82 """
83 retcode, output, error = utils.call(
84@@ -305,8 +323,7 @@
85 raise ProgramExit(error)
86 # Assuming there is always at least one API address, grab the first one
87 # from the JSON output.
88- api_address = json.loads(output)[0]
89- return 'wss://{}'.format(api_address)
90+ return json.loads(output)[0]
91
92
93 def connect(api_url, username, password):
94@@ -391,7 +408,8 @@
95 default charm URL is used if the charm store service is not available.
96
97 Return a tuple including the following values:
98- - charm_url: the charm URL that will be used to deploy the service;
99+ - charm: the charm that will be used to deploy the service, as an
100+ instance of "quickstart.models.charms.Charm";
101 - machine: the machine where to deploy to (e.g. "0") or None if a new
102 machine must be created;
103 - service_data: the service info as returned by the mega-watcher for
104@@ -442,7 +460,7 @@
105 (charm.series == bootstrap_node_series)
106 ):
107 machine = '0'
108- return charm_url, machine, service_data, unit_data
109+ return charm, machine, service_data, unit_data
110
111
112 def deploy_gui(env, service_name, charm_url, machine, service_data, unit_data):
113@@ -572,9 +590,8 @@
114 def deploy_bundle(env, bundle_yaml, bundle_name, bundle_id):
115 """Deploy a bundle.
116
117- Receive an API URL to a WebSocket server supporting bundle deployments, the
118- admin_secret to use in the authentication process, the bundle YAML encoded
119- contents and the bundle name to be imported.
120+ Receive the environment connection to use for deploying the bundle, the
121+ bundle YAML encoded contents, the bundle name to be imported and its id.
122
123 Raise a ProgramExit if the API server returns an error response.
124 """
125
126=== modified file 'quickstart/jujutools.py'
127--- quickstart/jujutools.py 2015-02-09 12:58:04 +0000
128+++ quickstart/jujutools.py 2015-02-09 18:28:10 +0000
129@@ -30,6 +30,47 @@
130 from quickstart.models import charms
131
132
133+def get_api_url(api_address, juju_version, env_uuid, prefix='', charm=None):
134+ """Return the Juju WebSocket API endpoint.
135+
136+ Receives the Juju API server address, the Juju version and the unique
137+ identifier of the current environment.
138+
139+ Optionally receive a prefix to be used in the path.
140+
141+ Optionally also receive the Juju GUI charm object as an instance of
142+ "quickstart.models.charms.Charm". If provided, the function checks that
143+ the specified Juju GUI charm supports the new Juju API endpoint.
144+ If not supported, the old endpoint is returned.
145+
146+ The environment UUID can be None, in which case the old-style API URL
147+ (not including the environment UUID) is returned.
148+ """
149+ base_url = 'wss://{}'.format(api_address)
150+ prefix = prefix.strip('/')
151+ if prefix:
152+ base_url = '{}/{}'.format(base_url, prefix)
153+ if (env_uuid is None) or (juju_version < (1, 22, 0)):
154+ return base_url
155+ complete_url = '{}/environment/{}/api'.format(base_url, env_uuid)
156+ if charm is None:
157+ return complete_url
158+ # If a customized Juju GUI charm is in use, there is no way to check if the
159+ # GUI server is recent enough to support the new Juju API endpoints.
160+ # In these cases, assume the customized charm is recent enough.
161+ if (
162+ charm.name != settings.JUJU_GUI_CHARM_NAME or
163+ charm.user or
164+ charm.is_local()
165+ ):
166+ return complete_url
167+ # This is the promulgated Juju GUI charm. Check if it supports new APIs.
168+ revision, series = charm.revision, charm.series
169+ if revision < settings.MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT[series]:
170+ return base_url
171+ return complete_url
172+
173+
174 def get_service_info(status, service_name):
175 """Retrieve information on the given service and on its first alive unit.
176
177@@ -62,7 +103,7 @@
178 Print (to stdout or to logs) info and warnings about the charm URL.
179
180 Return the parsed charm object as an instance of
181- quickstart.models.charms.Charm.
182+ "quickstart.models.charms.Charm".
183 """
184 print('charm URL: {}'.format(charm_url))
185 charm = charms.Charm.from_url(charm_url)
186
187=== modified file 'quickstart/manage.py'
188--- quickstart/manage.py 2015-02-09 12:58:04 +0000
189+++ quickstart/manage.py 2015-02-09 18:28:10 +0000
190@@ -32,6 +32,7 @@
191 import quickstart
192 from quickstart import (
193 app,
194+ jujutools,
195 netutils,
196 packaging,
197 platform_support,
198@@ -526,8 +527,8 @@
199 # Bootstrap the Juju environment or reuse an already bootstrapped one.
200 already_bootstrapped = True
201 env_type = options.env_type
202- api_url = app.check_bootstrapped(options.env_name)
203- if api_url is None:
204+ api_address = app.check_bootstrapped(options.env_name)
205+ if api_address is None:
206 print('bootstrapping the {} environment'.format(options.env_name))
207 if env_type == 'local':
208 # If this is a local environment, notify the user that "sudo" will
209@@ -551,9 +552,15 @@
210
211 # If the environment was not already bootstrapped, we need to retrieve
212 # the API address.
213- if api_url is None:
214+ if api_address is None:
215 print('retrieving the Juju API address')
216- api_url = app.get_api_url(options.env_name, juju_command)
217+ api_address = app.get_api_address(options.env_name, juju_command)
218+
219+ # Retrieve the Juju environment unique identifier.
220+ env_uuid = app.get_env_uuid_or_none(options.env_name)
221+
222+ # Build the Juju API endpoint based on the Juju version and environment id.
223+ api_url = jujutools.get_api_url(api_address, juju_version, env_uuid)
224
225 # Retrieve the admin-secret for the current environment.
226 print('retrieving the Juju environment credentials')
227@@ -571,21 +578,26 @@
228 print('environment type: {}'.format(env_type))
229
230 # Inspect the environment and deploy the charm if required.
231- charm_url, machine, service_data, unit_data = app.check_environment(
232+ charm, machine, service_data, unit_data = app.check_environment(
233 env, settings.JUJU_GUI_SERVICE_NAME, options.charm_url,
234 env_type, bootstrap_node_series, already_bootstrapped)
235 unit_name = app.deploy_gui(
236- env, settings.JUJU_GUI_SERVICE_NAME, charm_url, machine,
237+ env, settings.JUJU_GUI_SERVICE_NAME, charm.url(), machine,
238 service_data, unit_data)
239
240 # Observe the deployment progress.
241 address = app.watch(env, unit_name)
242 env.close()
243+
244+ # Print out Juju GUI unit and credential information.
245 url = 'https://{}'.format(address)
246 print('\nJuju GUI URL: {}\nusername: {}\npassword: {}\n'.format(
247 url, username, password))
248- gui_api_url = 'wss://{}:443/ws'.format(address)
249+
250+ # Connect to the GUI server WebSocket API.
251 print('connecting to the Juju GUI server')
252+ gui_api_url = jujutools.get_api_url(
253+ address + ':443', juju_version, env_uuid, prefix='ws', charm=charm)
254 gui_env = app.connect(gui_api_url, username, password)
255
256 # Handle bundle deployment.
257
258=== modified file 'quickstart/models/charms.py'
259--- quickstart/models/charms.py 2013-12-06 17:17:19 +0000
260+++ quickstart/models/charms.py 2015-02-09 18:28:10 +0000
261@@ -118,6 +118,10 @@
262 def __repr__(self):
263 return b'<Charm: {}>'.format(bytes(self))
264
265+ def __eq__(self, other):
266+ """Two charms are equal if they have the same URL."""
267+ return isinstance(other, self.__class__) and self.url() == other.url()
268+
269 def url(self):
270 """Return the charm URL."""
271 user_part = '~{}/'.format(self.user) if self.user else ''
272
273=== modified file 'quickstart/models/jenv.py'
274--- quickstart/models/jenv.py 2015-01-12 12:10:38 +0000
275+++ quickstart/models/jenv.py 2015-02-09 18:28:10 +0000
276@@ -122,6 +122,23 @@
277 return username, password
278
279
280+def get_env_uuid(env_name):
281+ """Return the Juju environment unique identifier.
282+
283+ Return None if the environment UUID is not included in the jenv file.
284+ Raise a ValueError if:
285+ - the environment file is not found;
286+ - the environment file contents are not parsable by YAML.
287+ """
288+ jenv_path = _get_jenv_path(env_name)
289+ data = serializers.yaml_load_from_path(jenv_path)
290+ try:
291+ return _get_value_from_yaml(data, 'environ-uuid')
292+ except ValueError:
293+ # This is probably an old version of Juju.
294+ return None
295+
296+
297 def get_env_db():
298 """Return an environment database parsing the existing jenv files.
299
300
301=== modified file 'quickstart/settings.py'
302--- quickstart/settings.py 2015-01-12 15:07:51 +0000
303+++ quickstart/settings.py 2015-02-09 18:28:10 +0000
304@@ -40,8 +40,8 @@
305 # temporary connection/charm store errors.
306 # Keep this list sorted by release date (older first).
307 DEFAULT_CHARM_URLS = collections.OrderedDict((
308- ('precise', 'cs:precise/juju-gui-104'),
309- ('trusty', 'cs:trusty/juju-gui-16'),
310+ ('precise', 'cs:precise/juju-gui-106'),
311+ ('trusty', 'cs:trusty/juju-gui-18'),
312 ))
313
314 # The quickstart app short description.
315@@ -88,3 +88,9 @@
316 # supported series. Assume not listed series to always support bundles.
317 MINIMUM_REVISIONS_FOR_BUNDLES = collections.defaultdict(
318 lambda: 0, {'precise': 80})
319+
320+# The minimum Juju GUI charm revision supporting the new Juju API endpoints
321+# including the environment UUID. Assume not listed series to always support
322+# new endpoints.
323+MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT = collections.defaultdict(
324+ lambda: 0, {'precise': 107, 'trusty': 19})
325
326=== modified file 'quickstart/tests/helpers.py'
327--- quickstart/tests/helpers.py 2014-12-17 11:47:43 +0000
328+++ quickstart/tests/helpers.py 2015-02-09 18:28:10 +0000
329@@ -143,6 +143,7 @@
330 jenv_data = {
331 'user': 'admin',
332 'password': 'Secret!',
333+ 'environ-uuid': '__unique_identifier__',
334 'state-servers': ['localhost:17070', '10.0.3.1:17070'],
335 'bootstrap-config': {
336 'admin-secret': 'Secret!',
337
338=== modified file 'quickstart/tests/models/test_charms.py'
339--- quickstart/tests/models/test_charms.py 2013-12-06 17:17:19 +0000
340+++ quickstart/tests/models/test_charms.py 2015-02-09 18:28:10 +0000
341@@ -205,3 +205,31 @@
342 # The is_local method returns True for local charms.
343 charm = self.make_charm(schema='local')
344 self.assertTrue(charm.is_local())
345+
346+ def test_equality(self):
347+ # Two charms are equal if they have the same URL.
348+ self.assertEqual(self.make_charm(), self.make_charm())
349+
350+ def test_equality_different_name(self):
351+ # Two charms with different names are not equal.
352+ self.assertNotEqual(
353+ self.make_charm(name='django'),
354+ self.make_charm(name='rails'))
355+
356+ def test_equality_different_revision(self):
357+ # Two charms with different revisions are not equal.
358+ self.assertNotEqual(
359+ self.make_charm(revision=0),
360+ self.make_charm(revision=1))
361+
362+ def test_equality_different_user(self):
363+ # Two charms with different users are not equal.
364+ self.assertNotEqual(
365+ self.make_charm(user=''),
366+ self.make_charm(user='who'))
367+
368+ def test_equality_different_types(self):
369+ # A charm never equals a non-charm object.
370+ self.assertNotEqual(self.make_charm(), 42)
371+ self.assertNotEqual(self.make_charm(), True)
372+ self.assertNotEqual(self.make_charm(), 'oranges')
373
374=== modified file 'quickstart/tests/models/test_jenv.py'
375--- quickstart/tests/models/test_jenv.py 2015-01-13 11:46:06 +0000
376+++ quickstart/tests/models/test_jenv.py 2015-02-09 18:28:10 +0000
377@@ -168,6 +168,30 @@
378 jenv.get_credentials('local')
379
380
381+class TestGetEnvUuid(helpers.JenvFileTestsMixin, unittest.TestCase):
382+
383+ def test_uuid_found(self):
384+ # The environment UUID is correctly returned when included in the jenv.
385+ with self.make_jenv('local', yaml.safe_dump(self.jenv_data)):
386+ env_uuid = jenv.get_env_uuid('local')
387+ self.assertEqual('__unique_identifier__', env_uuid)
388+
389+ def test_uuid_not_found(self):
390+ # None is returned if the environment UUID is not present in the jenv.
391+ data = {'user': 'jean-luc', 'password': 'Secret!'}
392+ with self.make_jenv('local', yaml.safe_dump(data)):
393+ env_uuid = jenv.get_env_uuid('local')
394+ self.assertIsNone(env_uuid)
395+
396+ def test_invalid_jenv(self):
397+ # A ValueError is raised if there are errors parsing the jenv file.
398+ expected_error = 'unable to parse file'
399+ with self.make_jenv('ec2', ':'):
400+ with self.assertRaises(ValueError) as context_manager:
401+ jenv.get_env_uuid('ec2')
402+ self.assertIn(expected_error, bytes(context_manager.exception))
403+
404+
405 class TestGetEnvDb(helpers.JenvFileTestsMixin, unittest.TestCase):
406
407 def test_no_juju_home(self):
408
409=== modified file 'quickstart/tests/test_app.py'
410--- quickstart/tests/test_app.py 2015-01-12 15:00:52 +0000
411+++ quickstart/tests/test_app.py 2015-02-09 18:28:10 +0000
412@@ -20,6 +20,7 @@
413
414 from contextlib import contextmanager
415 import json
416+import os
417 import unittest
418
419 import jujuclient
420@@ -31,6 +32,7 @@
421 platform_support,
422 settings,
423 )
424+from quickstart.models import charms
425 from quickstart.tests import helpers
426
427
428@@ -457,56 +459,56 @@
429 class TestCheckBootstrapped(helpers.JenvFileTestsMixin, unittest.TestCase):
430
431 def test_no_jenv_file(self):
432- # A None API URL is returned if the jenv file is not present.
433+ # A None API address is returned if the jenv file is not present.
434 with self.make_jenv('ec2', ''):
435 with helpers.assert_logs([], level='warn'):
436- api_url = app.check_bootstrapped('hp')
437- self.assertIsNone(api_url)
438+ api_address = app.check_bootstrapped('hp')
439+ self.assertIsNone(api_address)
440
441 def test_invalid_jenv_file(self):
442- # A None API URL is returned if the list of API addresses cannot be
443+ # A None API address is returned if the list of API addresses cannot be
444 # retrieved from the jenv file.
445 with self.make_jenv('ec2', '') as path:
446 logs = [
447- 'cannot retrieve the Juju API URL: '
448+ 'cannot retrieve the Juju API address: '
449 'cannot read {}: invalid YAML contents: '
450 'state-servers key not found in the root section'.format(path)
451 ]
452 with helpers.assert_logs(logs, level='warn'):
453- api_url = app.check_bootstrapped('ec2')
454- self.assertIsNone(api_url)
455+ api_address = app.check_bootstrapped('ec2')
456+ self.assertIsNone(api_address)
457
458 def test_no_api_addresses(self):
459- # A None API URL is returned if the list of API addresses is empty.
460+ # A None API address is returned if the list of API addresses is empty.
461 jenv_data = {'state-servers': []}
462- logs = ['cannot retrieve the Juju API URL: no addresses found']
463+ logs = ['cannot retrieve the Juju API address: no addresses found']
464 with self.make_jenv('local', yaml.safe_dump(jenv_data)):
465 with helpers.assert_logs(logs, level='warn'):
466- api_url = app.check_bootstrapped('local')
467- self.assertIsNone(api_url)
468+ api_address = app.check_bootstrapped('local')
469+ self.assertIsNone(api_address)
470
471 def test_api_address_not_listening(self):
472- # A None API URL is returned if there is no reachable API address.
473+ # A None API address is returned if there is no reachable API address.
474 logs = [
475- 'cannot retrieve the Juju API URL: '
476+ 'cannot retrieve the Juju API address: '
477 'cannot connect to any of the following addresses: '
478 'localhost:17070, 10.0.3.1:17070'
479 ]
480 with self.make_jenv('local', yaml.safe_dump(self.jenv_data)):
481 with helpers.assert_logs(logs, level='warn'):
482 with helpers.patch_socket_create_connection('bad wolf'):
483- api_url = app.check_bootstrapped('local')
484- self.assertIsNone(api_url)
485+ api_address = app.check_bootstrapped('local')
486+ self.assertIsNone(api_address)
487
488 def test_bootstrapped(self):
489- # The first listening API URL is returned if the environment is already
490- # bootstrapped.
491+ # The first listening API address is returned if the environment is
492+ # already bootstrapped.
493 with self.make_jenv('hp', yaml.safe_dump(self.jenv_data)):
494 with helpers.assert_logs([], level='warn'):
495 with helpers.patch_socket_create_connection():
496- api_url = app.check_bootstrapped('hp')
497+ api_address = app.check_bootstrapped('hp')
498 # The first API address is returned.
499- self.assertEqual('wss://localhost:17070', api_url)
500+ self.assertEqual('localhost:17070', api_address)
501
502
503 class TestBootstrap(
504@@ -700,6 +702,34 @@
505 mock_call.assert_has_calls(expected_calls)
506
507
508+class TestGetEnvUuidOrNone(
509+ helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
510+
511+ def test_success(self):
512+ # The environment UUID is successfully retrieved.
513+ with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)):
514+ env_uuid = app.get_env_uuid_or_none('ec2')
515+ self.assertEqual('__unique_identifier__', env_uuid)
516+
517+ def test_no_uuid(self):
518+ # None is returned if the environment UUID is not found.
519+ data = {'user': 'jean-luc', 'password': 'Secret!'}
520+ with self.make_jenv('ec2', yaml.safe_dump(data)):
521+ env_uuid = app.get_env_uuid_or_none('ec2')
522+ self.assertIsNone(env_uuid)
523+
524+ def test_error(self):
525+ # A ProgramExit is raised if the environment UUID cannot be retrieved.
526+ with self.make_jenv('ec2', '') as path:
527+ os.remove(path)
528+ expected_error = (
529+ 'cannot retrieve environment unique identifier: unable to '
530+ "open file {}: [Errno 2] No such file or directory: '{}'"
531+ ''.format(path, path))
532+ with self.assert_program_exit(expected_error):
533+ app.get_env_uuid_or_none('ec2')
534+
535+
536 class TestGetCredentials(
537 helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
538
539@@ -722,27 +752,27 @@
540 app.get_credentials('ec2')
541
542
543-class TestGetApiUrl(
544+class TestGetApiAddress(
545 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
546
547 env_name = 'ec2'
548 juju_command = settings.JUJU_CMD_PATHS['default']
549
550 def test_success(self):
551- # The API URL is correctly returned.
552+ # The API address is correctly returned.
553 api_addresses = json.dumps(['api.example.com:17070', 'not-today'])
554 with self.patch_call(retcode=0, output=api_addresses) as mock_call:
555- api_url = app.get_api_url(self.env_name, self.juju_command)
556- self.assertEqual('wss://api.example.com:17070', api_url)
557+ api_address = app.get_api_address(self.env_name, self.juju_command)
558+ self.assertEqual('api.example.com:17070', api_address)
559 mock_call.assert_called_once_with(
560 self.juju_command, 'api-endpoints', '-e', self.env_name,
561 '--format', 'json')
562
563 def test_failure(self):
564- # A ProgramExit is raised if an error occurs retrieving the API URL.
565+ # A ProgramExit is raised if an error occurs retrieving the address.
566 with self.patch_call(retcode=1, error='bad wolf') as mock_call:
567 with self.assert_program_exit('bad wolf'):
568- app.get_api_url(self.env_name, self.juju_command)
569+ app.get_api_address(self.env_name, self.juju_command)
570 mock_call.assert_called_once_with(
571 self.juju_command, 'api-endpoints', '-e', self.env_name,
572 '--format', 'json')
573@@ -904,6 +934,11 @@
574 return mock.patch(
575 'quickstart.netutils.get_charm_url', mock_get_charm_url)
576
577+ def assert_charm_equal(self, expected_url, charm):
578+ """Ensure the given charm has the expected URL."""
579+ expected_charm = charms.Charm.from_url(expected_url)
580+ self.assertEqual(expected_charm, charm)
581+
582 def test_environment_just_bootstrapped(self, mock_print):
583 # The function correctly retrieves the charm URL and machine, and
584 # handles the case when the charm URL is not provided by the user.
585@@ -917,14 +952,14 @@
586 check_preexisting = False
587 with self.patch_get_charm_url(
588 return_value='cs:trusty/juju-gui-42') as mock_get_charm_url:
589- url, machine, service_data, unit_data = app.check_environment(
590+ charm, machine, service_data, unit_data = app.check_environment(
591 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
592 check_preexisting)
593 # There is no need to call status if the environment was just created.
594 self.assertFalse(env.get_status.called)
595 # The charm URL has been retrieved from the charm store API based on
596 # the current bootstrap node series.
597- self.assertEqual('cs:trusty/juju-gui-42', url)
598+ self.assert_charm_equal('cs:trusty/juju-gui-42', charm)
599 mock_get_charm_url.assert_called_once_with(bootstrap_node_series)
600 # Since the bootstrap node series is supported by the GUI charm, the
601 # GUI unit can be deployed to machine 0.
602@@ -952,14 +987,14 @@
603 check_preexisting = True
604 with self.patch_get_charm_url(
605 return_value='cs:precise/juju-gui-42') as mock_get_charm_url:
606- url, machine, service_data, unit_data = app.check_environment(
607+ charm, machine, service_data, unit_data = app.check_environment(
608 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
609 check_preexisting)
610 # The environment status has been retrieved.
611 env.get_status.assert_called_once_with()
612 # The charm URL has been retrieved from the charm store API based on
613 # the current bootstrap node series.
614- self.assertEqual('cs:precise/juju-gui-42', url)
615+ self.assert_charm_equal('cs:precise/juju-gui-42', charm)
616 mock_get_charm_url.assert_called_once_with(bootstrap_node_series)
617 # Since the bootstrap node series is supported by the GUI charm, the
618 # GUI unit can be deployed to machine 0.
619@@ -984,13 +1019,13 @@
620 bootstrap_node_series = 'precise'
621 check_preexisting = True
622 with self.patch_get_charm_url() as mock_get_charm_url:
623- url, machine, service_data, unit_data = app.check_environment(
624+ charm, machine, service_data, unit_data = app.check_environment(
625 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
626 check_preexisting)
627 # The environment status has been retrieved.
628 env.get_status.assert_called_once_with()
629 # The charm URL has been retrieved from the environment.
630- self.assertEqual('cs:precise/juju-gui-47', url)
631+ self.assert_charm_equal('cs:precise/juju-gui-47', charm)
632 self.assertFalse(mock_get_charm_url.called)
633 # Since the bootstrap node series is supported by the GUI charm, the
634 # GUI unit can be safely deployed to machine 0.
635@@ -1009,12 +1044,12 @@
636 check_preexisting = False
637 with self.patch_get_charm_url(
638 return_value='cs:trusty/juju-gui-42') as mock_get_charm_url:
639- url, machine, service_data, unit_data = app.check_environment(
640+ charm, machine, service_data, unit_data = app.check_environment(
641 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
642 check_preexisting)
643 # The charm URL has been retrieved from the charm store API using the
644 # most recent supported series.
645- self.assertEqual('cs:trusty/juju-gui-42', url)
646+ self.assert_charm_equal('cs:trusty/juju-gui-42', charm)
647 mock_get_charm_url.assert_called_once_with('trusty')
648 # The Juju GUI unit cannot be deployed to saucy machine 0.
649 self.assertIsNone(machine)
650@@ -1034,11 +1069,11 @@
651 bootstrap_node_series = 'trusty'
652 check_preexisting = False
653 with self.patch_get_charm_url(return_value='cs:trusty/juju-gui-42'):
654- url, machine, service_data, unit_data = app.check_environment(
655+ charm, machine, service_data, unit_data = app.check_environment(
656 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
657 check_preexisting)
658 # The charm URL has been correctly retrieved from the charm store API.
659- self.assertEqual('cs:trusty/juju-gui-42', url)
660+ self.assert_charm_equal('cs:trusty/juju-gui-42', charm)
661 # The Juju GUI unit cannot be deployed to localhost.
662 self.assertIsNone(machine)
663
664@@ -1051,7 +1086,7 @@
665 bootstrap_node_series = 'trusty'
666 check_preexisting = False
667 with self.patch_get_charm_url(return_value='cs:trusty/juju-gui-42'):
668- url, machine, service_data, unit_data = app.check_environment(
669+ _, machine, _, _ = app.check_environment(
670 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
671 check_preexisting)
672 self.assertIsNone(machine)
673@@ -1065,11 +1100,11 @@
674 bootstrap_node_series = 'precise'
675 check_preexisting = False
676 with self.patch_get_charm_url(side_effect=IOError('boo!')):
677- url, machine, service_data, unit_data = app.check_environment(
678+ charm, machine, service_data, unit_data = app.check_environment(
679 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
680 check_preexisting)
681 # The default charm URL for the given series is returned.
682- self.assertEqual(settings.DEFAULT_CHARM_URLS['precise'], url)
683+ self.assert_charm_equal(settings.DEFAULT_CHARM_URLS['precise'], charm)
684 self.assertEqual('0', machine)
685
686 def test_most_recent_default_charm_url(self, mock_print):
687@@ -1082,12 +1117,12 @@
688 bootstrap_node_series = 'saucy'
689 check_preexisting = False
690 with self.patch_get_charm_url(side_effect=IOError('boo!')):
691- url, machine, service_data, unit_data = app.check_environment(
692+ charm, machine, service_data, unit_data = app.check_environment(
693 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
694 check_preexisting)
695 # The default charm URL for the given series is returned.
696 series = settings.JUJU_GUI_SUPPORTED_SERIES[-1]
697- self.assertEqual(settings.DEFAULT_CHARM_URLS[series], url)
698+ self.assert_charm_equal(settings.DEFAULT_CHARM_URLS[series], charm)
699 self.assertIsNone(machine)
700
701 def test_charm_url_provided(self, mock_print):
702@@ -1099,14 +1134,14 @@
703 bootstrap_node_series = 'trusty'
704 check_preexisting = False
705 with self.patch_get_charm_url() as mock_get_charm_url:
706- url, machine, service_data, unit_data = app.check_environment(
707+ charm, machine, service_data, unit_data = app.check_environment(
708 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
709 check_preexisting)
710 # There is no need to call the charmword API if the charm URL is
711 # provided by the user.
712 self.assertFalse(mock_get_charm_url.called)
713 # The provided charm URL has been correctly returned.
714- self.assertEqual(charm_url, url)
715+ self.assert_charm_equal(charm_url, charm)
716 # Since the provided charm series is trusty, the charm itself can be
717 # safely deployed to machine 0.
718 self.assertEqual('0', machine)
719@@ -1126,14 +1161,14 @@
720 bootstrap_node_series = 'precise'
721 check_preexisting = False
722 with self.patch_get_charm_url() as mock_get_charm_url:
723- url, machine, service_data, unit_data = app.check_environment(
724+ charm, machine, service_data, unit_data = app.check_environment(
725 env, 'my-gui', charm_url, env_type, bootstrap_node_series,
726 check_preexisting)
727 # There is no need to call the charmword API if the charm URL is
728 # provided by the user.
729 self.assertFalse(mock_get_charm_url.called)
730 # The provided charm URL has been correctly returned.
731- self.assertEqual(charm_url, url)
732+ self.assert_charm_equal(charm_url, charm)
733 # Since the provided charm series is not precise, the charm must be
734 # deployed to a new machine.
735 self.assertIsNone(machine)
736
737=== modified file 'quickstart/tests/test_jujutools.py'
738--- quickstart/tests/test_jujutools.py 2015-02-09 12:58:04 +0000
739+++ quickstart/tests/test_jujutools.py 2015-02-09 18:28:10 +0000
740@@ -28,6 +28,124 @@
741 from quickstart.tests import helpers
742
743
744+class TestGetApiUrl(unittest.TestCase):
745+
746+ def test_new_url(self):
747+ # The new Juju API endpoint is returned if a recent Juju is used.
748+ url = jujutools.get_api_url('1.2.3.4:17070', (1, 22, 0), 'env-uuid')
749+ self.assertEqual('wss://1.2.3.4:17070/environment/env-uuid/api', url)
750+
751+ def test_new_url_with_prefix(self):
752+ # The new Juju API endpoint is returned with the given path prefix.
753+ url = jujutools.get_api_url(
754+ '1.2.3.4:17070', (1, 22, 0), 'env-uuid', prefix='/my/path/')
755+ self.assertEqual(
756+ 'wss://1.2.3.4:17070/my/path/environment/env-uuid/api', url)
757+
758+ def test_old_juju(self):
759+ # The old Juju API endpoint is returned if the Juju in use is not a
760+ # recent version.
761+ url = jujutools.get_api_url('1.2.3.4:17070', (1, 21, 7), 'env-uuid')
762+ self.assertEqual('wss://1.2.3.4:17070', url)
763+
764+ def test_old_juju_with_prefix(self):
765+ # The old Juju API endpoint is returned with the given path prefix.
766+ url = jujutools.get_api_url(
767+ '1.2.3.4:8888', (1, 21, 7), 'env-uuid', 'proxy/')
768+ self.assertEqual('wss://1.2.3.4:8888/proxy', url)
769+
770+ def test_no_env_uuid(self):
771+ # The old Juju API endpoint is returned if the environment unique
772+ # identifier is unreachable.
773+ url = jujutools.get_api_url('1.2.3.4:17070', (1, 23, 42), None)
774+ self.assertEqual('wss://1.2.3.4:17070', url)
775+
776+ def test_no_env_uuid_with_prefix(self):
777+ # The old Juju API endpoint is returned with the given path prefix.
778+ url = jujutools.get_api_url(
779+ '1.2.3.4:17070', (1, 23, 42), None, 'my/prefix')
780+ self.assertEqual('wss://1.2.3.4:17070/my/prefix', url)
781+
782+ def test_new_charm_old_juju(self):
783+ # The old Juju API endpoints are used if and old version of Juju is in
784+ # use, even if the Juju GUI charm is recent.
785+ charm = charms.Charm.from_url('cs:trusty/juju-gui-42')
786+ url = jujutools.get_api_url(
787+ '1.2.3.4:5678', (1, 21, 7), 'env-uuid', charm=charm)
788+ self.assertEqual('wss://1.2.3.4:5678', url)
789+
790+ def test_customized_charm_unexpected_name(self):
791+ # If a customized Juju GUI charm is used, then we assume it supports
792+ # the new Juju Login API endpoint (unexpected charm name).
793+ charm = charms.Charm.from_url('cs:trusty/the-amazing-gui-0')
794+ url = jujutools.get_api_url(
795+ 'example.com:17070', (1, 22, 2), 'uuid', charm=charm)
796+ self.assertEqual('wss://example.com:17070/environment/uuid/api', url)
797+
798+ def test_customized_charm_unexpected_user(self):
799+ # If a customized Juju GUI charm is used, then we assume it supports
800+ # the new Juju Login API endpoint (unexpected charm user).
801+ charm = charms.Charm.from_url('cs:~who/trusty/juju-gui-0')
802+ url = jujutools.get_api_url(
803+ 'example.com:17070', (1, 22, 2), 'uuid', charm=charm)
804+ self.assertEqual('wss://example.com:17070/environment/uuid/api', url)
805+
806+ def test_customized_charm_unexpected_schema(self):
807+ # If a customized Juju GUI charm is used, then we assume it supports
808+ # the new Juju Login API endpoint (local charm).
809+ charm = charms.Charm.from_url('local:precise/juju-gui-0')
810+ url = jujutools.get_api_url(
811+ 'example.com:17070', (1, 22, 2), 'uuid', prefix='/', charm=charm)
812+ self.assertEqual('wss://example.com:17070/environment/uuid/api', url)
813+
814+ def test_customized_charm_unexpected_series(self):
815+ # If a customized Juju GUI charm is used, then we assume it supports
816+ # the new Juju Login API endpoint (unsupported charm series).
817+ charm = charms.Charm.from_url('cs:vivid/juju-gui-0')
818+ url = jujutools.get_api_url(
819+ 'example.com:22', (1, 22, 2), 'uuid', prefix='ws', charm=charm)
820+ self.assertEqual('wss://example.com:22/ws/environment/uuid/api', url)
821+
822+ def test_recent_precise_charm(self):
823+ # The new API endpoints are used if a recent precise charm is in use.
824+ charm = charms.Charm.from_url('cs:precise/juju-gui-107')
825+ url = jujutools.get_api_url(
826+ '1.2.3.4:4747', (1, 42, 0), 'env-id', charm=charm)
827+ self.assertEqual('wss://1.2.3.4:4747/environment/env-id/api', url)
828+
829+ def test_recent_trusty_charm(self):
830+ # The new API endpoints are used if a recent trusty charm is in use.
831+ charm = charms.Charm.from_url('cs:trusty/juju-gui-19')
832+ url = jujutools.get_api_url(
833+ '1.2.3.4:4747', (1, 42, 0), 'env-id', charm=charm)
834+ self.assertEqual('wss://1.2.3.4:4747/environment/env-id/api', url)
835+
836+ def test_old_precise_charm(self):
837+ # The old API endpoint is returned if the precise Juju GUI charm in use
838+ # is outdated.
839+ charm = charms.Charm.from_url('cs:precise/juju-gui-106')
840+ url = jujutools.get_api_url(
841+ '1.2.3.4:4747', (1, 42, 0), 'env-uuid', charm=charm)
842+ self.assertEqual('wss://1.2.3.4:4747', url)
843+
844+ def test_old_trusty_charm(self):
845+ # The old API endpoint is returned if the trusty Juju GUI charm in use
846+ # is outdated.
847+ charm = charms.Charm.from_url('cs:trusty/juju-gui-18')
848+ url = jujutools.get_api_url(
849+ '1.2.3.4:4747', (1, 42, 0), 'env-uuid', prefix='ws', charm=charm)
850+ self.assertEqual('wss://1.2.3.4:4747/ws', url)
851+
852+ def test_recent_charm_and_prefix(self):
853+ # The new API endpoint is returned if a recent charm and a prefix are
854+ # both provided. This test exercises the real case in which the GUI
855+ # server API endpoint is returned.
856+ charm = charms.Charm.from_url('cs:trusty/juju-gui-42')
857+ url = jujutools.get_api_url(
858+ '1.2.3.4:17070', (1, 22, 0), 'env-id', prefix='ws', charm=charm)
859+ self.assertEqual('wss://1.2.3.4:17070/ws/environment/env-id/api', url)
860+
861+
862 class TestGetServiceInfo(helpers.WatcherDataTestsMixin, unittest.TestCase):
863
864 def test_service_and_unit(self):
865
866=== modified file 'quickstart/tests/test_manage.py'
867--- quickstart/tests/test_manage.py 2015-01-12 12:10:38 +0000
868+++ quickstart/tests/test_manage.py 2015-02-09 18:28:10 +0000
869@@ -40,6 +40,7 @@
870 views,
871 )
872 from quickstart.models import (
873+ charms,
874 envs,
875 jenv,
876 )
877@@ -808,7 +809,7 @@
878 env = mock.Mock()
879 defaults = {
880 # Dependencies are installed.
881- 'ensure_dependencies': (1, 18, 0),
882+ 'ensure_dependencies': (1, 22, 0),
883 # Ensure the current Juju version is supported.
884 'check_juju_supported': None,
885 # Ensure the SSH keys are properly configured.
886@@ -819,15 +820,17 @@
887 'bootstrap': False,
888 # Status is then called, returning the bootstrap node series.
889 'status': 'trusty',
890- # The API URL must be retrieved (the environment was not ready).
891- 'get_api_url': 'wss://1.2.3.4:17070',
892+ # The API address must be retrieved (the environment is not ready).
893+ 'get_api_address': '1.2.3.4:17070',
894+ # Retrieve the environment unique identifier.
895+ 'get_env_uuid_or_none': 'env-uuid',
896 # Retrieve the environment credentials.
897 'get_credentials': ('MyUser', 'Secret!'),
898 # Connect to the Juju Environment API endpoint.
899 'connect': env,
900 # The environment is then checked.
901 'check_environment': (
902- 'cs:trusty/juju-gui-42',
903+ charms.Charm.from_url('cs:trusty/juju-gui-42'),
904 '0',
905 {'Name': 'juju-gui'},
906 {'Name': 'juju-gui/0'}
907@@ -835,7 +838,7 @@
908 # Deploy the Juju GUI charm.
909 'deploy_gui': 'juju-gui/0',
910 # Watch the deployment progress and return the unit address.
911- 'watch': '1.2.3.4',
912+ 'watch': '1.2.3.5',
913 # Create the login token for the Juju GUI.
914 'create_auth_token': 'TOKEN',
915 }
916@@ -859,7 +862,7 @@
917 # Ensure the functions have been used correctly.
918 mock_app.ensure_dependencies.assert_called_once_with(
919 options.distro_only, options.platform, self.juju_command)
920- mock_app.check_juju_supported.assert_called_once_with((1, 18, 0))
921+ mock_app.check_juju_supported.assert_called_once_with((1, 22, 0))
922 mock_app.ensure_ssh_keys.assert_called_once_with()
923 mock_app.check_bootstrapped.assert_called_once_with(options.env_name)
924 mock_app.bootstrap.assert_called_once_with(
925@@ -870,13 +873,20 @@
926 constraints=options.constraints)
927 mock_app.status.assert_called_once_with(
928 options.env_name, self.juju_command)
929- mock_app.get_api_url.assert_called_once_with(
930+ mock_app.get_api_address.assert_called_once_with(
931 options.env_name, self.juju_command)
932+ mock_app.get_env_uuid_or_none.assert_called_once_with(options.env_name)
933 mock_app.get_credentials.assert_called_once_with(options.env_name)
934 mock_app.connect.assert_has_calls([
935- mock.call('wss://1.2.3.4:17070', 'MyUser', 'Secret!'),
936+ mock.call(
937+ 'wss://1.2.3.4:17070/environment/env-uuid/api',
938+ 'MyUser',
939+ 'Secret!'),
940 mock.call().close(),
941- mock.call('wss://1.2.3.4:443/ws', 'MyUser', 'Secret!'),
942+ mock.call(
943+ 'wss://1.2.3.5:443/ws/environment/env-uuid/api',
944+ 'MyUser',
945+ 'Secret!'),
946 mock.call().close(),
947 ])
948 mock_app.check_environment.assert_called_once_with(
949@@ -887,24 +897,61 @@
950 '0', {'Name': 'juju-gui'}, {'Name': 'juju-gui/0'})
951 mock_app.watch.assert_called_once_with(env, 'juju-gui/0')
952 mock_app.create_auth_token.assert_called_once_with(env)
953- mock_open.assert_called_once_with('https://1.2.3.4/?authtoken=TOKEN')
954+ mock_open.assert_called_once_with('https://1.2.3.5/?authtoken=TOKEN')
955 # Ensure some of the app function have not been called.
956 self.assertFalse(mock_app.get_env_type.called)
957 self.assertFalse(mock_app.deploy_bundle.called)
958
959+ def test_old_juju_api_endpoint(self, mock_app, mock_open):
960+ # The old Juju WebSocket API endpoint is used if the Juju version is
961+ # not recent enough.
962+ self.configure_app(mock_app, ensure_dependencies=(1, 19, 0))
963+ # Run the application.
964+ options = self.make_options()
965+ with self.patch_get_juju_command():
966+ manage.run(options)
967+ mock_app.connect.assert_has_calls([
968+ mock.call('wss://1.2.3.4:17070', 'MyUser', 'Secret!'),
969+ mock.call().close(),
970+ mock.call('wss://1.2.3.5:443/ws', 'MyUser', 'Secret!'),
971+ mock.call().close(),
972+ ])
973+
974+ def test_new_api_endpoint_old_charm(self, mock_app, mock_open):
975+ # Even if the Juju version is new, the old GUI server login API is used
976+ # if the charm in the environment is not recent enough.
977+ self.configure_app(mock_app, check_environment=(
978+ charms.Charm.from_url('cs:trusty/juju-gui-0'),
979+ '0',
980+ {'Name': 'juju-gui'},
981+ {'Name': 'juju-gui/0'}
982+ ))
983+ # Run the application.
984+ options = self.make_options()
985+ with self.patch_get_juju_command():
986+ manage.run(options)
987+ mock_app.connect.assert_has_calls([
988+ mock.call(
989+ 'wss://1.2.3.4:17070/environment/env-uuid/api',
990+ 'MyUser',
991+ 'Secret!'),
992+ mock.call().close(),
993+ mock.call('wss://1.2.3.5:443/ws', 'MyUser', 'Secret!'),
994+ mock.call().close(),
995+ ])
996+
997 def test_already_bootstrapped(self, mock_app, mock_open):
998 # The application correctly reuses an already bootstrapped environment.
999- env = self.configure_app(
1000- mock_app, check_bootstrapped='wss://example.com')
1001+ env = self.configure_app(mock_app, check_bootstrapped='example.com')
1002 # Run the application.
1003 options = self.make_options()
1004 with self.patch_get_juju_command():
1005 manage.run(options)
1006 # The environment type is retrieved from the jenv.
1007 mock_app.get_env_type.assert_called_once_with(env)
1008- # No reason to call bootstrap or get_api_url functions.
1009+ # No reason to call bootstrap or get_api_address functions.
1010 self.assertFalse(mock_app.bootstrap.called)
1011- self.assertFalse(mock_app.get_api_url.called)
1012+ self.assertFalse(mock_app.get_api_address.called)
1013
1014 def test_already_bootstrapped_race(self, mock_app, mock_open):
1015 # The application correctly reuses an already bootstrapped environment.
1016@@ -915,8 +962,8 @@
1017 options = self.make_options()
1018 with self.patch_get_juju_command():
1019 manage.run(options)
1020- # The bootstrap and get_api_url functions are still called, but this
1021- # time also get_env_type is required.
1022+ # The bootstrap and get_api_address functions are still called, but
1023+ # this time also get_env_type is required.
1024 # The environment type is retrieved from the jenv.
1025 mock_app.bootstrap.assert_called_once_with(
1026 options.env_name, self.juju_command,
1027@@ -924,7 +971,7 @@
1028 upload_tools=options.upload_tools,
1029 upload_series=options.upload_series,
1030 constraints=options.constraints)
1031- mock_app.get_api_url.assert_called_once_with(
1032+ mock_app.get_api_address.assert_called_once_with(
1033 options.env_name, self.juju_command)
1034 mock_app.get_env_type.assert_called_once_with(env)
1035
1036@@ -938,7 +985,7 @@
1037 manage.run(options)
1038 # Ensure the browser is still open without an auth token.
1039 mock_app.create_auth_token.assert_called_once_with(env)
1040- mock_open.assert_called_once_with('https://1.2.3.4')
1041+ mock_open.assert_called_once_with('https://1.2.3.5')
1042
1043 def test_bundle(self, mock_app, mock_open):
1044 # A bundle is correctly deployed by the application.

Subscribers

People subscribed via source and target branches