Merge lp:~juju-gui/charms/precise/juju-gui/trunk into lp:~charmers/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- trunk
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Mark Mims |
Approved revision: | 34 |
Merged at revision: | 32 |
Proposed branch: | lp:~juju-gui/charms/precise/juju-gui/trunk |
Merge into: | lp:~charmers/charms/precise/juju-gui/trunk |
Diff against target: |
1381 lines (+502/-214) 18 files modified
HACKING.md (+2/-2) README.md (+3/-7) config.yaml (+14/-5) config/config.js.template (+10/-6) config/haproxy.cfg.template (+42/-0) config/haproxy.conf (+22/-0) config/juju-api-agent.conf.template (+4/-4) config/juju-api-improv.conf.template (+4/-4) config/nginx-site.template (+18/-14) config/nginx.conf (+1/-1) hooks/config-changed (+21/-25) hooks/install (+33/-22) hooks/start (+11/-20) hooks/stop (+6/-10) hooks/utils.py (+132/-31) revision (+1/-1) tests/deploy.test (+63/-32) tests/test_utils.py (+115/-30) |
To merge this branch: | bzr merge lp:~juju-gui/charms/precise/juju-gui/trunk |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mark Mims (community) | Approve | ||
Review via email: mp+152429@code.launchpad.net |
Commit message
- Add support for Firefox and self-signed certs
- Add support to help us integrate the charm into CI tests
- Address problem with newer charmhelpers incompatibility
Description of the change
- Add support for Firefox and self-signed certs
- Add support to help us integrate the charm into CI tests
- Address problem with newer charmhelpers incompatibility
mgz fixed the last problem separately and differently in https:/
- 34. By Gary Poster
-
add support for socket_protocol so that the websocket address can be generated dynamically in the browser for greater deployment flexibility (such as canonistack), while maintaining backwards compatibility with earlier GUI releases. Note that, as of this writing, you need to use lp:juju-gui (trunk) to make this dynamic addressing work.
add ability to deploy insecurely (http/ws), in order to facilitate CI tests
add tests for new functionality
update documentation
speed up default deployment by only getting staging dependencies when needed, and only getting build dependencies when needed (bug 1117896). On EC2, this speeds up the default deployment (from release) by about 2.5 minutes.
work around backward incompatibility in update to charmhelpers' log function.
increase timeout for tests to make them pass more often
Mark Mims (mark-mims) wrote : | # |
Preview Diff
1 | === modified file 'HACKING.md' |
2 | --- HACKING.md 2013-01-11 17:56:35 +0000 |
3 | +++ HACKING.md 2013-03-13 22:47:20 +0000 |
4 | @@ -16,7 +16,7 @@ |
5 | |
6 | You'll also need some dependencies and developer basics. |
7 | |
8 | - sudo apt-get install bzr autoconf libtool python-charmhelpers |
9 | + sudo apt-get install bzr autoconf libtool python-charmhelpers xvfb |
10 | |
11 | Next, you need the bzr branch. We work from |
12 | [lp:~juju-gui/charms/precise/juju-gui/trunk](https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk). |
13 | @@ -29,7 +29,7 @@ |
14 | locally. The files may be installed globally, or into your home directory (as |
15 | here): |
16 | |
17 | - sudo apt-get install autoconf libtool python-charmhelpers |
18 | + sudo apt-get install autoconf libtool python-charmhelpers python-tempita |
19 | bzr branch lp:~jimbaker/juju-jitsu/unit-test jitsu-unit-test |
20 | cd jitsu-unit-test |
21 | autoreconf |
22 | |
23 | === modified file 'README.md' |
24 | --- README.md 2013-01-24 16:37:54 +0000 |
25 | +++ README.md 2013-03-13 22:47:20 +0000 |
26 | @@ -6,8 +6,7 @@ |
27 | |
28 | The Juju GUI supports recent releases of Chrome and Chromium. Recent Firefox |
29 | releases are also supported, but regressions may occur until the completion of |
30 | -upcoming continuous integration work. Internet Explorer 10 will be supported |
31 | -soon. |
32 | +upcoming continuous integration work. |
33 | |
34 | ## Demo/Staging Server ## |
35 | |
36 | @@ -50,7 +49,6 @@ |
37 | open-ports: |
38 | - 80/tcp |
39 | - 443/tcp |
40 | - - 8080/tcp |
41 | public-address: ec2-www-xxx-yyy-zzz.compute-1.amazonaws.com |
42 | |
43 | That tells me I can go to the public-address in my browser via HTTPS |
44 | @@ -58,10 +56,8 @@ |
45 | start configuring the rest of Juju with the GUI. You should see a similar |
46 | web address. Accessing the GUI via HTTP will redirect to using HTTPS. |
47 | |
48 | -Note that currently, when using Firefox with self-signed certificates, you need |
49 | -to first manually add a TLS certificate exception for the Juju API port |
50 | -(default is 8080, see `juju-api-port` in `config.yaml`), and only then do the |
51 | -same for the main Juju GUI HTTPS connection on port 443. |
52 | +By default, the deployment uses self-signed certificates. The browser will ask |
53 | +you to accept a security exception once. |
54 | |
55 | You will see a login form with the username fixed to "admin". The password is |
56 | the same as your Juju environment's `admin-secret`, found in |
57 | |
58 | === modified file 'config.yaml' |
59 | --- config.yaml 2013-01-23 16:41:38 +0000 |
60 | +++ config.yaml 2013-03-13 22:47:20 +0000 |
61 | @@ -17,11 +17,6 @@ |
62 | The Juju API Bazaar branch (implementing the websocket server). |
63 | type: string |
64 | default: lp:~hazmat/juju/rapi-rollup |
65 | - juju-api-port: |
66 | - description: | |
67 | - The port used by the websocket server. |
68 | - type: int |
69 | - default: 8080 |
70 | staging: |
71 | description: | |
72 | Connect the Juju GUI to the staging backend |
73 | @@ -78,3 +73,17 @@ |
74 | to mutate the environment. |
75 | type: boolean |
76 | default: false |
77 | + serve-tests: |
78 | + description: | |
79 | + Whether or not the GUI unit tests are exposed. If this option is enabled, |
80 | + unit tests can be run in the browser by visiting the URL |
81 | + "https://[Juju GUI address]/test/". |
82 | + type: boolean |
83 | + default: false |
84 | + secure: |
85 | + description: | |
86 | + In order to run the GUI over a non secure connection (HTTP) set this flag |
87 | + to false. Do not set this property unless you understand and accept the |
88 | + risks. |
89 | + type: boolean |
90 | + default: true |
91 | |
92 | === modified file 'config/config.js.template' |
93 | --- config/config.js.template 2013-01-16 10:53:11 +0000 |
94 | +++ config/config.js.template 2013-03-13 22:47:20 +0000 |
95 | @@ -1,14 +1,18 @@ |
96 | var juju_config = { |
97 | - consoleEnabled: %(console_enabled)s, |
98 | + consoleEnabled: {{console_enabled}}, |
99 | serverRouting: false, |
100 | html5: true, |
101 | container: '#main', |
102 | viewContainer: '#main', |
103 | transitions: false, |
104 | charm_store_url: 'https://jujucharms.com/', |
105 | - socket_url: 'wss://%(address)s:%(port)s/ws', |
106 | - user: %(user)s, |
107 | - password: %(password)s, |
108 | - readOnly: %(readonly)s, |
109 | - login_help: %(login_help)s |
110 | + // socket_url is only honored in older versions of the GUI. |
111 | + socket_url: '{{raw_protocol}}://{{address}}/ws', |
112 | + // socket_protocol is used instead by newer versions of the GUI to |
113 | + // dynamically generate the websocket address. |
114 | + socket_protocol: {{protocol}}, |
115 | + user: {{user}}, |
116 | + password: {{password}}, |
117 | + readOnly: {{readonly}}, |
118 | + login_help: {{login_help}} |
119 | }; |
120 | |
121 | === added file 'config/haproxy.cfg.template' |
122 | --- config/haproxy.cfg.template 1970-01-01 00:00:00 +0000 |
123 | +++ config/haproxy.cfg.template 2013-03-13 22:47:20 +0000 |
124 | @@ -0,0 +1,42 @@ |
125 | +global |
126 | + maxconn 4096 |
127 | + user haproxy |
128 | + group haproxy |
129 | + daemon |
130 | + # Set the base path for the WebSocket TLS certificate. |
131 | + ca-base {{ssl_cert_path}} |
132 | + # Set the base path for the HTTPS TLS certificate. |
133 | + crt-base {{ssl_cert_path}} |
134 | + |
135 | +defaults |
136 | + log global |
137 | + maxconn 4096 |
138 | + mode http |
139 | + option http-server-close |
140 | + timeout connect 5s |
141 | + timeout client 30s |
142 | + timeout server 30s |
143 | + # Set a long timeout for WebSocket connections. |
144 | + timeout tunnel 8h |
145 | + |
146 | +backend juju |
147 | + # The Juju WebSocket backend. |
148 | + # Re-encrypt outgoing connections. |
149 | + server ws1 127.0.0.1:{{api_port}} ssl ca-file {{api_pem}} verify required check-ssl inter 500ms |
150 | + |
151 | +backend web |
152 | + # Web traffic. |
153 | + server web1 127.0.0.1:{{web_port}} check inter 500ms |
154 | + |
155 | +frontend public |
156 | + # Redirect all HTTP traffic to HTTPS. |
157 | + bind :80 |
158 | + {{if secure}} |
159 | + redirect scheme https if !{ ssl_fc } |
160 | + {{endif}} |
161 | + # Handle HTTPS. |
162 | + bind :443 ssl crt {{web_pem}} |
163 | + # Send WebSocket connections to the Juju backend. |
164 | + use_backend juju if { path /ws } |
165 | + # Send everything else to the web server. |
166 | + default_backend web |
167 | |
168 | === added file 'config/haproxy.conf' |
169 | --- config/haproxy.conf 1970-01-01 00:00:00 +0000 |
170 | +++ config/haproxy.conf 2013-03-13 22:47:20 +0000 |
171 | @@ -0,0 +1,22 @@ |
172 | +description "HAProxy" |
173 | +author "Canonical" |
174 | + |
175 | +start on (filesystem and net-device-up IFACE=lo) |
176 | +stop on runlevel [!2345] |
177 | + |
178 | +env CONF=/etc/haproxy/haproxy.cfg |
179 | +env DAEMON=/usr/sbin/haproxy |
180 | + |
181 | +expect fork |
182 | +respawn |
183 | +respawn limit 10 5 |
184 | + |
185 | +pre-start script |
186 | + # Test configuration, exit if errors are found. |
187 | + $DAEMON -c -f $CONF |
188 | + if [ $? -ne 0 ] |
189 | + then exit $? |
190 | + fi |
191 | +end script |
192 | + |
193 | +exec $DAEMON -f $CONF |
194 | |
195 | === modified file 'config/juju-api-agent.conf.template' |
196 | --- config/juju-api-agent.conf.template 2013-01-03 15:02:38 +0000 |
197 | +++ config/juju-api-agent.conf.template 2013-03-13 22:47:20 +0000 |
198 | @@ -4,11 +4,11 @@ |
199 | start on runlevel [2345] |
200 | stop on runlevel [!2345] |
201 | |
202 | -env PYTHONPATH=%(juju_dir)s:$PYTHONPATH |
203 | -env JUJU_ZOOKEEPER=%(zookeeper)s |
204 | +env PYTHONPATH={{juju_dir}}:$PYTHONPATH |
205 | +env JUJU_ZOOKEEPER={{zookeeper}} |
206 | |
207 | # Use --nodaemon so that upstart can correctly retrieve the process ID. |
208 | -exec /usr/bin/python -m juju.agents.api --nodaemon --port %(port)s \ |
209 | +exec /usr/bin/python -m juju.agents.api --nodaemon --port {{port}} \ |
210 | --logfile /var/log/juju/api-agent.log \ |
211 | --session-file /var/run/juju/api-agent.zksession \ |
212 | - --secure --keys %(keys)s |
213 | + --secure --keys {{keys}} |
214 | |
215 | === modified file 'config/juju-api-improv.conf.template' |
216 | --- config/juju-api-improv.conf.template 2013-01-03 15:02:38 +0000 |
217 | +++ config/juju-api-improv.conf.template 2013-03-13 22:47:20 +0000 |
218 | @@ -5,8 +5,8 @@ |
219 | stop on runlevel [!2345] |
220 | |
221 | setuid ubuntu |
222 | -env PYTHONPATH=%(juju_dir)s:$PYTHONPATH |
223 | +env PYTHONPATH={{juju_dir}}:$PYTHONPATH |
224 | |
225 | -exec /usr/bin/python %(juju_dir)s/improv.py --port %(port)s \ |
226 | - -f %(juju_dir)s/%(staging_env)s.json \ |
227 | - --secure --keys %(keys)s |
228 | +exec /usr/bin/python {{juju_dir}}/improv.py --port {{port}} \ |
229 | + -f {{juju_dir}}/{{staging_env}}.json \ |
230 | + --secure --keys {{keys}} |
231 | |
232 | === renamed file 'config/nginx.conf.template' => 'config/nginx-site.template' |
233 | --- config/nginx.conf.template 2013-01-03 14:17:21 +0000 |
234 | +++ config/nginx-site.template 2013-03-13 22:47:20 +0000 |
235 | @@ -1,29 +1,33 @@ |
236 | server { |
237 | - listen 80; |
238 | - server_name _; |
239 | - return 301 https://$host$request_uri; |
240 | -} |
241 | - |
242 | -server { |
243 | - listen 443 default_server ssl; |
244 | - server_name _; |
245 | - root %(server_root)s; |
246 | + listen 127.0.0.1:{{port}} default_server; |
247 | + server_name _; |
248 | + root {{server_root}}; |
249 | index index.html; |
250 | - ssl_certificate %(ssl_cert_path)s/juju.crt; |
251 | - ssl_certificate_key %(ssl_cert_path)s/juju.key; |
252 | |
253 | # Serve static assets. |
254 | location ^~ /juju-ui/ { |
255 | - root %(server_root)s; |
256 | + root {{server_root}}; |
257 | } |
258 | location = /favicon.ico { |
259 | expires 10d; |
260 | - root %(server_root)s; |
261 | + root {{server_root}}; |
262 | } |
263 | location = /index.html { |
264 | - root %(server_root)s; |
265 | + root {{server_root}}; |
266 | } |
267 | |
268 | + {{if serve_tests}} |
269 | + # Serve unit tests. |
270 | + location ^~ /test/ { |
271 | + alias {{tests_root}}; |
272 | + } |
273 | + {{else}} |
274 | + # Redirect to site root so that the test URL does not directly |
275 | + # reference the manifest file. This avoids automatically adding the URL |
276 | + # to the application cache. |
277 | + rewrite ^/test/ $scheme://$host/ redirect; |
278 | + {{endif}} |
279 | + |
280 | # Fall through to the single-page app for all other URLs. |
281 | location ~ / { |
282 | rewrite .* /index.html last; |
283 | |
284 | === renamed file 'config/juju-gui.conf.template' => 'config/nginx.conf' |
285 | --- config/juju-gui.conf.template 2012-12-12 13:23:56 +0000 |
286 | +++ config/nginx.conf 2013-03-13 22:47:20 +0000 |
287 | @@ -1,4 +1,4 @@ |
288 | -description "Juju GUI" |
289 | +description "nginx" |
290 | author "Canonical" |
291 | # Original author: George Shammas <georgyo@gmail.com>. |
292 | # See http://wiki.nginx.org/Upstart. |
293 | |
294 | === modified file 'hooks/config-changed' |
295 | --- hooks/config-changed 2013-01-23 16:41:38 +0000 |
296 | +++ hooks/config-changed 2013-03-13 22:47:20 +0000 |
297 | @@ -4,14 +4,11 @@ |
298 | # Copyright 2012 Canonical Ltd. This software is licensed under the |
299 | # GNU Affero General Public License version 3 (see the file LICENSE). |
300 | |
301 | -from subprocess import CalledProcessError |
302 | import sys |
303 | |
304 | from charmhelpers import ( |
305 | get_config, |
306 | log, |
307 | - log_entry, |
308 | - log_exit, |
309 | service_control, |
310 | STOP, |
311 | ) |
312 | @@ -25,8 +22,11 @@ |
313 | config_json, |
314 | fetch_api, |
315 | fetch_gui, |
316 | - GUI, |
317 | + get_staging_dependencies, |
318 | + HAPROXY, |
319 | IMPROV, |
320 | + log_hook, |
321 | + NGINX, |
322 | save_or_create_certificates, |
323 | setup_gui, |
324 | start_agent, |
325 | @@ -40,7 +40,6 @@ |
326 | log('Updating configuration.') |
327 | |
328 | added_or_changed = diff.added_or_changed |
329 | - juju_api_port = config.get('juju-api-port') |
330 | in_staging = config.get('staging') |
331 | |
332 | # The juju_gui_source_changed and juju_api_branch_changed variables |
333 | @@ -70,14 +69,15 @@ |
334 | |
335 | # Handle changes to the improv server configuration. |
336 | if in_staging: |
337 | - staging_properties = set( |
338 | - ['staging', 'staging-environment', 'juju-api-port']) |
339 | + staging_properties = set(['staging', 'staging-environment']) |
340 | staging_changed = added_or_changed & staging_properties |
341 | if staging_changed or ssl_changed or juju_api_branch_changed: |
342 | if 'staging' in added_or_changed: |
343 | # 'staging' went from False to True, so the agent server is |
344 | # running and must be stopped. |
345 | current_api = AGENT |
346 | + # We need to make sure we have staging dependencies. |
347 | + get_staging_dependencies() |
348 | else: |
349 | # Only staging parameters changed, so the existing staging |
350 | # server must be stopped and later restarted. |
351 | @@ -86,13 +86,12 @@ |
352 | service_control(current_api, STOP) |
353 | # Now the improv server can be cleanly started. |
354 | log('Starting or restarting staging.') |
355 | - start_improv(juju_api_port, config.get('staging-environment'), |
356 | + start_improv(config.get('staging-environment'), |
357 | config['ssl-cert-path']) |
358 | else: |
359 | - agent_properties = set(['juju-api-port', 'staging']) |
360 | - agent_changed = added_or_changed & agent_properties |
361 | + agent_changed = 'staging' in added_or_changed |
362 | if agent_changed or ssl_changed or juju_api_branch_changed: |
363 | - if 'staging' in added_or_changed: |
364 | + if agent_changed: |
365 | # If 'staging' transitions to False we need to stop the backend |
366 | # and start the agent. |
367 | current_api = IMPROV |
368 | @@ -102,22 +101,26 @@ |
369 | current_api = AGENT |
370 | service_control(current_api, STOP) |
371 | log('Starting or restarting Juju API agent.') |
372 | - start_agent(juju_api_port, config['ssl-cert-path']) |
373 | + start_agent(config['ssl-cert-path']) |
374 | |
375 | # Handle changes to the juju-gui configuration. |
376 | gui_properties = set([ |
377 | - 'juju-api-port', 'juju-gui-console-enabled', 'login-help', 'read-only', |
378 | - 'staging']) |
379 | + 'juju-gui-console-enabled', 'login-help', 'read-only', 'serve-tests', |
380 | + 'staging', 'secure']) |
381 | gui_changed = added_or_changed & gui_properties |
382 | if gui_changed or ssl_changed or juju_gui_source_changed: |
383 | with su('root'): |
384 | - service_control(GUI, STOP) |
385 | + service_control(HAPROXY, STOP) |
386 | + service_control(NGINX, STOP) |
387 | console_enabled = config.get('juju-gui-console-enabled') |
388 | login_help = config['login-help'] |
389 | readonly = config['read-only'] |
390 | + serve_tests = config['serve-tests'] |
391 | ssl_cert_path = config['ssl-cert-path'] |
392 | - start_gui(juju_api_port, console_enabled, login_help, readonly, |
393 | - in_staging, ssl_cert_path) |
394 | + secure = config['secure'] |
395 | + start_gui( |
396 | + console_enabled, login_help, readonly, in_staging, ssl_cert_path, |
397 | + serve_tests, secure=secure) |
398 | |
399 | |
400 | def main(): |
401 | @@ -133,12 +136,5 @@ |
402 | |
403 | |
404 | if __name__ == '__main__': |
405 | - log_entry() |
406 | - try: |
407 | + with log_hook(): |
408 | main() |
409 | - except CalledProcessError as e: |
410 | - log('Exception caught:') |
411 | - log(e.output) |
412 | - raise |
413 | - finally: |
414 | - log_exit() |
415 | |
416 | === modified file 'hooks/install' |
417 | --- hooks/install 2013-01-23 16:11:16 +0000 |
418 | +++ hooks/install 2013-03-13 22:47:20 +0000 |
419 | @@ -1,10 +1,10 @@ |
420 | #!/usr/bin/env python2 |
421 | # -*- python -*- |
422 | |
423 | -from subprocess import ( |
424 | - CalledProcessError, |
425 | - check_call, |
426 | -) |
427 | +import os |
428 | +import shutil |
429 | +from subprocess import check_call |
430 | + |
431 | |
432 | # If the user's environment has "juju-origin: ppa" set, they will |
433 | # automatically have access to python-charmhelpers and python-shelltoolbox. |
434 | @@ -14,17 +14,20 @@ |
435 | import bootstrap_utils |
436 | bootstrap_utils.install_extra_repositories('ppa:juju/pkgs') |
437 | |
438 | -# python-shelltoolbox is installed as a dependency of python-charmhelpers. |
439 | -check_call(['apt-get', 'install', '-y', 'python-charmhelpers']) |
440 | - |
441 | - |
442 | -# These modules depend on charmhelpers and shelltoolbox being installed so they |
443 | +# Python dependencies must be installed here so that the charm can import and |
444 | +# use required libraries. |
445 | +PYTHON_DEPENDENCIES = ( |
446 | + 'python-charmhelpers', 'python-launchpadlib', 'python-shelltoolbox', |
447 | + 'python-tempita', |
448 | +) |
449 | +check_call(('apt-get', 'install', '-y') + PYTHON_DEPENDENCIES) |
450 | + |
451 | + |
452 | +# These modules depend on the Python dependencies above being installed so they |
453 | # must not be imported until those packages are available. |
454 | from charmhelpers import ( |
455 | get_config, |
456 | log, |
457 | - log_entry, |
458 | - log_exit, |
459 | ) |
460 | from shelltoolbox import ( |
461 | apt_get_install, |
462 | @@ -36,27 +39,41 @@ |
463 | config_json, |
464 | fetch_api, |
465 | fetch_gui, |
466 | + log_hook, |
467 | + get_staging_dependencies, |
468 | save_or_create_certificates, |
469 | setup_gui, |
470 | setup_nginx, |
471 | ) |
472 | |
473 | |
474 | +CONFIG_FILES = ('haproxy.conf', 'nginx.conf') |
475 | +REPOSITORIES = ('ppa:juju-gui/ppa',) |
476 | DEB_DEPENDENCIES = ( |
477 | - 'bzr', 'curl', 'imagemagick', 'make', 'nginx', 'nodejs', 'npm', 'openssl', |
478 | - 'python-launchpadlib', 'zookeeper', |
479 | + 'curl', 'haproxy', 'nginx', 'openssl', |
480 | ) |
481 | |
482 | |
483 | def get_dependencies(): |
484 | + """Install deb dependencies.""" |
485 | log('Installing dependencies.') |
486 | - cmd_log(install_extra_repositories('ppa:chris-lea/node.js')) |
487 | + cmd_log(install_extra_repositories(*REPOSITORIES)) |
488 | cmd_log(apt_get_install(*DEB_DEPENDENCIES)) |
489 | |
490 | |
491 | +def setup_services(): |
492 | + """Set up haproxy and nginx upstart configuration files.""" |
493 | + log('Setting up haproxy and nginx start up scripts.') |
494 | + source_dir = os.path.join(os.path.dirname(__file__), '..', 'config') |
495 | + for config_file in CONFIG_FILES: |
496 | + shutil.copy(os.path.join(source_dir, config_file), '/etc/init/') |
497 | + |
498 | + |
499 | def main(): |
500 | config = get_config() |
501 | get_dependencies() |
502 | + if config.get('staging'): |
503 | + get_staging_dependencies() |
504 | release_tarball = fetch_gui( |
505 | config['juju-gui-source'], config['command-log-file']) |
506 | setup_gui(release_tarball) |
507 | @@ -65,16 +82,10 @@ |
508 | config['ssl-cert-path'], config.get('ssl-cert-contents'), |
509 | config.get('ssl-key-contents')) |
510 | fetch_api(config['juju-api-branch']) |
511 | + setup_services() |
512 | config_json.set(config) |
513 | |
514 | |
515 | if __name__ == '__main__': |
516 | - log_entry() |
517 | - try: |
518 | + with log_hook(): |
519 | main() |
520 | - except CalledProcessError as e: |
521 | - log('Exception caught:') |
522 | - log(e.output) |
523 | - raise |
524 | - finally: |
525 | - log_exit() |
526 | |
527 | === modified file 'hooks/start' |
528 | --- hooks/start 2013-01-23 16:41:38 +0000 |
529 | +++ hooks/start 2013-03-13 22:47:20 +0000 |
530 | @@ -4,47 +4,38 @@ |
531 | from charmhelpers import ( |
532 | get_config, |
533 | log, |
534 | - log_entry, |
535 | - log_exit, |
536 | open_port, |
537 | ) |
538 | |
539 | from utils import ( |
540 | + log_hook, |
541 | start_agent, |
542 | start_gui, |
543 | start_improv, |
544 | ) |
545 | |
546 | |
547 | -def open_ports(juju_api_port): |
548 | - """Expose Juju GUI web server and websocket server ports.""" |
549 | +def open_ports(): |
550 | + """Expose Juju GUI web server (HTTP and HTTPS) ports.""" |
551 | log('Exposing services.') |
552 | - # Open the Juju GUI web server HTTP and HTTPS ports. |
553 | open_port(80) |
554 | open_port(443) |
555 | - # Open the Juju websocket server port. |
556 | - open_port(juju_api_port) |
557 | |
558 | |
559 | def main(): |
560 | config = get_config() |
561 | - juju_api_port = config['juju-api-port'] |
562 | staging = config.get('staging') |
563 | - start_gui( |
564 | - juju_api_port, config['juju-gui-console-enabled'], |
565 | - config['login-help'], config['read-only'], staging, |
566 | - config['ssl-cert-path']) |
567 | if staging: |
568 | - start_improv(juju_api_port, config['staging-environment'], |
569 | - config['ssl-cert-path']) |
570 | + start_improv(config['staging-environment'], config['ssl-cert-path']) |
571 | else: |
572 | - start_agent(juju_api_port, config['ssl-cert-path']) |
573 | - open_ports(juju_api_port) |
574 | + start_agent(config['ssl-cert-path']) |
575 | + start_gui( |
576 | + config['juju-gui-console-enabled'], config['login-help'], |
577 | + config['read-only'], staging, config['ssl-cert-path'], |
578 | + config['serve-tests'], secure=config['secure']) |
579 | + open_ports() |
580 | |
581 | |
582 | if __name__ == '__main__': |
583 | - log_entry() |
584 | - try: |
585 | + with log_hook(): |
586 | main() |
587 | - finally: |
588 | - log_exit() |
589 | |
590 | === modified file 'hooks/stop' |
591 | --- hooks/stop 2012-12-19 09:57:45 +0000 |
592 | +++ hooks/stop 2013-03-13 22:47:20 +0000 |
593 | @@ -1,14 +1,13 @@ |
594 | #!/usr/bin/env python2 |
595 | #-*- python -*- |
596 | |
597 | -from charmhelpers import ( |
598 | - get_config, |
599 | - log_entry, |
600 | - log_exit, |
601 | +from charmhelpers import get_config |
602 | + |
603 | +from utils import ( |
604 | + log_hook, |
605 | + stop, |
606 | ) |
607 | |
608 | -from utils import stop |
609 | - |
610 | |
611 | def main(): |
612 | config = get_config() |
613 | @@ -16,8 +15,5 @@ |
614 | |
615 | |
616 | if __name__ == '__main__': |
617 | - log_entry() |
618 | - try: |
619 | + with log_hook(): |
620 | main() |
621 | - finally: |
622 | - log_exit() |
623 | |
624 | === modified file 'hooks/utils.py' |
625 | --- hooks/utils.py 2013-01-23 16:41:38 +0000 |
626 | +++ hooks/utils.py 2013-03-13 22:47:20 +0000 |
627 | @@ -2,6 +2,7 @@ |
628 | |
629 | __all__ = [ |
630 | 'AGENT', |
631 | + 'API_PORT', |
632 | 'bzr_checkout', |
633 | 'cmd_log', |
634 | 'CURRENT_DIR', |
635 | @@ -9,11 +10,16 @@ |
636 | 'fetch_gui', |
637 | 'first_path_in_dir', |
638 | 'get_release_file_url', |
639 | + 'get_staging_dependencies', |
640 | 'get_zookeeper_address', |
641 | - 'GUI', |
642 | + 'HAPROXY', |
643 | 'IMPROV', |
644 | 'JUJU_DIR', |
645 | 'JUJU_GUI_DIR', |
646 | + 'JUJU_GUI_SITE', |
647 | + 'JUJU_PEM', |
648 | + 'log_hook', |
649 | + 'NGINX', |
650 | 'parse_source', |
651 | 'render_to_file', |
652 | 'save_or_create_certificates', |
653 | @@ -23,18 +29,26 @@ |
654 | 'start_gui', |
655 | 'start_improv', |
656 | 'stop', |
657 | + 'WEB_PORT', |
658 | ] |
659 | |
660 | +from contextlib import contextmanager |
661 | import json |
662 | import os |
663 | import logging |
664 | +import shutil |
665 | +from subprocess import CalledProcessError |
666 | import tempfile |
667 | +import tempita |
668 | |
669 | from launchpadlib.launchpad import Launchpad |
670 | from shelltoolbox import ( |
671 | + apt_get_install, |
672 | command, |
673 | environ, |
674 | + install_extra_repositories, |
675 | run, |
676 | + script_name, |
677 | search_file, |
678 | Serializer, |
679 | su, |
680 | @@ -51,11 +65,25 @@ |
681 | |
682 | AGENT = 'juju-api-agent' |
683 | IMPROV = 'juju-api-improv' |
684 | -GUI = 'juju-gui' |
685 | +HAPROXY = 'haproxy' |
686 | +NGINX = 'nginx' |
687 | + |
688 | +API_PORT = 8080 |
689 | +WEB_PORT = 8000 |
690 | + |
691 | CURRENT_DIR = os.getcwd() |
692 | JUJU_DIR = os.path.join(CURRENT_DIR, 'juju') |
693 | JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui') |
694 | JUJU_GUI_SITE = '/etc/nginx/sites-available/juju-gui' |
695 | +JUJU_PEM = 'juju.includes-private-key.pem' |
696 | +BUILD_REPOSITORIES = ('ppa:chris-lea/node.js',) |
697 | +DEB_BUILD_DEPENDENCIES = ( |
698 | + 'bzr', 'imagemagick', 'make', 'nodejs', 'npm', |
699 | +) |
700 | +DEB_STAGE_DEPENDENCIES = ( |
701 | + 'zookeeper', |
702 | +) |
703 | + |
704 | |
705 | # Store the configuration from on invocation to the next. |
706 | config_json = Serializer('/tmp/config.json') |
707 | @@ -63,6 +91,19 @@ |
708 | bzr_checkout = command('bzr', 'co', '--lightweight') |
709 | |
710 | |
711 | +def _get_build_dependencies(): |
712 | + """Install deb dependencies for building.""" |
713 | + log('Installing build dependencies.') |
714 | + cmd_log(install_extra_repositories(*BUILD_REPOSITORIES)) |
715 | + cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES)) |
716 | + |
717 | + |
718 | +def get_staging_dependencies(): |
719 | + """Install deb dependencies for the stage (improv) environment.""" |
720 | + log('Installing stage dependencies.') |
721 | + cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES)) |
722 | + |
723 | + |
724 | def first_path_in_dir(directory): |
725 | """Return the full path of the first file/dir in *directory*.""" |
726 | return os.path.join(directory, os.listdir(directory)[0]) |
727 | @@ -118,6 +159,25 @@ |
728 | return line.split('=')[1].strip('"') |
729 | |
730 | |
731 | +@contextmanager |
732 | +def log_hook(): |
733 | + """Log when an hook starts and stops its execution. |
734 | + |
735 | + Also log to stdout possible CalledProcessError exceptions raised executing |
736 | + the hook. |
737 | + """ |
738 | + script = script_name() |
739 | + log(">>> Entering {}".format(script)) |
740 | + try: |
741 | + yield |
742 | + except CalledProcessError as err: |
743 | + log('Exception caught:') |
744 | + log(err.output) |
745 | + raise |
746 | + finally: |
747 | + log("<<< Exiting {}".format(script)) |
748 | + |
749 | + |
750 | def parse_source(source): |
751 | """Parse the ``juju-gui-source`` option. |
752 | |
753 | @@ -138,19 +198,21 @@ |
754 | return 'stable', source |
755 | |
756 | |
757 | -def render_to_file(template, context, destination): |
758 | - """Render the given *template* into *destination* using *context*. |
759 | +def render_to_file(template_name, context, destination): |
760 | + """Render the given *template_name* into *destination* using *context*. |
761 | |
762 | - The arguments *template* is the name or path of the template file: it may |
763 | - be either a path relative to ``../config`` or an absolute path. |
764 | + The tempita template language is used to render contents |
765 | + (see http://pythonpaste.org/tempita/). |
766 | + The argument *template_name* is the name or path of the template file: |
767 | + it may be either a path relative to ``../config`` or an absolute path. |
768 | The argument *destination* is a file path. |
769 | The argument *context* is a dict-like object. |
770 | """ |
771 | template_path = os.path.join( |
772 | - os.path.dirname(__file__), '..', 'config', template) |
773 | - contents = open(template_path).read() |
774 | + os.path.dirname(__file__), '..', 'config', template_name) |
775 | + template = tempita.Template.from_filename(template_path) |
776 | with open(destination, 'w') as stream: |
777 | - stream.write(contents % context) |
778 | + stream.write(template.substitute(context)) |
779 | |
780 | |
781 | results_log = None |
782 | @@ -165,7 +227,7 @@ |
783 | filename=config['command-log-file'], |
784 | level=logging.INFO, |
785 | format="%(asctime)s: %(name)s@%(levelname)s %(message)s") |
786 | - results_log = logging.getLogger(GUI) |
787 | + results_log = logging.getLogger('juju-gui') |
788 | |
789 | |
790 | def cmd_log(results): |
791 | @@ -179,15 +241,15 @@ |
792 | results_log.info('\n' + results) |
793 | |
794 | |
795 | -def start_improv(juju_api_port, staging_env, ssl_cert_path, |
796 | +def start_improv(staging_env, ssl_cert_path, |
797 | config_path='/etc/init/juju-api-improv.conf'): |
798 | """Start a simulated juju environment using ``improv.py``.""" |
799 | log('Setting up staging start up script.') |
800 | context = { |
801 | 'juju_dir': JUJU_DIR, |
802 | - 'port': juju_api_port, |
803 | + 'keys': ssl_cert_path, |
804 | + 'port': API_PORT, |
805 | 'staging_env': staging_env, |
806 | - 'keys': ssl_cert_path, |
807 | } |
808 | render_to_file('juju-api-improv.conf.template', context, config_path) |
809 | log('Starting the staging backend.') |
810 | @@ -195,8 +257,7 @@ |
811 | service_control(IMPROV, START) |
812 | |
813 | |
814 | -def start_agent(juju_api_port, ssl_cert_path, |
815 | - config_path='/etc/init/juju-api-agent.conf'): |
816 | +def start_agent(ssl_cert_path, config_path='/etc/init/juju-api-agent.conf'): |
817 | """Start the Juju agent and connect to the current environment.""" |
818 | # Retrieve the Zookeeper address from the start up script. |
819 | unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..')) |
820 | @@ -205,9 +266,9 @@ |
821 | log('Setting up API agent start up script.') |
822 | context = { |
823 | 'juju_dir': JUJU_DIR, |
824 | - 'port': juju_api_port, |
825 | + 'keys': ssl_cert_path, |
826 | + 'port': API_PORT, |
827 | 'zookeeper': zookeeper, |
828 | - 'keys': ssl_cert_path, |
829 | } |
830 | render_to_file('juju-api-agent.conf.template', context, config_path) |
831 | log('Starting API agent.') |
832 | @@ -216,26 +277,37 @@ |
833 | |
834 | |
835 | def start_gui( |
836 | - juju_api_port, console_enabled, login_help, readonly, in_staging, |
837 | - ssl_cert_path, config_path='/etc/init/juju-gui.conf', |
838 | - nginx_path=JUJU_GUI_SITE, config_js_path=None): |
839 | + console_enabled, login_help, readonly, in_staging, ssl_cert_path, |
840 | + serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg', |
841 | + nginx_path=JUJU_GUI_SITE, config_js_path=None, secure=True): |
842 | """Set up and start the Juju GUI server.""" |
843 | with su('root'): |
844 | run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR) |
845 | - build_dir = JUJU_GUI_DIR + '/build-' |
846 | - build_dir += 'debug' if in_staging else 'prod' |
847 | - log('Setting up Juju GUI start up script.') |
848 | - render_to_file('juju-gui.conf.template', {}, config_path) |
849 | + # XXX 2013-02-05 frankban bug=1116320: |
850 | + # External insecure resources are still loaded when testing in the |
851 | + # debug environment. For now, switch to the production environment if |
852 | + # the charm is configured to serve tests. |
853 | + if in_staging and not serve_tests: |
854 | + build_dirname = 'build-debug' |
855 | + else: |
856 | + build_dirname = 'build-prod' |
857 | + build_dir = os.path.join(JUJU_GUI_DIR, build_dirname) |
858 | log('Generating the Juju GUI configuration file.') |
859 | user, password = ('admin', 'admin') if in_staging else (None, None) |
860 | + if secure: |
861 | + protocol = 'wss' |
862 | + else: |
863 | + log('Running in insecure mode! Port 80 will serve unencrypted.') |
864 | + protocol = 'ws' |
865 | context = { |
866 | + 'raw_protocol': protocol, |
867 | 'address': unit_get('public-address'), |
868 | 'console_enabled': json.dumps(console_enabled), |
869 | 'login_help': json.dumps(login_help), |
870 | 'password': json.dumps(password), |
871 | - 'port': juju_api_port, |
872 | 'readonly': json.dumps(readonly), |
873 | 'user': json.dumps(user), |
874 | + 'protocol': json.dumps(protocol) |
875 | } |
876 | if config_js_path is None: |
877 | config_js_path = os.path.join( |
878 | @@ -243,21 +315,37 @@ |
879 | render_to_file('config.js.template', context, config_js_path) |
880 | log('Generating the nginx site configuration file.') |
881 | context = { |
882 | + 'port': WEB_PORT, |
883 | + 'serve_tests': serve_tests, |
884 | 'server_root': build_dir, |
885 | - 'ssl_cert_path': ssl_cert_path.rstrip('/'), |
886 | - } |
887 | - render_to_file('nginx.conf.template', context, nginx_path) |
888 | + 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''), |
889 | + } |
890 | + render_to_file('nginx-site.template', context, nginx_path) |
891 | + log('Generating haproxy configuration file.') |
892 | + context = { |
893 | + 'api_pem': JUJU_PEM, |
894 | + 'api_port': API_PORT, |
895 | + 'ssl_cert_path': ssl_cert_path, |
896 | + # Use the same certificate for both HTTPS and Websocket connections. |
897 | + # In the long term, we want separate certs to be used here. |
898 | + 'web_pem': JUJU_PEM, |
899 | + 'web_port': WEB_PORT, |
900 | + 'secure': secure |
901 | + } |
902 | + render_to_file('haproxy.cfg.template', context, haproxy_path) |
903 | log('Starting Juju GUI.') |
904 | with su('root'): |
905 | # Start the Juju GUI. |
906 | - service_control(GUI, START) |
907 | + service_control(NGINX, START) |
908 | + service_control(HAPROXY, START) |
909 | |
910 | |
911 | def stop(in_staging): |
912 | """Stop the Juju API agent.""" |
913 | with su('root'): |
914 | log('Stopping Juju GUI.') |
915 | - service_control(GUI, STOP) |
916 | + service_control(HAPROXY, STOP) |
917 | + service_control(NGINX, STOP) |
918 | if in_staging: |
919 | log('Stopping the staging backend.') |
920 | service_control(IMPROV, STOP) |
921 | @@ -271,6 +359,9 @@ |
922 | # Retrieve a Juju GUI release. |
923 | origin, version_or_branch = parse_source(juju_gui_source) |
924 | if origin == 'branch': |
925 | + # Make sure we have the dependencies necessary for us to actually make |
926 | + # a build. |
927 | + _get_build_dependencies() |
928 | # Create a release starting from a branch. |
929 | juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source') |
930 | log('Retrieving Juju GUI source checkout from %s.' % version_or_branch) |
931 | @@ -279,7 +370,7 @@ |
932 | log('Preparing a Juju GUI release.') |
933 | logdir = os.path.dirname(logpath) |
934 | fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir) |
935 | - log('Output from "make distfile" sent to', name) |
936 | + log('Output from "make distfile" sent to %s' % name) |
937 | with environ(NO_BZR='1'): |
938 | run('make', '-C', juju_gui_source_dir, 'distfile', |
939 | stdout=fd, stderr=fd) |
940 | @@ -338,6 +429,9 @@ |
941 | |
942 | If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them |
943 | as certificates; otherwise, generate them. |
944 | + |
945 | + Also create a pem file, suitable for use in the haproxy configuration, |
946 | + concatenating the key and the certificate files. |
947 | """ |
948 | crt_path = os.path.join(ssl_cert_path, 'juju.crt') |
949 | key_path = os.path.join(ssl_cert_path, 'juju.key') |
950 | @@ -358,3 +452,10 @@ |
951 | # These are arbitrary test values for the certificate. |
952 | '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com', |
953 | '-keyout', key_path, '-out', crt_path)) |
954 | + # Generate the pem file. |
955 | + pem_path = os.path.join(ssl_cert_path, JUJU_PEM) |
956 | + if os.path.exists(pem_path): |
957 | + os.remove(pem_path) |
958 | + with open(pem_path, 'w') as pem_file: |
959 | + shutil.copyfileobj(open(key_path), pem_file) |
960 | + shutil.copyfileobj(open(crt_path), pem_file) |
961 | |
962 | === modified file 'revision' |
963 | --- revision 2013-01-23 16:41:57 +0000 |
964 | +++ revision 2013-03-13 22:47:20 +0000 |
965 | @@ -1,1 +1,1 @@ |
966 | -25 |
967 | +29 |
968 | |
969 | === modified file 'tests/deploy.test' |
970 | --- tests/deploy.test 2013-01-22 11:40:17 +0000 |
971 | +++ tests/deploy.test 2013-03-13 22:47:20 +0000 |
972 | @@ -6,7 +6,6 @@ |
973 | import urlparse |
974 | |
975 | from charmhelpers import make_charm_config_file |
976 | -from selenium.common import exceptions |
977 | from selenium.webdriver import Firefox |
978 | from selenium.webdriver.support import ui |
979 | from shelltoolbox import command |
980 | @@ -33,16 +32,15 @@ |
981 | # Create a Selenium browser instance. |
982 | selenium = self.selenium = Firefox() |
983 | self.addCleanup(selenium.quit) |
984 | - self.wait = ui.WebDriverWait(selenium, 20) |
985 | |
986 | def tearDown(self): |
987 | juju('destroy-service', self.charm) |
988 | |
989 | def assertEnvironmentIsConnected(self): |
990 | """Assert the GUI environment is connected to the Juju API agent.""" |
991 | - def connected(driver): |
992 | - return driver.execute_script('return app.env.get("connected");') |
993 | - self.wait.until(connected, 'Environment not connected.') |
994 | + self.wait_for_script( |
995 | + 'return app.env.get("connected");', |
996 | + error='Environment not connected.') |
997 | |
998 | def make_config_file(self, options=None): |
999 | """Create a charm config file adding, if required, the Juju GUI source. |
1000 | @@ -59,6 +57,19 @@ |
1001 | options.setdefault('juju-gui-source', JUJU_GUI_SOURCE) |
1002 | return make_charm_config_file({self.charm: options}) |
1003 | |
1004 | + def handle_browser_warning(self): |
1005 | + """Overstep the browser warning dialog if required.""" |
1006 | + self.wait_for_script( |
1007 | + 'return window.isBrowserSupported', |
1008 | + error='Function isBrowserSupported not found.') |
1009 | + script = 'return window.isBrowserSupported(navigator.userAgent)' |
1010 | + supported = self.selenium.execute_script(script) |
1011 | + if not supported: |
1012 | + continue_button = self.wait_for_css_selector( |
1013 | + '#browser-warning input', |
1014 | + error='Browser warning dialog not found.') |
1015 | + continue_button.click() |
1016 | + |
1017 | def navigate_to(self, hostname, path='/'): |
1018 | """Load a page using the current Selenium driver. |
1019 | |
1020 | @@ -72,16 +83,41 @@ |
1021 | def page_ready(driver): |
1022 | driver.get(url) |
1023 | return driver.title == 'Juju Admin' |
1024 | - self.wait.until(page_ready, 'Juju GUI not found.') |
1025 | + self.wait_for(page_ready, error='Juju GUI not found.') |
1026 | + |
1027 | + def wait_for(self, condition, error=None, timeout=20): |
1028 | + """Wait for condition to be True. |
1029 | + |
1030 | + The argument condition is a callable accepting a driver object. |
1031 | + Fail printing the provided error if timeout is exceeded. |
1032 | + Otherwise, return the value returned by the condition call. |
1033 | + """ |
1034 | + wait = ui.WebDriverWait(self.selenium, timeout) |
1035 | + return wait.until(condition, error) |
1036 | + |
1037 | + def wait_for_css_selector(self, selector, error=None, timeout=20): |
1038 | + """Wait until the provided CSS selector is found. |
1039 | + |
1040 | + Fail printing the provided error if timeout is exceeded. |
1041 | + Otherwise, return the value returned by the script. |
1042 | + """ |
1043 | + condition = lambda driver: driver.find_elements_by_css_selector( |
1044 | + selector) |
1045 | + elements = self.wait_for(condition, error=error, timeout=timeout) |
1046 | + return elements[0] |
1047 | + |
1048 | + def wait_for_script(self, script, error=None, timeout=20): |
1049 | + """Wait for the given JavaScript snippet to return a True value. |
1050 | + |
1051 | + Fail printing the provided error if timeout is exceeded. |
1052 | + Otherwise, return the value returned by the script. |
1053 | + """ |
1054 | + condition = lambda driver: driver.execute_script(script) |
1055 | + return self.wait_for(condition, error=error, timeout=timeout) |
1056 | |
1057 | def login(self, password): |
1058 | """Log in to access the Juju GUI using the provided *password*.""" |
1059 | - def form_displayed(driver): |
1060 | - try: |
1061 | - return driver.find_element_by_css_selector('form') |
1062 | - except exceptions.NoSuchElementException: |
1063 | - return False |
1064 | - form = self.wait.until(form_displayed, 'Login form not found.') |
1065 | + form = self.wait_for_css_selector('form', 'Login form not found.') |
1066 | passwd = form.find_element_by_css_selector('input[type=password]') |
1067 | passwd.send_keys(password) |
1068 | submit = form.find_element_by_css_selector('input[type=submit]') |
1069 | @@ -91,7 +127,7 @@ |
1070 | """Return the set of services' names displayed in the current page.""" |
1071 | def services_found(driver): |
1072 | return driver.find_elements_by_css_selector('.service .name') |
1073 | - services = self.wait.until(services_found, 'Services not displayed.') |
1074 | + services = self.wait_for(services_found, 'Services not displayed.') |
1075 | return set([element.text for element in services]) |
1076 | |
1077 | def stop_services(self, hostname, services): |
1078 | @@ -129,30 +165,21 @@ |
1079 | hostname = self.deploy() |
1080 | # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
1081 | self.addCleanup( |
1082 | - self.stop_services, hostname, ['juju-api-agent', 'juju-gui']) |
1083 | - self.navigate_to(hostname) |
1084 | - self.assertEnvironmentIsConnected() |
1085 | - |
1086 | - def test_customized_api_port(self): |
1087 | - # It is possible to customize the port used by the websocket server. |
1088 | - api_port = 8081 |
1089 | - hostname = self.deploy({'juju-api-port': api_port}) |
1090 | - # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
1091 | - self.addCleanup( |
1092 | - self.stop_services, hostname, ['juju-api-agent', 'juju-gui']) |
1093 | - self.navigate_to(hostname) |
1094 | - self.assertEnvironmentIsConnected() |
1095 | - ws_url = self.selenium.execute_script( |
1096 | - 'return app.env.get("socket_url");') |
1097 | - self.assertIn(str(api_port), ws_url) |
1098 | + self.stop_services, |
1099 | + hostname, ['haproxy', 'nginx', 'juju-api-agent']) |
1100 | + self.navigate_to(hostname) |
1101 | + self.handle_browser_warning() |
1102 | + self.assertEnvironmentIsConnected() |
1103 | |
1104 | def test_staging(self): |
1105 | # Ensure the Juju GUI and improv services are correctly set up. |
1106 | hostname = self.deploy({'staging': 'true'}) |
1107 | # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
1108 | self.addCleanup( |
1109 | - self.stop_services, hostname, ['juju-api-improv', 'juju-gui']) |
1110 | + self.stop_services, |
1111 | + hostname, ['haproxy', 'nginx', 'juju-api-improv']) |
1112 | self.navigate_to(hostname) |
1113 | + self.handle_browser_warning() |
1114 | self.assertEnvironmentIsConnected() |
1115 | # The staging environment contains five deployed services. |
1116 | self.assertSetEqual(set(STAGING_SERVICES), self.get_service_names()) |
1117 | @@ -162,8 +189,10 @@ |
1118 | hostname = self.deploy({'juju-gui-source': JUJU_GUI_TEST_BRANCH}) |
1119 | # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
1120 | self.addCleanup( |
1121 | - self.stop_services, hostname, ['juju-api-agent', 'juju-gui']) |
1122 | + self.stop_services, |
1123 | + hostname, ['haproxy', 'nginx', 'juju-api-agent']) |
1124 | self.navigate_to(hostname) |
1125 | + self.handle_browser_warning() |
1126 | self.assertEnvironmentIsConnected() |
1127 | |
1128 | |
1129 | @@ -192,8 +221,10 @@ |
1130 | hostname = self.deploy_to() |
1131 | # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. |
1132 | self.addCleanup( |
1133 | - self.stop_services, hostname, ['juju-api-agent', 'juju-gui']) |
1134 | + self.stop_services, |
1135 | + hostname, ['haproxy', 'nginx', 'juju-api-agent']) |
1136 | self.navigate_to(hostname) |
1137 | + self.handle_browser_warning() |
1138 | self.assertEnvironmentIsConnected() |
1139 | |
1140 | |
1141 | |
1142 | === modified file 'tests/test_utils.py' |
1143 | --- tests/test_utils.py 2013-01-23 16:41:38 +0000 |
1144 | +++ tests/test_utils.py 2013-03-13 22:47:20 +0000 |
1145 | @@ -4,16 +4,23 @@ |
1146 | import os |
1147 | import shutil |
1148 | from simplejson import dumps |
1149 | +from subprocess import CalledProcessError |
1150 | import tempfile |
1151 | +import tempita |
1152 | import unittest |
1153 | |
1154 | import charmhelpers |
1155 | + |
1156 | from utils import ( |
1157 | _get_by_attr, |
1158 | + API_PORT, |
1159 | cmd_log, |
1160 | first_path_in_dir, |
1161 | get_release_file_url, |
1162 | get_zookeeper_address, |
1163 | + JUJU_GUI_DIR, |
1164 | + JUJU_PEM, |
1165 | + log_hook, |
1166 | parse_source, |
1167 | render_to_file, |
1168 | save_or_create_certificates, |
1169 | @@ -21,6 +28,7 @@ |
1170 | start_gui, |
1171 | start_improv, |
1172 | stop, |
1173 | + WEB_PORT, |
1174 | ) |
1175 | # Import the whole utils package for monkey patching. |
1176 | import utils |
1177 | @@ -261,6 +269,49 @@ |
1178 | self.assertEqual(self.zookeeper_address, address) |
1179 | |
1180 | |
1181 | +class LogHookTest(unittest.TestCase): |
1182 | + |
1183 | + def setUp(self): |
1184 | + # Monkeypatch the charmhelpers log function. |
1185 | + self.output = [] |
1186 | + self.original = utils.log |
1187 | + utils.log = self.output.append |
1188 | + |
1189 | + def tearDown(self): |
1190 | + # Restore the original charmhelpers log function. |
1191 | + utils.log = self.original |
1192 | + |
1193 | + def test_logging(self): |
1194 | + # The function emits log messages on entering and exiting the hook. |
1195 | + with log_hook(): |
1196 | + self.output.append('executing hook') |
1197 | + self.assertEqual(3, len(self.output)) |
1198 | + enter_message, executing_message, exit_message = self.output |
1199 | + self.assertIn('>>> Entering', enter_message) |
1200 | + self.assertEqual('executing hook', executing_message) |
1201 | + self.assertIn('<<< Exiting', exit_message) |
1202 | + |
1203 | + def test_subprocess_error(self): |
1204 | + # If a CalledProcessError exception is raised, the command output is |
1205 | + # logged. |
1206 | + with self.assertRaises(CalledProcessError) as cm: |
1207 | + with log_hook(): |
1208 | + raise CalledProcessError(2, 'command', 'output') |
1209 | + exception = cm.exception |
1210 | + self.assertIsInstance(exception, CalledProcessError) |
1211 | + self.assertEqual(2, exception.returncode) |
1212 | + self.assertEqual('output', self.output[-2]) |
1213 | + |
1214 | + def test_error(self): |
1215 | + # Possible errors are re-raised by the context manager. |
1216 | + with self.assertRaises(TypeError) as cm: |
1217 | + with log_hook(): |
1218 | + raise TypeError |
1219 | + exception = cm.exception |
1220 | + self.assertIsInstance(exception, TypeError) |
1221 | + self.assertIn('<<< Exiting', self.output[-1]) |
1222 | + |
1223 | + |
1224 | class ParseSourceTest(unittest.TestCase): |
1225 | |
1226 | def test_latest_stable_release(self): |
1227 | @@ -295,9 +346,9 @@ |
1228 | def setUp(self): |
1229 | self.destination_file = tempfile.NamedTemporaryFile() |
1230 | self.addCleanup(self.destination_file.close) |
1231 | - self.template_contents = '%(foo)s, %(bar)s' |
1232 | + self.template = tempita.Template('{{foo}}, {{bar}}') |
1233 | with tempfile.NamedTemporaryFile(delete=False) as template_file: |
1234 | - template_file.write(self.template_contents) |
1235 | + template_file.write(self.template.content) |
1236 | self.template_path = template_file.name |
1237 | self.addCleanup(os.remove, self.template_path) |
1238 | |
1239 | @@ -305,7 +356,7 @@ |
1240 | # Ensure the template is correctly rendered using the given context. |
1241 | context = {'foo': 'spam', 'bar': 'eggs'} |
1242 | render_to_file(self.template_path, context, self.destination_file.name) |
1243 | - expected = self.template_contents % context |
1244 | + expected = self.template.substitute(context) |
1245 | self.assertEqual(expected, self.destination_file.read()) |
1246 | |
1247 | |
1248 | @@ -331,6 +382,12 @@ |
1249 | self.assertIn('mycert', open(self.cert_file).read()) |
1250 | self.assertIn('mykey', open(self.key_file).read()) |
1251 | |
1252 | + def test_pem_file(self): |
1253 | + # Ensure the pem file is created concatenating the key and cert files. |
1254 | + save_or_create_certificates(self.cert_path, 'Certificate', 'Key') |
1255 | + pem_file = os.path.join(self.cert_path, JUJU_PEM) |
1256 | + self.assertEqual('KeyCertificate', open(pem_file).read()) |
1257 | + |
1258 | |
1259 | class CmdLogTest(unittest.TestCase): |
1260 | def setUp(self): |
1261 | @@ -401,12 +458,11 @@ |
1262 | charmhelpers.command = self.command |
1263 | |
1264 | def test_start_improv(self): |
1265 | - port = '1234' |
1266 | staging_env = 'large' |
1267 | - start_improv(port, staging_env, self.ssl_cert_path, |
1268 | - self.destination_file.name) |
1269 | + start_improv( |
1270 | + staging_env, self.ssl_cert_path, self.destination_file.name) |
1271 | conf = self.destination_file.read() |
1272 | - self.assertTrue('--port %s' % port in conf) |
1273 | + self.assertTrue('--port %s' % API_PORT in conf) |
1274 | self.assertTrue(staging_env + '.json' in conf) |
1275 | self.assertTrue(self.ssl_cert_path in conf) |
1276 | self.assertEqual(self.svc_ctl_call_count, 1) |
1277 | @@ -414,10 +470,9 @@ |
1278 | self.assertEqual(self.actions, [charmhelpers.START]) |
1279 | |
1280 | def test_start_agent(self): |
1281 | - port = '1234' |
1282 | - start_agent(port, self.ssl_cert_path, self.destination_file.name) |
1283 | + start_agent(self.ssl_cert_path, self.destination_file.name) |
1284 | conf = self.destination_file.read() |
1285 | - self.assertTrue('--port %s' % port in conf) |
1286 | + self.assertTrue('--port %s' % API_PORT in conf) |
1287 | self.assertTrue('JUJU_ZOOKEEPER=%s' % self.fake_zk_address in conf) |
1288 | self.assertTrue(self.ssl_cert_path in conf) |
1289 | self.assertEqual(self.svc_ctl_call_count, 1) |
1290 | @@ -425,41 +480,71 @@ |
1291 | self.assertEqual(self.actions, [charmhelpers.START]) |
1292 | |
1293 | def test_start_gui(self): |
1294 | - port = '1234' |
1295 | + config_js_file = self.destination_file |
1296 | + haproxy_file = tempfile.NamedTemporaryFile() |
1297 | + self.addCleanup(haproxy_file.close) |
1298 | nginx_file = tempfile.NamedTemporaryFile() |
1299 | self.addCleanup(nginx_file.close) |
1300 | - config_js_file = tempfile.NamedTemporaryFile() |
1301 | - self.addCleanup(config_js_file.close) |
1302 | + ssl_cert_path = '/tmp/certificates/' |
1303 | start_gui( |
1304 | - port, False, 'This is login help.', True, True, |
1305 | - '/tmp/certificates/', self.destination_file.name, nginx_file.name, |
1306 | - config_js_file.name) |
1307 | - conf = self.destination_file.read() |
1308 | - self.assertTrue('/usr/sbin/nginx' in conf) |
1309 | + False, 'This is login help.', True, True, ssl_cert_path, True, |
1310 | + haproxy_path=haproxy_file.name, nginx_path=nginx_file.name, |
1311 | + config_js_path=config_js_file.name) |
1312 | + self.assertEqual(self.svc_ctl_call_count, 2) |
1313 | + self.assertEqual(self.service_names, ['nginx', 'haproxy']) |
1314 | + self.assertEqual(self.actions, [charmhelpers.START] * 2) |
1315 | + haproxy_conf = haproxy_file.read() |
1316 | + self.assertIn('ca-base {0}'.format(ssl_cert_path), haproxy_conf) |
1317 | + self.assertIn('crt-base {0}'.format(ssl_cert_path), haproxy_conf) |
1318 | + self.assertIn('ws1 127.0.0.1:{0}'.format(API_PORT), haproxy_conf) |
1319 | + self.assertIn('web1 127.0.0.1:{0}'.format(WEB_PORT), haproxy_conf) |
1320 | + self.assertIn('ca-file {0}'.format(JUJU_PEM), haproxy_conf) |
1321 | + self.assertIn('crt {0}'.format(JUJU_PEM), haproxy_conf) |
1322 | + self.assertIn('redirect scheme https', haproxy_conf) |
1323 | js_conf = config_js_file.read() |
1324 | + self.assertIn('consoleEnabled: false', js_conf) |
1325 | self.assertIn('user: "admin"', js_conf) |
1326 | self.assertIn('password: "admin"', js_conf) |
1327 | self.assertIn('login_help: "This is login help."', js_conf) |
1328 | self.assertIn('readOnly: true', js_conf) |
1329 | + self.assertIn("socket_url: 'wss://", js_conf) |
1330 | + self.assertIn('socket_protocol: "wss"', js_conf) |
1331 | nginx_conf = nginx_file.read() |
1332 | - self.assertTrue('juju-gui/build-debug' in nginx_conf) |
1333 | - self.assertIn('/tmp/certificates/juju.crt', nginx_conf) |
1334 | - self.assertIn('/tmp/certificates/juju.key', nginx_conf) |
1335 | - self.assertEqual(self.svc_ctl_call_count, 1) |
1336 | - self.assertEqual(self.service_names, ['juju-gui']) |
1337 | - self.assertEqual(self.actions, [charmhelpers.START]) |
1338 | + self.assertIn('juju-gui/build-', nginx_conf) |
1339 | + self.assertIn('listen 127.0.0.1:{0}'.format(WEB_PORT), nginx_conf) |
1340 | + self.assertIn('alias {0}/test/;'.format(JUJU_GUI_DIR), nginx_conf) |
1341 | + |
1342 | + def test_start_gui_insecure(self): |
1343 | + config_js_file = self.destination_file |
1344 | + haproxy_file = tempfile.NamedTemporaryFile() |
1345 | + self.addCleanup(haproxy_file.close) |
1346 | + nginx_file = tempfile.NamedTemporaryFile() |
1347 | + self.addCleanup(nginx_file.close) |
1348 | + ssl_cert_path = '/tmp/certificates/' |
1349 | + start_gui( |
1350 | + False, 'This is login help.', True, True, ssl_cert_path, True, |
1351 | + haproxy_path=haproxy_file.name, nginx_path=nginx_file.name, |
1352 | + config_js_path=config_js_file.name, secure=False) |
1353 | + js_conf = config_js_file.read() |
1354 | + self.assertIn("socket_url: 'ws://", js_conf) |
1355 | + self.assertIn('socket_protocol: "ws"', js_conf) |
1356 | + haproxy_conf = haproxy_file.read() |
1357 | + # The insecure approach eliminates the https redirect. |
1358 | + self.assertNotIn('redirect scheme https', haproxy_conf) |
1359 | |
1360 | def test_stop_staging(self): |
1361 | stop(True) |
1362 | - self.assertEqual(self.svc_ctl_call_count, 2) |
1363 | - self.assertEqual(self.service_names, ['juju-gui', 'juju-api-improv']) |
1364 | - self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP]) |
1365 | + self.assertEqual(self.svc_ctl_call_count, 3) |
1366 | + self.assertEqual( |
1367 | + self.service_names, ['haproxy', 'nginx', 'juju-api-improv']) |
1368 | + self.assertEqual(self.actions, [charmhelpers.STOP] * 3) |
1369 | |
1370 | def test_stop_production(self): |
1371 | stop(False) |
1372 | - self.assertEqual(self.svc_ctl_call_count, 2) |
1373 | - self.assertEqual(self.service_names, ['juju-gui', 'juju-api-agent']) |
1374 | - self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP]) |
1375 | + self.assertEqual(self.svc_ctl_call_count, 3) |
1376 | + self.assertEqual( |
1377 | + self.service_names, ['haproxy', 'nginx', 'juju-api-agent']) |
1378 | + self.assertEqual(self.actions, [charmhelpers.STOP] * 3) |
1379 | |
1380 | |
1381 | if __name__ == '__main__': |
It appears that these changes get reviewed before they land in lp:~juju-gui/charms/precise/juju-gui/trunk so I'm rubber-stamping this.
This team is a perfect example of a "charm team", and once we verify the process works, we'll make lp:~juju-gui/charms/precise/juju-gui/trunk the "official" lp:charms/juju-gui branch. Then reviews won't block on ~charmers... Anyone in ~juju-gui (or ~charmers) can review/merge/push.