Merge lp:~frankban/juju-quickstart/jenv-env into lp:juju-quickstart

Proposed by Francesco Banconi
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
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+244772@code.launchpad.net

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.

https://codereview.appspot.com/190060043/

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

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:
- 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.

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):
   A [revision details]
   M quickstart/app.py
   M quickstart/manage.py
   M quickstart/models/jenv.py
   M quickstart/tests/models/test_jenv.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_manage.py

Revision history for this message
Jay R. Wren (evarlast) wrote :

On 2014/12/15 17:51:50, frankban wrote:
> Please take a look.

LGTM

https://codereview.appspot.com/190060043/

Revision history for this message
Richard Harding (rharding) wrote :

LGTM no qa. Love it when things are as straight forward as this seems.

https://codereview.appspot.com/190060043/

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

https://codereview.appspot.com/190060043/

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

https://codereview.appspot.com/190060043/

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

R=jay.wren, rharding, bac
CC=
https://codereview.appspot.com/190060043

https://codereview.appspot.com/190060043/

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 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(

Subscribers

People subscribed via source and target branches