Merge lp:~gary/charms/precise/juju-gui/credentialConfig into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- credentialConfig
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+188670@code.launchpad.net |
Commit message
Description of the change
Adjust formatting
Gary Poster (gary) wrote : | # |
Gary Poster (gary) wrote : | # |
Added comment
https:/
File hooks/charmhelp
https:/
hooks/charmhelp
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.
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.
Gary Poster (gary) wrote : | # |
On 2013/10/01 19:21:33, benji wrote:
> LGTM
> https:/
> File Makefile (right):
> https:/
> 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-
> https:/
> File hooks/install (right):
> https:/
> 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.
Gary Poster (gary) wrote : | # |
Preview Diff
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. |
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/ credentialConfi g/+merge/ 188670
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/14226043/
Affected files (+333, -16 lines): ers.py
M Makefile
A [revision details]
M config.yaml
M hooks/backend.py
A hooks/charmhelp
M hooks/install
M hooks/utils.py
M tests/00-setup
M tests/test_utils.py