Merge lp:~gary/charms/precise/juju-gui/credentialConfig into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Gary Poster
Status: Merged
Merged at revision: 111
Proposed branch: lp:~gary/charms/precise/juju-gui/credentialConfig
Merge into: lp:~juju-gui/charms/precise/juju-gui/trunk
Diff against target: 447 lines (+331/-16)
8 files modified
Makefile (+2/-1)
config.yaml (+5/-0)
hooks/backend.py (+2/-1)
hooks/charmhelpers.py (+288/-0)
hooks/install (+3/-6)
hooks/utils.py (+7/-7)
tests/00-setup (+2/-1)
tests/test_utils.py (+22/-0)
To merge this branch: bzr merge lp:~gary/charms/precise/juju-gui/credentialConfig
Reviewer Review Type Date Requested Status
charmers Pending
Review via email: mp+188670@code.launchpad.net

Description of the change

To post a comment you must log in.
Revision history for this message
Gary Poster (gary) wrote :

Reviewers: mp+188670_code.launchpad.net,

Message:
Please take a look.

Description:
Add password option to charm

In order to support ecosystems work, add a password option that lets the
charm pre-fill the password in the GUI.

To QA, start up the charm, and verify that you need to enter your
password in the GUI. Do not log in, or if you do, log out afterwards.
Next, juju set juju-gui password=[YOUR ADMIN SECRET] and wait about 20
seconds for the charm to reconfigure itself. Then go to the browser
again and you should not have to log in.

https://code.launchpad.net/~gary/charms/precise/juju-gui/credentialConfig/+merge/188670

(do not edit description out of merge proposal)

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

Affected files (+333, -16 lines):
   M Makefile
   A [revision details]
   M config.yaml
   M hooks/backend.py
   A hooks/charmhelpers.py
   M hooks/install
   M hooks/utils.py
   M tests/00-setup
   M tests/test_utils.py

Revision history for this message
Gary Poster (gary) wrote :

Added comment

https://codereview.appspot.com/14226043/diff/1/hooks/charmhelpers.py
File hooks/charmhelpers.py (right):

https://codereview.appspot.com/14226043/diff/1/hooks/charmhelpers.py#newcode4
hooks/charmhelpers.py:4: # This file is taken from the
python-charmhelpers package. It is possibly no
I added this file because tests wouldn't pass without it even though it
was installed on my system, and for the reasons I describe in the
comment. Please only review the comments I added. The rest is
unnecessary.

https://codereview.appspot.com/14226043/

Revision history for this message
Gary Poster (gary) wrote :

*** Submitted:

Add password option to charm

In order to support ecosystems work, add a password option that lets the
charm pre-fill the password in the GUI.

To QA, start up the charm, and verify that you need to enter your
password in the GUI. Do not log in, or if you do, log out afterwards.
Next, juju set juju-gui password=[YOUR ADMIN SECRET] and wait about 20
seconds for the charm to reconfigure itself. Then go to the browser
again and you should not have to log in.

R=
CC=
https://codereview.appspot.com/14226043

https://codereview.appspot.com/14226043/

Revision history for this message
Gary Poster (gary) wrote :

On 2013/10/01 19:21:33, benji wrote:
> LGTM

> https://codereview.appspot.com/14226043/diff/1/Makefile
> File Makefile (right):

> https://codereview.appspot.com/14226043/diff/1/Makefile#newcode20
> Makefile:20: libpython-dev
> Most of the other line continuations use a tab and then four spaces.
Mixing
> tabs and spaces isn't the greatest thing in the world, but neither are
the deep,
> deep indents caused by all-tabs-all-the-time. I'm cool either way.

> https://codereview.appspot.com/14226043/diff/1/hooks/install
> File hooks/install (right):

> https://codereview.appspot.com/14226043/diff/1/hooks/install#newcode24
> hooks/install:24: # We need to install the Juju PPA, which we will do
with a
> It looks like this needs to be word wrapped with the line below.

Argh, sorry, I missed that you had requested changes. Fixing.

https://codereview.appspot.com/14226043/

Revision history for this message
Gary Poster (gary) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2013-09-25 08:58:17 +0000
3+++ Makefile 2013-10-01 17:57:29 +0000
4@@ -16,7 +16,8 @@
5
6 JUJUTEST = juju-test --timeout=120m -v -e "$(JUJU_ENV)" --upload-tools
7 VENV = ./tests/.venv
8-SYSDEPS = build-essential bzr libapt-pkg-dev python-pip python-virtualenv xvfb
9+SYSDEPS = build-essential bzr libapt-pkg-dev python-pip python-virtualenv xvfb \
10+ libpython-dev
11
12 all: setup
13
14
15=== modified file 'config.yaml'
16--- config.yaml 2013-09-12 19:58:07 +0000
17+++ config.yaml 2013-10-01 17:57:29 +0000
18@@ -117,6 +117,11 @@
19 Do not set unless you understand and accept the risks.
20 type: boolean
21 default: true
22+ password:
23+ description: |
24+ If given, the password to use for the environment to immediately
25+ connect. Do not set unless you understand and accept the risks.
26+ type: string
27 sandbox:
28 description: |
29 Run using an in-memory sandbox rather than a real (or even improv) Juju
30
31=== modified file 'hooks/backend.py'
32--- hooks/backend.py 2013-09-12 19:58:07 +0000
33+++ hooks/backend.py 2013-10-01 17:57:29 +0000
34@@ -148,7 +148,8 @@
35 build_dir, secure=config['secure'], sandbox=config['sandbox'],
36 ga_key=config['ga-key'],
37 default_viewmode=config['default-viewmode'],
38- show_get_juju_button=config['show-get-juju-button'])
39+ show_get_juju_button=config['show-get-juju-button'],
40+ password=config.get('password'))
41 # Expose the service.
42 charmhelpers.open_port(80)
43 charmhelpers.open_port(443)
44
45=== added file 'hooks/charmhelpers.py'
46--- hooks/charmhelpers.py 1970-01-01 00:00:00 +0000
47+++ hooks/charmhelpers.py 2013-10-01 17:57:29 +0000
48@@ -0,0 +1,288 @@
49+# Copyright 2012 Canonical Ltd. This software is licensed under the
50+# GNU Affero General Public License version 3 (see the file LICENSE).
51+
52+# This file is taken from the python-charmhelpers package. It is possibly no
53+# longer maintained there, now that a new charmhelpers package has replaced
54+# the one from which this was taken, so the file has been copied here. We
55+# intend to transition to the newer charmhelpers library at some point in the
56+# future.
57+
58+# IMPORTANT: Do not modify this file to add or change functionality. If you
59+# really feel the need to do so, first convert our code to the charmhelpers
60+# library, and modify it instead (or modify the helpers or utils module here,
61+# as appropriate).
62+
63+"""Helper functions for writing Juju charms in Python."""
64+
65+__metaclass__ = type
66+__all__ = ['get_config',
67+ 'log',
68+ 'log_entry',
69+ 'log_exit',
70+ 'relation_get',
71+ 'relation_set',
72+ 'relation_ids',
73+ 'relation_list',
74+ 'config_get',
75+ 'unit_get',
76+ 'open_port',
77+ 'close_port',
78+ 'service_control',
79+ 'unit_info',
80+ 'wait_for_machine',
81+ 'wait_for_page_contents',
82+ 'wait_for_relation',
83+ 'wait_for_unit',
84+ ]
85+
86+from collections import namedtuple
87+import json
88+import operator
89+from shelltoolbox import (
90+ command,
91+ script_name,
92+ run,
93+)
94+import tempfile
95+import time
96+import urllib2
97+import yaml
98+from subprocess import CalledProcessError
99+
100+
101+SLEEP_AMOUNT = 0.1
102+Env = namedtuple('Env', 'uid gid home')
103+# We create a juju_status Command here because it makes testing much,
104+# much easier.
105+juju_status = lambda: command('juju')('status')
106+
107+
108+def log(message, juju_log=command('juju-log')):
109+ return juju_log('--', message)
110+
111+
112+def log_entry():
113+ log("--> Entering {}".format(script_name()))
114+
115+
116+def log_exit():
117+ log("<-- Exiting {}".format(script_name()))
118+
119+
120+def get_config():
121+ _config_get = command('config-get', '--format=json')
122+ return json.loads(_config_get())
123+
124+
125+def relation_get(attribute=None, unit=None, rid=None):
126+ cmd = command('relation-get')
127+ if attribute is None and unit is None and rid is None:
128+ return cmd().strip()
129+ _args = []
130+ if rid:
131+ _args.append('-r')
132+ _args.append(rid)
133+ if attribute is not None:
134+ _args.append(attribute)
135+ if unit:
136+ _args.append(unit)
137+ return cmd(*_args).strip()
138+
139+
140+def relation_set(**kwargs):
141+ cmd = command('relation-set')
142+ args = ['{}={}'.format(k, v) for k, v in kwargs.items()]
143+ cmd(*args)
144+
145+
146+def relation_ids(relation_name):
147+ cmd = command('relation-ids')
148+ args = [relation_name]
149+ return cmd(*args).split()
150+
151+
152+def relation_list(rid=None):
153+ cmd = command('relation-list')
154+ args = []
155+ if rid:
156+ args.append('-r')
157+ args.append(rid)
158+ return cmd(*args).split()
159+
160+
161+def config_get(attribute):
162+ cmd = command('config-get')
163+ args = [attribute]
164+ return cmd(*args).strip()
165+
166+
167+def unit_get(attribute):
168+ cmd = command('unit-get')
169+ args = [attribute]
170+ return cmd(*args).strip()
171+
172+
173+def open_port(port, protocol="TCP"):
174+ cmd = command('open-port')
175+ args = ['{}/{}'.format(port, protocol)]
176+ cmd(*args)
177+
178+
179+def close_port(port, protocol="TCP"):
180+ cmd = command('close-port')
181+ args = ['{}/{}'.format(port, protocol)]
182+ cmd(*args)
183+
184+START = "start"
185+RESTART = "restart"
186+STOP = "stop"
187+RELOAD = "reload"
188+
189+
190+def service_control(service_name, action):
191+ cmd = command('service')
192+ args = [service_name, action]
193+ try:
194+ if action == RESTART:
195+ try:
196+ cmd(*args)
197+ except CalledProcessError:
198+ service_control(service_name, START)
199+ else:
200+ cmd(*args)
201+ except CalledProcessError:
202+ log("Failed to perform {} on service {}".format(action, service_name))
203+
204+
205+def configure_source(update=False):
206+ source = config_get('source')
207+ if ((source.startswith('ppa:') or
208+ source.startswith('cloud:') or
209+ source.startswith('http:'))):
210+ run('add-apt-repository', source)
211+ if source.startswith("http:"):
212+ run('apt-key', 'import', config_get('key'))
213+ if update:
214+ run('apt-get', 'update')
215+
216+
217+def make_charm_config_file(charm_config):
218+ charm_config_file = tempfile.NamedTemporaryFile()
219+ charm_config_file.write(yaml.dump(charm_config))
220+ charm_config_file.flush()
221+ # The NamedTemporaryFile instance is returned instead of just the name
222+ # because we want to take advantage of garbage collection-triggered
223+ # deletion of the temp file when it goes out of scope in the caller.
224+ return charm_config_file
225+
226+
227+def unit_info(service_name, item_name, data=None, unit=None):
228+ if data is None:
229+ data = yaml.safe_load(juju_status())
230+ service = data['services'].get(service_name)
231+ if service is None:
232+ # XXX 2012-02-08 gmb:
233+ # This allows us to cope with the race condition that we
234+ # have between deploying a service and having it come up in
235+ # `juju status`. We could probably do with cleaning it up so
236+ # that it fails a bit more noisily after a while.
237+ return ''
238+ units = service['units']
239+ if unit is not None:
240+ item = units[unit][item_name]
241+ else:
242+ # It might seem odd to sort the units here, but we do it to
243+ # ensure that when no unit is specified, the first unit for the
244+ # service (or at least the one with the lowest number) is the
245+ # one whose data gets returned.
246+ sorted_unit_names = sorted(units.keys())
247+ item = units[sorted_unit_names[0]][item_name]
248+ return item
249+
250+
251+def get_machine_data():
252+ return yaml.safe_load(juju_status())['machines']
253+
254+
255+def wait_for_machine(num_machines=1, timeout=300):
256+ """Wait `timeout` seconds for `num_machines` machines to come up.
257+
258+ This wait_for... function can be called by other wait_for functions
259+ whose timeouts might be too short in situations where only a bare
260+ Juju setup has been bootstrapped.
261+
262+ :return: A tuple of (num_machines, time_taken). This is used for
263+ testing.
264+ """
265+ # You may think this is a hack, and you'd be right. The easiest way
266+ # to tell what environment we're working in (LXC vs EC2) is to check
267+ # the dns-name of the first machine. If it's localhost we're in LXC
268+ # and we can just return here.
269+ if get_machine_data()[0]['dns-name'] == 'localhost':
270+ return 1, 0
271+ start_time = time.time()
272+ while True:
273+ # Drop the first machine, since it's the Zookeeper and that's
274+ # not a machine that we need to wait for. This will only work
275+ # for EC2 environments, which is why we return early above if
276+ # we're in LXC.
277+ machine_data = get_machine_data()
278+ non_zookeeper_machines = [
279+ machine_data[key] for key in machine_data.keys()[1:]]
280+ if len(non_zookeeper_machines) >= num_machines:
281+ all_machines_running = True
282+ for machine in non_zookeeper_machines:
283+ if machine.get('instance-state') != 'running':
284+ all_machines_running = False
285+ break
286+ if all_machines_running:
287+ break
288+ if time.time() - start_time >= timeout:
289+ raise RuntimeError('timeout waiting for service to start')
290+ time.sleep(SLEEP_AMOUNT)
291+ return num_machines, time.time() - start_time
292+
293+
294+def wait_for_unit(service_name, timeout=480):
295+ """Wait `timeout` seconds for a given service name to come up."""
296+ wait_for_machine(num_machines=1)
297+ start_time = time.time()
298+ while True:
299+ state = unit_info(service_name, 'agent-state')
300+ if 'error' in state or state == 'started':
301+ break
302+ if time.time() - start_time >= timeout:
303+ raise RuntimeError('timeout waiting for service to start')
304+ time.sleep(SLEEP_AMOUNT)
305+ if state != 'started':
306+ raise RuntimeError('unit did not start, agent-state: ' + state)
307+
308+
309+def wait_for_relation(service_name, relation_name, timeout=120):
310+ """Wait `timeout` seconds for a given relation to come up."""
311+ start_time = time.time()
312+ while True:
313+ relation = unit_info(service_name, 'relations').get(relation_name)
314+ if relation is not None and relation['state'] == 'up':
315+ break
316+ if time.time() - start_time >= timeout:
317+ raise RuntimeError('timeout waiting for relation to be up')
318+ time.sleep(SLEEP_AMOUNT)
319+
320+
321+def wait_for_page_contents(url, contents, timeout=120, validate=None):
322+ if validate is None:
323+ validate = operator.contains
324+ start_time = time.time()
325+ while True:
326+ try:
327+ stream = urllib2.urlopen(url)
328+ except (urllib2.HTTPError, urllib2.URLError):
329+ pass
330+ else:
331+ page = stream.read()
332+ if validate(page, contents):
333+ return page
334+ if time.time() - start_time >= timeout:
335+ raise RuntimeError('timeout waiting for contents of ' + url)
336+ time.sleep(SLEEP_AMOUNT)
337
338=== modified file 'hooks/install'
339--- hooks/install 2013-08-06 09:56:52 +0000
340+++ hooks/install 2013-10-01 17:57:29 +0000
341@@ -21,10 +21,7 @@
342 import json
343 import os
344
345-# If the user's environment has "juju-origin: ppa" set, they will
346-# automatically have access to python-charmhelpers and python-shelltoolbox.
347-# However, we want to support environments that use the non-PPA environment as
348-# well. To do so, we need to install the Juju PPA, which we will do with a
349+# We need to install the Juju PPA, which we will do with a
350 # couple of functions that are actually maintained in python-shelltoolbox.
351 import bootstrap_utils
352
353@@ -39,8 +36,8 @@
354 # Python dependencies must be installed here so that the charm can import and
355 # use required libraries.
356 PYTHON_DEPENDENCIES = (
357- 'python-apt', 'python-charmhelpers', 'python-launchpadlib',
358- 'python-shelltoolbox', 'python-tempita',
359+ 'python-apt', 'python-launchpadlib', 'python-shelltoolbox',
360+ 'python-tempita',
361 )
362 bootstrap_utils.run(*(('apt-get', 'install', '-y') + PYTHON_DEPENDENCIES))
363
364
365=== modified file 'hooks/utils.py'
366--- hooks/utils.py 2013-09-23 07:33:58 +0000
367+++ hooks/utils.py 2013-10-01 17:57:29 +0000
368@@ -406,15 +406,15 @@
369 console_enabled, login_help, readonly, in_staging, charmworld_url,
370 build_dir, secure=True, sandbox=False,
371 default_viewmode='sidebar', show_get_juju_button=False,
372- config_js_path=None, ga_key=''):
373+ config_js_path=None, ga_key='', password=None):
374 """Generate the GUI configuration file."""
375 log('Generating the Juju GUI configuration file.')
376 is_legacy_juju = legacy_juju()
377- user, password = None, None
378- if (is_legacy_juju and in_staging) or sandbox:
379- user, password = 'admin', 'admin'
380- else:
381- user, password = None, None
382+ user = 'admin' if is_legacy_juju else 'user-admin'
383+ # Normalize empty string passwords to None.
384+ password = password if password else None
385+ if password is None and ((is_legacy_juju and in_staging) or sandbox):
386+ password = 'admin'
387 api_backend = 'python' if is_legacy_juju else 'go'
388 if secure:
389 protocol = 'wss'
390@@ -678,7 +678,7 @@
391 release_dir = os.path.join(BASE_DIR, 'release')
392 cmd_log(run('rm', '-rf', release_dir))
393 os.mkdir(release_dir)
394- uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f')
395+ uncompress = command('tar', '-x', '-a', '-C', release_dir, '-f')
396 cmd_log(uncompress(release_tarball))
397 # Link the Juju GUI dir to the contents of the release tarball.
398 cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR))
399
400=== modified file 'tests/00-setup'
401--- tests/00-setup 2013-06-11 14:04:04 +0000
402+++ tests/00-setup 2013-10-01 17:57:29 +0000
403@@ -27,7 +27,8 @@
404 if [ ! -f "$ACTIVATE" -o "$REQUIREMENTS" -nt "$ACTIVATE" ]; then
405 virtualenv --distribute $VENV
406 . $VENV/bin/activate && \
407- yes w | pip install --use-mirrors -r $REQUIREMENTS
408+ yes w | env BZR_PLUGIN_PATH='-user' \
409+ pip install --use-mirrors -r $REQUIREMENTS
410 touch $VENV/bin/activate
411 fi
412 }
413
414=== added symlink 'tests/charmhelpers.py'
415=== target is u'../hooks/charmhelpers.py'
416=== modified file 'tests/test_utils.py'
417--- tests/test_utils.py 2013-09-12 19:58:07 +0000
418+++ tests/test_utils.py 2013-10-01 17:57:29 +0000
419@@ -791,6 +791,28 @@
420 self.assertIn("socket_url: 'ws://", js_conf)
421 self.assertIn('socket_protocol: "ws"', js_conf)
422
423+ @mock.patch('utils.legacy_juju')
424+ def test_write_gui_config_default_python_password(self, mock_legacy_juju):
425+ mock_legacy_juju.return_value = True
426+ write_gui_config(
427+ False, 'This is login help.', True, True, self.charmworld_url,
428+ self.build_dir, config_js_path='config',
429+ password='kumquat')
430+ js_conf = self.files['config']
431+ self.assertIn('user: "admin"', js_conf)
432+ self.assertIn('password: "kumquat"', js_conf)
433+
434+ @mock.patch('utils.legacy_juju')
435+ def test_write_gui_config_default_go_password(self, mock_legacy_juju):
436+ mock_legacy_juju.return_value = False
437+ write_gui_config(
438+ False, 'This is login help.', True, True, self.charmworld_url,
439+ self.build_dir, config_js_path='config',
440+ password='kumquat')
441+ js_conf = self.files['config']
442+ self.assertIn('user: "user-admin"', js_conf)
443+ self.assertIn('password: "kumquat"', js_conf)
444+
445 def test_setup_haproxy_config_insecure(self):
446 setup_haproxy_config(self.ssl_cert_path, secure=False)
447 # The insecure approach eliminates the https redirect.

Subscribers

People subscribed via source and target branches

to all changes: