Merge lp:~frankban/juju-quickstart/new-auth-api-endpoint into lp:juju-quickstart
- new-auth-api-endpoint
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+249102@code.launchpad.net |
Commit message
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://
"wss://
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.
before releasing the new Quickstart.
Tests: `make check`
QA:
- bootstrap quickstart as usual: `devenv/
- 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/
- ensure that the deployment request succeeds;
- if possible, do the above with and older version of Juju,
to ensure backward compatibility.
Done, thank you!
Francesco Banconi (frankban) wrote : | # |
Martin Hilton (martin-hilton) wrote : | # |
LGTM: No QA
https:/
File quickstart/app.py (right):
https:/
quickstart/
Is it really necessary to have or_none in the name of this function?
Jeff Pihach (hatch) wrote : | # |
LGTM with some possible cleanups.
QA OK!
https:/
File quickstart/app.py (right):
https:/
quickstart/
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:/
File quickstart/
https:/
quickstart/
serializers.
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.
Francesco Banconi (frankban) wrote : | # |
Thanks for the reviews!
https:/
File quickstart/app.py (right):
https:/
quickstart/
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:/
File quickstart/
https:/
quickstart/
serializers.
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.
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://
"wss://
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.
before releasing the new Quickstart.
Tests: `make check`
QA:
- bootstrap quickstart as usual: `devenv/
- 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/
- 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:/
Preview Diff
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. |
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 <address> :17070" , the new <address> :17070/ environment/ <env-uuid> /api"
path. In essence, instead of the usual
"wss://
"wss://
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 MINIMUM_ REVISIONS_ FOR_NEW_ API_ENDPOINT
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.
before releasing the new Quickstart.
Tests: `make check`
QA: bin/juju- quickstart` ; bin/juju- quickstart bundle: mediawiki/ single` ;
- bootstrap quickstart as usual: `devenv/
- 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/
- 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): jujutools. py manage. py models/ charms. py models/ jenv.py settings. py tests/helpers. py tests/models/ test_charms. py tests/models/ test_jenv. py tests/test_ app.py tests/test_ jujutools. py tests/test_ manage. py
A [revision details]
M quickstart/app.py
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/