Merge lp:~frankban/juju-quickstart/jenv-env into lp:juju-quickstart
- jenv-env
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 107 |
Proposed branch: | lp:~frankban/juju-quickstart/jenv-env |
Merge into: | lp:juju-quickstart |
Diff against target: |
414 lines (+171/-44) 6 files modified
quickstart/app.py (+8/-8) quickstart/manage.py (+6/-4) quickstart/models/jenv.py (+69/-2) quickstart/tests/models/test_jenv.py (+57/-4) quickstart/tests/test_app.py (+26/-21) quickstart/tests/test_manage.py (+5/-5) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/jenv-env |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+244772@code.launchpad.net |
Commit message
Description of the change
Use both credentials to connect to Juju.
Use both the user name and password when connecting
to the Juju WebSocket API.
Update the jenv models to reflect what we expect to
find in the jenv file.
QA:
- use quickstart as usual
(.venv/bin/python juju-quickstart ...);
- the user is now printed to stdout;
- the environment and the GUI log in correctly,
in the case the environment is bootstrapped by
quickstart or already there.
Francesco Banconi (frankban) wrote : | # |
Jay R. Wren (evarlast) wrote : | # |
On 2014/12/15 17:51:50, frankban wrote:
> Please take a look.
LGTM
Richard Harding (rharding) wrote : | # |
LGTM no qa. Love it when things are as straight forward as this seems.
Brad Crittenden (bac) wrote : | # |
QA - OK
I was surprised to see the username printed as 'user-bac' (because I
hadn't noticed that part of the code).
Why do we have the user- prefix? Is that a juju-gui thing?
I don't think we should make that user visible, i.e. don't print 'user-'
when showing the credentials being used. The prefix does not appear in
the .jenv file or 'juju user list'.
Francesco Banconi (frankban) wrote : | # |
On 2014/12/15 19:54:50, bac wrote:
> QA - OK
> I was surprised to see the username printed as 'user-bac' (because I
hadn't
> noticed that part of the code).
> Why do we have the user- prefix? Is that a juju-gui thing?
> I don't think we should make that user visible, i.e. don't print
'user-' when
> showing the credentials being used. The prefix does not appear in the
.jenv file
> or 'juju user list'.
Hi Brad,
the use prefix is required when loggin in to the Juju API. But I agree
with you:
hiding it in the UX could be a good idea, especially because I checked
with
the GUI trunk and we already do that in the login form.
I'll merge this and propose a follow up branch to hide the prefix.
Thank you for the QA and thank you all for the reviews!
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Use both credentials to connect to Juju.
Use both the user name and password when connecting
to the Juju WebSocket API.
Update the jenv models to reflect what we expect to
find in the jenv file.
QA:
- use quickstart as usual
(.venv/
- the user is now printed to stdout;
- the environment and the GUI log in correctly,
in the case the environment is bootstrapped by
quickstart or already there.
R=jay.wren, rharding, bac
CC=
https:/
Preview Diff
1 | === modified file 'quickstart/app.py' |
2 | --- quickstart/app.py 2014-11-12 16:40:54 +0000 |
3 | +++ quickstart/app.py 2014-12-15 17:51:32 +0000 |
4 | @@ -293,16 +293,16 @@ |
5 | raise ProgramExit(msg) |
6 | |
7 | |
8 | -def get_admin_secret(env_name): |
9 | - """Return the Juju admin secret for the given environment name. |
10 | +def get_credentials(env_name): |
11 | + """Return the Juju credentials for the given environment name. |
12 | |
13 | - Parse the jenv file to retrieve the admin secret. |
14 | - Raise a ProgramExit if the admin secret cannot be retrieved. |
15 | + Parse the jenv file to retrieve the environment user name and password. |
16 | + Raise a ProgramExit if the credentials cannot be retrieved. |
17 | """ |
18 | try: |
19 | - return jenv.get_value(env_name, 'bootstrap-config', 'admin-secret') |
20 | + return jenv.get_credentials(env_name) |
21 | except ValueError as err: |
22 | - msg = b'cannot retrieve environment admin-secret: {}'.format(err) |
23 | + msg = b'cannot retrieve environment credentials: {}'.format(err) |
24 | raise ProgramExit(msg) |
25 | |
26 | |
27 | @@ -323,7 +323,7 @@ |
28 | return 'wss://{}'.format(api_address) |
29 | |
30 | |
31 | -def connect(api_url, admin_secret): |
32 | +def connect(api_url, username, password): |
33 | """Connect to the Juju API server and log in using the given credentials. |
34 | |
35 | Return a connected and authenticated Juju Environment instance. |
36 | @@ -346,7 +346,7 @@ |
37 | else: |
38 | break |
39 | try: |
40 | - env.login(admin_secret) |
41 | + env.login(password, user=username) |
42 | except jujuclient.EnvError as err: |
43 | msg = 'unable to log in to the Juju API server on {}: {}' |
44 | raise ProgramExit(msg.format(api_url, err.message)) |
45 | |
46 | === modified file 'quickstart/manage.py' |
47 | --- quickstart/manage.py 2014-11-12 08:48:35 +0000 |
48 | +++ quickstart/manage.py 2014-12-15 17:51:32 +0000 |
49 | @@ -542,10 +542,11 @@ |
50 | api_url = app.get_api_url(options.env_name, juju_command) |
51 | |
52 | # Retrieve the admin-secret for the current environment. |
53 | - admin_secret = app.get_admin_secret(options.env_name) |
54 | + print('retrieving the Juju environment credentials') |
55 | + username, password = app.get_credentials(options.env_name) |
56 | |
57 | print('connecting to {}'.format(api_url)) |
58 | - env = app.connect(api_url, admin_secret) |
59 | + env = app.connect(api_url, username, password) |
60 | |
61 | # Inspect the environment and deploy the charm if required. |
62 | charm_url, machine, service_data, unit_data = app.check_environment( |
63 | @@ -559,10 +560,11 @@ |
64 | address = app.watch(env, unit_name) |
65 | env.close() |
66 | url = 'https://{}'.format(address) |
67 | - print('\nJuju GUI URL: {}\npassword: {}\n'.format(url, admin_secret)) |
68 | + print('\nJuju GUI URL: {}\nusername: {}\npassword: {}\n'.format( |
69 | + url, username, password)) |
70 | gui_api_url = 'wss://{}:443/ws'.format(address) |
71 | print('connecting to the Juju GUI server') |
72 | - gui_env = app.connect(gui_api_url, admin_secret) |
73 | + gui_env = app.connect(gui_api_url, username, password) |
74 | |
75 | # Handle bundle deployment. |
76 | if options.bundle is not None: |
77 | |
78 | === modified file 'quickstart/models/jenv.py' |
79 | --- quickstart/models/jenv.py 2014-11-11 13:21:57 +0000 |
80 | +++ quickstart/models/jenv.py 2014-12-15 17:51:32 +0000 |
81 | @@ -22,6 +22,7 @@ |
82 | |
83 | from __future__ import unicode_literals |
84 | |
85 | +import logging |
86 | import os |
87 | |
88 | from quickstart import ( |
89 | @@ -30,6 +31,12 @@ |
90 | ) |
91 | |
92 | |
93 | +# Define the constant tag used by Juju for user entities. |
94 | +JUJU_USER_TAG = 'user' |
95 | +# Define the default Juju user when an environment is initially bootstrapped. |
96 | +JUJU_DEFAULT_USER = 'admin' |
97 | + |
98 | + |
99 | def exists(env_name): |
100 | """Report whether the jenv generated file exists for the given env_name.""" |
101 | jenv_path = _get_jenv_path(env_name) |
102 | @@ -52,13 +59,73 @@ |
103 | """ |
104 | jenv_path = _get_jenv_path(env_name) |
105 | data = serializers.yaml_load_from_path(jenv_path) |
106 | + try: |
107 | + return _get_value_from_yaml(data, *args) |
108 | + except ValueError as err: |
109 | + raise ValueError( |
110 | + b'cannot read {}: {}'.format(jenv_path.encode('utf-8'), err)) |
111 | + |
112 | + |
113 | +def get_credentials(env_name): |
114 | + """Return the Juju environment credentials stored in the jenv file. |
115 | + |
116 | + The credentials are returned as a tuple (username, password). |
117 | + |
118 | + Raise a ValueError if: |
119 | + - the environment file is not found; |
120 | + - the environment file contents are not parsable by YAML; |
121 | + - the credentials cannot be found. |
122 | + """ |
123 | + jenv_path = _get_jenv_path(env_name) |
124 | + data = serializers.yaml_load_from_path(jenv_path) |
125 | + |
126 | + # Retrieve the user name. |
127 | + try: |
128 | + user = _get_value_from_yaml(data, 'user') |
129 | + except ValueError as err: |
130 | + # This is probably an old version of Juju not supporting multiple |
131 | + # users. Return the default user name. |
132 | + logging.warn('cannot retrieve the user name from {}: {}'.format( |
133 | + jenv_path, bytes(err).decode('utf-8'))) |
134 | + user = JUJU_DEFAULT_USER |
135 | + |
136 | + # Retrieve the password. |
137 | + try: |
138 | + password = _get_value_from_yaml(data, 'password') |
139 | + except ValueError as err: |
140 | + # This is probably an old version of Juju not supporting multiple |
141 | + # users. Fall back to the admin secret. |
142 | + msg = b'cannot retrieve the password from {}: '.format( |
143 | + jenv_path.encode('utf-8')) |
144 | + logging.debug(msg + bytes(err)) |
145 | + try: |
146 | + password = _get_value_from_yaml( |
147 | + data, 'bootstrap-config', 'admin-secret') |
148 | + except ValueError as err: |
149 | + raise ValueError(msg + bytes(err)) |
150 | + |
151 | + return '{}-{}'.format(JUJU_USER_TAG, user), password |
152 | + |
153 | + |
154 | +def _get_value_from_yaml(data, *args): |
155 | + """Read and return a value from the given YAML decoded data. |
156 | + |
157 | + Return the value corresponding to the specified args sequence. |
158 | + For instance, if args is ('bootstrap-config', 'admin-secret'), the value |
159 | + associated with the "admin-secret" key included on the "bootstrap-config" |
160 | + section is returned. |
161 | + |
162 | + Raise a ValueError if: |
163 | + - the YAML contents are not properly structured; |
164 | + - one the keys in args is not found. |
165 | + """ |
166 | section = 'root' |
167 | for key in args: |
168 | try: |
169 | data = data[key] |
170 | except (KeyError, TypeError): |
171 | - msg = ('invalid YAML contents in {}: ''{} key not found in the {} ' |
172 | - 'section'.format(jenv_path, key, section)) |
173 | + msg = ('invalid YAML contents: {} key not found in the {} section' |
174 | + ''.format(key, section)) |
175 | raise ValueError(msg.encode('utf-8')) |
176 | section = key |
177 | return data |
178 | |
179 | === modified file 'quickstart/tests/models/test_jenv.py' |
180 | --- quickstart/tests/models/test_jenv.py 2014-11-11 13:21:57 +0000 |
181 | +++ quickstart/tests/models/test_jenv.py 2014-12-15 17:51:32 +0000 |
182 | @@ -87,8 +87,8 @@ |
183 | # A ValueError is raised if the specified section cannot be found. |
184 | with self.make_jenv('local', yaml.safe_dump(self.jenv_data)) as path: |
185 | expected_error = ( |
186 | - 'invalid YAML contents in {}: no-such key not found in the ' |
187 | - 'root section'.format(path)) |
188 | + 'cannot read {}: invalid YAML contents: no-such key not found ' |
189 | + 'in the root section'.format(path)) |
190 | with self.assert_value_error(expected_error): |
191 | jenv.get_value('local', 'no-such') |
192 | |
193 | @@ -96,8 +96,8 @@ |
194 | # A ValueError is raised if the specified subsection cannot be found. |
195 | with self.make_jenv('local', yaml.safe_dump(self.jenv_data)) as path: |
196 | expected_error = ( |
197 | - 'invalid YAML contents in {}: series key not found in the ' |
198 | - 'bootstrap-config section'.format(path)) |
199 | + 'cannot read {}: invalid YAML contents: series key not found ' |
200 | + 'in the bootstrap-config section'.format(path)) |
201 | with self.assert_value_error(expected_error): |
202 | jenv.get_value('local', 'bootstrap-config', 'series') |
203 | |
204 | @@ -108,3 +108,56 @@ |
205 | with self.assertRaises(ValueError) as context_manager: |
206 | jenv.get_value('ec2') |
207 | self.assertIn(expected_error, bytes(context_manager.exception)) |
208 | + |
209 | + |
210 | +class TestGetCredentials( |
211 | + helpers.JenvFileTestsMixin, helpers.ValueErrorTestsMixin, |
212 | + unittest.TestCase): |
213 | + |
214 | + def test_valid_credentials(self): |
215 | + # The user name and password are correctly returned. |
216 | + data = {'user': 'jean-luc', 'password': 'Secret!'} |
217 | + with self.make_jenv('local', yaml.safe_dump(data)): |
218 | + username, password = jenv.get_credentials('local') |
219 | + self.assertEqual('user-jean-luc', username) |
220 | + self.assertEqual('Secret!', password) |
221 | + |
222 | + def test_default_user(self): |
223 | + # The default user name is returned if it's not possible to retrieve it |
224 | + # otherwise. |
225 | + data = {'password': 'Secret!'} |
226 | + with self.make_jenv('local', yaml.safe_dump(data)) as path: |
227 | + expected_logs = [ |
228 | + 'cannot retrieve the user name from {}: ' |
229 | + 'invalid YAML contents: ' |
230 | + 'user key not found in the root section'.format(path)] |
231 | + with helpers.assert_logs(expected_logs, 'warn'): |
232 | + username, password = jenv.get_credentials('local') |
233 | + self.assertEqual('user-admin', username) |
234 | + self.assertEqual('Secret!', password) |
235 | + |
236 | + def test_using_admin_secret(self): |
237 | + # If the password is not found in the jenv file, the admin secret is |
238 | + # returned if present. |
239 | + data = { |
240 | + 'user': 'who', |
241 | + 'bootstrap-config': {'admin-secret': 'Admin!'}, |
242 | + } |
243 | + with self.make_jenv('local', yaml.safe_dump(data)) as path: |
244 | + expected_logs = [ |
245 | + 'cannot retrieve the password from {}: ' |
246 | + 'invalid YAML contents: ' |
247 | + 'password key not found in the root section'.format(path)] |
248 | + with helpers.assert_logs(expected_logs, 'debug'): |
249 | + username, password = jenv.get_credentials('local') |
250 | + self.assertEqual('Admin!', password) |
251 | + |
252 | + def test_password_not_found(self): |
253 | + # A ValueError is raised if the password cannot be found anywhere. |
254 | + with self.make_jenv('local', yaml.safe_dump({})) as path: |
255 | + expected_error = ( |
256 | + 'cannot retrieve the password from {}: ' |
257 | + 'invalid YAML contents: bootstrap-config key ' |
258 | + 'not found in the root section'.format(path)) |
259 | + with self.assert_value_error(expected_error): |
260 | + jenv.get_credentials('local') |
261 | |
262 | === modified file 'quickstart/tests/test_app.py' |
263 | --- quickstart/tests/test_app.py 2014-11-12 12:21:09 +0000 |
264 | +++ quickstart/tests/test_app.py 2014-12-15 17:51:32 +0000 |
265 | @@ -469,7 +469,7 @@ |
266 | with self.make_jenv('ec2', '') as path: |
267 | logs = [ |
268 | 'cannot retrieve the Juju API URL: ' |
269 | - 'invalid YAML contents in {}: ' |
270 | + 'cannot read {}: invalid YAML contents: ' |
271 | 'state-servers key not found in the root section'.format(path) |
272 | ] |
273 | with helpers.assert_logs(logs, level='warn'): |
274 | @@ -713,31 +713,32 @@ |
275 | # A ProgramExit is raised if the environment type cannot be retrieved. |
276 | with self.make_jenv('aws', '') as path: |
277 | expected_error = ( |
278 | - 'cannot retrieve environment type: invalid YAML ' |
279 | - 'contents in {}: bootstrap-config key not found in the root ' |
280 | + 'cannot retrieve environment type: cannot read {}: invalid ' |
281 | + 'YAML contents: bootstrap-config key not found in the root ' |
282 | 'section'.format(path)) |
283 | with self.assert_program_exit(expected_error): |
284 | app.get_env_type('aws') |
285 | |
286 | |
287 | -class TestGetAdminSecret( |
288 | +class TestGetCredentials( |
289 | helpers.JenvFileTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
290 | |
291 | def test_success(self): |
292 | - # The admin secret is successfully retrieved. |
293 | + # The user name and password are successfully retrieved. |
294 | with self.make_jenv('ec2', yaml.safe_dump(self.jenv_data)): |
295 | - admin_secret = app.get_admin_secret('ec2') |
296 | - self.assertEqual('Secret!', admin_secret) |
297 | + username, password = app.get_credentials('ec2') |
298 | + self.assertEqual('user-admin', username) |
299 | + self.assertEqual('Secret!', password) |
300 | |
301 | def test_error(self): |
302 | - # A ProgramExit is raised if the admin secret cannot be retrieved. |
303 | + # A ProgramExit is raised if the credentials cannot be retrieved. |
304 | with self.make_jenv('ec2', '') as path: |
305 | expected_error = ( |
306 | - 'cannot retrieve environment admin-secret: invalid YAML ' |
307 | - 'contents in {}: bootstrap-config key not found in the root ' |
308 | - 'section'.format(path)) |
309 | + 'cannot retrieve environment credentials: cannot retrieve the ' |
310 | + 'password from {}: invalid YAML contents: bootstrap-config ' |
311 | + 'key not found in the root section'.format(path)) |
312 | with self.assert_program_exit(expected_error): |
313 | - app.get_admin_secret('ec2') |
314 | + app.get_credentials('ec2') |
315 | |
316 | |
317 | class TestGetApiUrl( |
318 | @@ -768,16 +769,18 @@ |
319 | |
320 | class TestConnect(ProgramExitTestsMixin, unittest.TestCase): |
321 | |
322 | - admin_secret = 'Secret!' |
323 | + username = 'MyUser' |
324 | + password = 'Secret!' |
325 | api_url = 'wss://api.example.com:17070' |
326 | |
327 | def test_connection_established(self): |
328 | # The connection is done and the Environment instance is returned. |
329 | with mock.patch('quickstart.juju.connect') as mock_connect: |
330 | - env = app.connect(self.api_url, self.admin_secret) |
331 | + env = app.connect(self.api_url, self.username, self.password) |
332 | mock_connect.assert_called_once_with(self.api_url) |
333 | mock_env = mock_connect() |
334 | - mock_env.login.assert_called_once_with(self.admin_secret) |
335 | + mock_env.login.assert_called_once_with( |
336 | + self.password, user=self.username) |
337 | self.assertEqual(mock_env, env) |
338 | |
339 | @mock.patch('time.sleep') |
340 | @@ -788,7 +791,7 @@ |
341 | expected = 'unable to connect to the Juju API server on {}: bad wolf' |
342 | with mock.patch('quickstart.juju.connect', mock_connect): |
343 | with self.assert_program_exit(expected.format(self.api_url)): |
344 | - app.connect(self.api_url, self.admin_secret) |
345 | + app.connect(self.api_url, self.username, self.password) |
346 | mock_connect.assert_called_with(self.api_url) |
347 | self.assertEqual(30, mock_connect.call_count) |
348 | mock_sleep.assert_called_with(1) |
349 | @@ -805,10 +808,11 @@ |
350 | mock_connect = mock.Mock( |
351 | side_effect=[ValueError('bad wolf'), mock_env]) |
352 | with mock.patch('quickstart.juju.connect', mock_connect): |
353 | - env = app.connect(self.api_url, self.admin_secret) |
354 | + env = app.connect(self.api_url, self.username, self.password) |
355 | mock_connect.assert_called_with(self.api_url) |
356 | self.assertEqual(2, mock_connect.call_count) |
357 | - mock_env.login.assert_called_once_with(self.admin_secret) |
358 | + mock_env.login.assert_called_once_with( |
359 | + self.password, user=self.username) |
360 | self.assertEqual(mock_env, env) |
361 | mock_sleep.assert_called_once_with(1) |
362 | expected = 'unable to connect to the Juju API server on {}: bad wolf' |
363 | @@ -822,9 +826,10 @@ |
364 | mock_login = mock_connect().login |
365 | mock_login.side_effect = self.make_env_error('bad wolf') |
366 | with self.assert_program_exit(expected.format(self.api_url)): |
367 | - app.connect(self.api_url, self.admin_secret) |
368 | + app.connect(self.api_url, self.username, self.password) |
369 | mock_connect.assert_called_with(self.api_url) |
370 | - mock_login.assert_called_once_with(self.admin_secret) |
371 | + mock_login.assert_called_once_with( |
372 | + self.password, user=self.username) |
373 | |
374 | def test_other_errors(self): |
375 | # Any other errors occurred during the log in process are not trapped. |
376 | @@ -833,7 +838,7 @@ |
377 | mock_login = mock_connect().login |
378 | mock_login.side_effect = error |
379 | with self.assertRaises(ValueError) as context_manager: |
380 | - app.connect(self.api_url, self.admin_secret) |
381 | + app.connect(self.api_url, self.username, self.password) |
382 | self.assertIs(error, context_manager.exception) |
383 | |
384 | |
385 | |
386 | === modified file 'quickstart/tests/test_manage.py' |
387 | --- quickstart/tests/test_manage.py 2014-11-12 14:10:26 +0000 |
388 | +++ quickstart/tests/test_manage.py 2014-12-15 17:51:32 +0000 |
389 | @@ -787,8 +787,8 @@ |
390 | 'status': 'trusty', |
391 | # The API URL must be retrieved (the environment was not ready). |
392 | 'get_api_url': 'wss://1.2.3.4:17070', |
393 | - # Retrieve the admin secret. |
394 | - 'get_admin_secret': 'Secret!', |
395 | + # Retrieve the environment credentials. |
396 | + 'get_credentials': ('MyUser', 'Secret!'), |
397 | # Connect to the Juju Environment API endpoint. |
398 | 'connect': env, |
399 | # The environment is then checked. |
400 | @@ -838,11 +838,11 @@ |
401 | options.env_name, self.juju_command) |
402 | mock_app.get_api_url.assert_called_once_with( |
403 | options.env_name, self.juju_command) |
404 | - mock_app.get_admin_secret.assert_called_once_with(options.env_name) |
405 | + mock_app.get_credentials.assert_called_once_with(options.env_name) |
406 | mock_app.connect.assert_has_calls([ |
407 | - mock.call('wss://1.2.3.4:17070', 'Secret!'), |
408 | + mock.call('wss://1.2.3.4:17070', 'MyUser', 'Secret!'), |
409 | mock.call().close(), |
410 | - mock.call('wss://1.2.3.4:443/ws', 'Secret!'), |
411 | + mock.call('wss://1.2.3.4:443/ws', 'MyUser', 'Secret!'), |
412 | mock.call().close(), |
413 | ]) |
414 | mock_app.check_environment.assert_called_once_with( |
Reviewers: mp+244772_ code.launchpad. net,
Message:
Please take a look.
Description:
Use both credentials to connect to Juju.
Use both the user name and password when connecting
to the Juju WebSocket API.
Update the jenv models to reflect what we expect to
find in the jenv file.
QA: bin/python juju-quickstart ...);
- use quickstart as usual
(.venv/
- the user is now printed to stdout;
- the environment and the GUI log in correctly,
in the case the environment is bootstrapped by
quickstart or already there.
https:/ /code.launchpad .net/~frankban/ juju-quickstart /jenv-env/ +merge/ 244772
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/190060043/
Affected files (+173, -44 lines): manage. py models/ jenv.py tests/models/ test_jenv. py tests/test_ app.py tests/test_ manage. py
A [revision details]
M quickstart/app.py
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/