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

Proposed by Gary Poster
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
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://code.launchpad.net/~gz/charms/precise/juju-gui/install_log_error_1152631/+merge/152421

To post a comment you must log in.
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

Revision history for this message
Mark Mims (mark-mims) wrote :

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.

review: Approve
Revision history for this message
Mark Mims (mark-mims) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'HACKING.md'
--- HACKING.md 2013-01-11 17:56:35 +0000
+++ HACKING.md 2013-03-13 22:47:20 +0000
@@ -16,7 +16,7 @@
1616
17You'll also need some dependencies and developer basics.17You'll also need some dependencies and developer basics.
1818
19 sudo apt-get install bzr autoconf libtool python-charmhelpers19 sudo apt-get install bzr autoconf libtool python-charmhelpers xvfb
2020
21Next, you need the bzr branch. We work from21Next, you need the bzr branch. We work from
22[lp:~juju-gui/charms/precise/juju-gui/trunk](https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk).22[lp:~juju-gui/charms/precise/juju-gui/trunk](https://code.launchpad.net/~juju-gui/charms/precise/juju-gui/trunk).
@@ -29,7 +29,7 @@
29locally. The files may be installed globally, or into your home directory (as29locally. The files may be installed globally, or into your home directory (as
30here):30here):
3131
32 sudo apt-get install autoconf libtool python-charmhelpers32 sudo apt-get install autoconf libtool python-charmhelpers python-tempita
33 bzr branch lp:~jimbaker/juju-jitsu/unit-test jitsu-unit-test33 bzr branch lp:~jimbaker/juju-jitsu/unit-test jitsu-unit-test
34 cd jitsu-unit-test34 cd jitsu-unit-test
35 autoreconf35 autoreconf
3636
=== modified file 'README.md'
--- README.md 2013-01-24 16:37:54 +0000
+++ README.md 2013-03-13 22:47:20 +0000
@@ -6,8 +6,7 @@
66
7The Juju GUI supports recent releases of Chrome and Chromium. Recent Firefox7The Juju GUI supports recent releases of Chrome and Chromium. Recent Firefox
8releases are also supported, but regressions may occur until the completion of8releases are also supported, but regressions may occur until the completion of
9upcoming continuous integration work. Internet Explorer 10 will be supported9upcoming continuous integration work.
10soon.
1110
12## Demo/Staging Server ##11## Demo/Staging Server ##
1312
@@ -50,7 +49,6 @@
50 open-ports:49 open-ports:
51 - 80/tcp50 - 80/tcp
52 - 443/tcp51 - 443/tcp
53 - 8080/tcp
54 public-address: ec2-www-xxx-yyy-zzz.compute-1.amazonaws.com52 public-address: ec2-www-xxx-yyy-zzz.compute-1.amazonaws.com
5553
56That tells me I can go to the public-address in my browser via HTTPS54That tells me I can go to the public-address in my browser via HTTPS
@@ -58,10 +56,8 @@
58start configuring the rest of Juju with the GUI. You should see a similar56start configuring the rest of Juju with the GUI. You should see a similar
59web address. Accessing the GUI via HTTP will redirect to using HTTPS.57web address. Accessing the GUI via HTTP will redirect to using HTTPS.
6058
61Note that currently, when using Firefox with self-signed certificates, you need59By default, the deployment uses self-signed certificates. The browser will ask
62to first manually add a TLS certificate exception for the Juju API port60you to accept a security exception once.
63(default is 8080, see `juju-api-port` in `config.yaml`), and only then do the
64same for the main Juju GUI HTTPS connection on port 443.
6561
66You will see a login form with the username fixed to "admin". The password is62You will see a login form with the username fixed to "admin". The password is
67the same as your Juju environment's `admin-secret`, found in63the same as your Juju environment's `admin-secret`, found in
6864
=== modified file 'config.yaml'
--- config.yaml 2013-01-23 16:41:38 +0000
+++ config.yaml 2013-03-13 22:47:20 +0000
@@ -17,11 +17,6 @@
17 The Juju API Bazaar branch (implementing the websocket server).17 The Juju API Bazaar branch (implementing the websocket server).
18 type: string18 type: string
19 default: lp:~hazmat/juju/rapi-rollup19 default: lp:~hazmat/juju/rapi-rollup
20 juju-api-port:
21 description: |
22 The port used by the websocket server.
23 type: int
24 default: 8080
25 staging:20 staging:
26 description: |21 description: |
27 Connect the Juju GUI to the staging backend22 Connect the Juju GUI to the staging backend
@@ -78,3 +73,17 @@
78 to mutate the environment.73 to mutate the environment.
79 type: boolean74 type: boolean
80 default: false75 default: false
76 serve-tests:
77 description: |
78 Whether or not the GUI unit tests are exposed. If this option is enabled,
79 unit tests can be run in the browser by visiting the URL
80 "https://[Juju GUI address]/test/".
81 type: boolean
82 default: false
83 secure:
84 description: |
85 In order to run the GUI over a non secure connection (HTTP) set this flag
86 to false. Do not set this property unless you understand and accept the
87 risks.
88 type: boolean
89 default: true
8190
=== modified file 'config/config.js.template'
--- config/config.js.template 2013-01-16 10:53:11 +0000
+++ config/config.js.template 2013-03-13 22:47:20 +0000
@@ -1,14 +1,18 @@
1var juju_config = {1var juju_config = {
2 consoleEnabled: %(console_enabled)s,2 consoleEnabled: {{console_enabled}},
3 serverRouting: false,3 serverRouting: false,
4 html5: true,4 html5: true,
5 container: '#main',5 container: '#main',
6 viewContainer: '#main',6 viewContainer: '#main',
7 transitions: false,7 transitions: false,
8 charm_store_url: 'https://jujucharms.com/',8 charm_store_url: 'https://jujucharms.com/',
9 socket_url: 'wss://%(address)s:%(port)s/ws',9 // socket_url is only honored in older versions of the GUI.
10 user: %(user)s,10 socket_url: '{{raw_protocol}}://{{address}}/ws',
11 password: %(password)s,11 // socket_protocol is used instead by newer versions of the GUI to
12 readOnly: %(readonly)s,12 // dynamically generate the websocket address.
13 login_help: %(login_help)s13 socket_protocol: {{protocol}},
14 user: {{user}},
15 password: {{password}},
16 readOnly: {{readonly}},
17 login_help: {{login_help}}
14};18};
1519
=== added file 'config/haproxy.cfg.template'
--- config/haproxy.cfg.template 1970-01-01 00:00:00 +0000
+++ config/haproxy.cfg.template 2013-03-13 22:47:20 +0000
@@ -0,0 +1,42 @@
1global
2 maxconn 4096
3 user haproxy
4 group haproxy
5 daemon
6 # Set the base path for the WebSocket TLS certificate.
7 ca-base {{ssl_cert_path}}
8 # Set the base path for the HTTPS TLS certificate.
9 crt-base {{ssl_cert_path}}
10
11defaults
12 log global
13 maxconn 4096
14 mode http
15 option http-server-close
16 timeout connect 5s
17 timeout client 30s
18 timeout server 30s
19 # Set a long timeout for WebSocket connections.
20 timeout tunnel 8h
21
22backend juju
23 # The Juju WebSocket backend.
24 # Re-encrypt outgoing connections.
25 server ws1 127.0.0.1:{{api_port}} ssl ca-file {{api_pem}} verify required check-ssl inter 500ms
26
27backend web
28 # Web traffic.
29 server web1 127.0.0.1:{{web_port}} check inter 500ms
30
31frontend public
32 # Redirect all HTTP traffic to HTTPS.
33 bind :80
34 {{if secure}}
35 redirect scheme https if !{ ssl_fc }
36 {{endif}}
37 # Handle HTTPS.
38 bind :443 ssl crt {{web_pem}}
39 # Send WebSocket connections to the Juju backend.
40 use_backend juju if { path /ws }
41 # Send everything else to the web server.
42 default_backend web
043
=== added file 'config/haproxy.conf'
--- config/haproxy.conf 1970-01-01 00:00:00 +0000
+++ config/haproxy.conf 2013-03-13 22:47:20 +0000
@@ -0,0 +1,22 @@
1description "HAProxy"
2author "Canonical"
3
4start on (filesystem and net-device-up IFACE=lo)
5stop on runlevel [!2345]
6
7env CONF=/etc/haproxy/haproxy.cfg
8env DAEMON=/usr/sbin/haproxy
9
10expect fork
11respawn
12respawn limit 10 5
13
14pre-start script
15 # Test configuration, exit if errors are found.
16 $DAEMON -c -f $CONF
17 if [ $? -ne 0 ]
18 then exit $?
19 fi
20end script
21
22exec $DAEMON -f $CONF
023
=== modified file 'config/juju-api-agent.conf.template'
--- config/juju-api-agent.conf.template 2013-01-03 15:02:38 +0000
+++ config/juju-api-agent.conf.template 2013-03-13 22:47:20 +0000
@@ -4,11 +4,11 @@
4start on runlevel [2345]4start on runlevel [2345]
5stop on runlevel [!2345]5stop on runlevel [!2345]
66
7env PYTHONPATH=%(juju_dir)s:$PYTHONPATH7env PYTHONPATH={{juju_dir}}:$PYTHONPATH
8env JUJU_ZOOKEEPER=%(zookeeper)s8env JUJU_ZOOKEEPER={{zookeeper}}
99
10# Use --nodaemon so that upstart can correctly retrieve the process ID.10# Use --nodaemon so that upstart can correctly retrieve the process ID.
11exec /usr/bin/python -m juju.agents.api --nodaemon --port %(port)s \11exec /usr/bin/python -m juju.agents.api --nodaemon --port {{port}} \
12 --logfile /var/log/juju/api-agent.log \12 --logfile /var/log/juju/api-agent.log \
13 --session-file /var/run/juju/api-agent.zksession \13 --session-file /var/run/juju/api-agent.zksession \
14 --secure --keys %(keys)s14 --secure --keys {{keys}}
1515
=== modified file 'config/juju-api-improv.conf.template'
--- config/juju-api-improv.conf.template 2013-01-03 15:02:38 +0000
+++ config/juju-api-improv.conf.template 2013-03-13 22:47:20 +0000
@@ -5,8 +5,8 @@
5stop on runlevel [!2345]5stop on runlevel [!2345]
66
7setuid ubuntu7setuid ubuntu
8env PYTHONPATH=%(juju_dir)s:$PYTHONPATH8env PYTHONPATH={{juju_dir}}:$PYTHONPATH
99
10exec /usr/bin/python %(juju_dir)s/improv.py --port %(port)s \10exec /usr/bin/python {{juju_dir}}/improv.py --port {{port}} \
11 -f %(juju_dir)s/%(staging_env)s.json \11 -f {{juju_dir}}/{{staging_env}}.json \
12 --secure --keys %(keys)s12 --secure --keys {{keys}}
1313
=== renamed file 'config/nginx.conf.template' => 'config/nginx-site.template'
--- config/nginx.conf.template 2013-01-03 14:17:21 +0000
+++ config/nginx-site.template 2013-03-13 22:47:20 +0000
@@ -1,29 +1,33 @@
1server {1server {
2 listen 80;2 listen 127.0.0.1:{{port}} default_server;
3 server_name _;3 server_name _;
4 return 301 https://$host$request_uri;4 root {{server_root}};
5}
6
7server {
8 listen 443 default_server ssl;
9 server_name _;
10 root %(server_root)s;
11 index index.html;5 index index.html;
12 ssl_certificate %(ssl_cert_path)s/juju.crt;
13 ssl_certificate_key %(ssl_cert_path)s/juju.key;
146
15 # Serve static assets.7 # Serve static assets.
16 location ^~ /juju-ui/ {8 location ^~ /juju-ui/ {
17 root %(server_root)s;9 root {{server_root}};
18 }10 }
19 location = /favicon.ico {11 location = /favicon.ico {
20 expires 10d;12 expires 10d;
21 root %(server_root)s;13 root {{server_root}};
22 }14 }
23 location = /index.html {15 location = /index.html {
24 root %(server_root)s;16 root {{server_root}};
25 }17 }
2618
19 {{if serve_tests}}
20 # Serve unit tests.
21 location ^~ /test/ {
22 alias {{tests_root}};
23 }
24 {{else}}
25 # Redirect to site root so that the test URL does not directly
26 # reference the manifest file. This avoids automatically adding the URL
27 # to the application cache.
28 rewrite ^/test/ $scheme://$host/ redirect;
29 {{endif}}
30
27 # Fall through to the single-page app for all other URLs.31 # Fall through to the single-page app for all other URLs.
28 location ~ / {32 location ~ / {
29 rewrite .* /index.html last;33 rewrite .* /index.html last;
3034
=== renamed file 'config/juju-gui.conf.template' => 'config/nginx.conf'
--- config/juju-gui.conf.template 2012-12-12 13:23:56 +0000
+++ config/nginx.conf 2013-03-13 22:47:20 +0000
@@ -1,4 +1,4 @@
1description "Juju GUI"1description "nginx"
2author "Canonical"2author "Canonical"
3# Original author: George Shammas <georgyo@gmail.com>.3# Original author: George Shammas <georgyo@gmail.com>.
4# See http://wiki.nginx.org/Upstart.4# See http://wiki.nginx.org/Upstart.
55
=== modified file 'hooks/config-changed'
--- hooks/config-changed 2013-01-23 16:41:38 +0000
+++ hooks/config-changed 2013-03-13 22:47:20 +0000
@@ -4,14 +4,11 @@
4# Copyright 2012 Canonical Ltd. This software is licensed under the4# Copyright 2012 Canonical Ltd. This software is licensed under the
5# GNU Affero General Public License version 3 (see the file LICENSE).5# GNU Affero General Public License version 3 (see the file LICENSE).
66
7from subprocess import CalledProcessError
8import sys7import sys
98
10from charmhelpers import (9from charmhelpers import (
11 get_config,10 get_config,
12 log,11 log,
13 log_entry,
14 log_exit,
15 service_control,12 service_control,
16 STOP,13 STOP,
17)14)
@@ -25,8 +22,11 @@
25 config_json,22 config_json,
26 fetch_api,23 fetch_api,
27 fetch_gui,24 fetch_gui,
28 GUI,25 get_staging_dependencies,
26 HAPROXY,
29 IMPROV,27 IMPROV,
28 log_hook,
29 NGINX,
30 save_or_create_certificates,30 save_or_create_certificates,
31 setup_gui,31 setup_gui,
32 start_agent,32 start_agent,
@@ -40,7 +40,6 @@
40 log('Updating configuration.')40 log('Updating configuration.')
4141
42 added_or_changed = diff.added_or_changed42 added_or_changed = diff.added_or_changed
43 juju_api_port = config.get('juju-api-port')
44 in_staging = config.get('staging')43 in_staging = config.get('staging')
4544
46 # The juju_gui_source_changed and juju_api_branch_changed variables45 # The juju_gui_source_changed and juju_api_branch_changed variables
@@ -70,14 +69,15 @@
7069
71 # Handle changes to the improv server configuration.70 # Handle changes to the improv server configuration.
72 if in_staging:71 if in_staging:
73 staging_properties = set(72 staging_properties = set(['staging', 'staging-environment'])
74 ['staging', 'staging-environment', 'juju-api-port'])
75 staging_changed = added_or_changed & staging_properties73 staging_changed = added_or_changed & staging_properties
76 if staging_changed or ssl_changed or juju_api_branch_changed:74 if staging_changed or ssl_changed or juju_api_branch_changed:
77 if 'staging' in added_or_changed:75 if 'staging' in added_or_changed:
78 # 'staging' went from False to True, so the agent server is76 # 'staging' went from False to True, so the agent server is
79 # running and must be stopped.77 # running and must be stopped.
80 current_api = AGENT78 current_api = AGENT
79 # We need to make sure we have staging dependencies.
80 get_staging_dependencies()
81 else:81 else:
82 # Only staging parameters changed, so the existing staging82 # Only staging parameters changed, so the existing staging
83 # server must be stopped and later restarted.83 # server must be stopped and later restarted.
@@ -86,13 +86,12 @@
86 service_control(current_api, STOP)86 service_control(current_api, STOP)
87 # Now the improv server can be cleanly started.87 # Now the improv server can be cleanly started.
88 log('Starting or restarting staging.')88 log('Starting or restarting staging.')
89 start_improv(juju_api_port, config.get('staging-environment'),89 start_improv(config.get('staging-environment'),
90 config['ssl-cert-path'])90 config['ssl-cert-path'])
91 else:91 else:
92 agent_properties = set(['juju-api-port', 'staging'])92 agent_changed = 'staging' in added_or_changed
93 agent_changed = added_or_changed & agent_properties
94 if agent_changed or ssl_changed or juju_api_branch_changed:93 if agent_changed or ssl_changed or juju_api_branch_changed:
95 if 'staging' in added_or_changed:94 if agent_changed:
96 # If 'staging' transitions to False we need to stop the backend95 # If 'staging' transitions to False we need to stop the backend
97 # and start the agent.96 # and start the agent.
98 current_api = IMPROV97 current_api = IMPROV
@@ -102,22 +101,26 @@
102 current_api = AGENT101 current_api = AGENT
103 service_control(current_api, STOP)102 service_control(current_api, STOP)
104 log('Starting or restarting Juju API agent.')103 log('Starting or restarting Juju API agent.')
105 start_agent(juju_api_port, config['ssl-cert-path'])104 start_agent(config['ssl-cert-path'])
106105
107 # Handle changes to the juju-gui configuration.106 # Handle changes to the juju-gui configuration.
108 gui_properties = set([107 gui_properties = set([
109 'juju-api-port', 'juju-gui-console-enabled', 'login-help', 'read-only',108 'juju-gui-console-enabled', 'login-help', 'read-only', 'serve-tests',
110 'staging'])109 'staging', 'secure'])
111 gui_changed = added_or_changed & gui_properties110 gui_changed = added_or_changed & gui_properties
112 if gui_changed or ssl_changed or juju_gui_source_changed:111 if gui_changed or ssl_changed or juju_gui_source_changed:
113 with su('root'):112 with su('root'):
114 service_control(GUI, STOP)113 service_control(HAPROXY, STOP)
114 service_control(NGINX, STOP)
115 console_enabled = config.get('juju-gui-console-enabled')115 console_enabled = config.get('juju-gui-console-enabled')
116 login_help = config['login-help']116 login_help = config['login-help']
117 readonly = config['read-only']117 readonly = config['read-only']
118 serve_tests = config['serve-tests']
118 ssl_cert_path = config['ssl-cert-path']119 ssl_cert_path = config['ssl-cert-path']
119 start_gui(juju_api_port, console_enabled, login_help, readonly,120 secure = config['secure']
120 in_staging, ssl_cert_path)121 start_gui(
122 console_enabled, login_help, readonly, in_staging, ssl_cert_path,
123 serve_tests, secure=secure)
121124
122125
123def main():126def main():
@@ -133,12 +136,5 @@
133136
134137
135if __name__ == '__main__':138if __name__ == '__main__':
136 log_entry()139 with log_hook():
137 try:
138 main()140 main()
139 except CalledProcessError as e:
140 log('Exception caught:')
141 log(e.output)
142 raise
143 finally:
144 log_exit()
145141
=== modified file 'hooks/install'
--- hooks/install 2013-01-23 16:11:16 +0000
+++ hooks/install 2013-03-13 22:47:20 +0000
@@ -1,10 +1,10 @@
1#!/usr/bin/env python21#!/usr/bin/env python2
2# -*- python -*-2# -*- python -*-
33
4from subprocess import (4import os
5 CalledProcessError,5import shutil
6 check_call,6from subprocess import check_call
7)7
88
9# If the user's environment has "juju-origin: ppa" set, they will9# If the user's environment has "juju-origin: ppa" set, they will
10# automatically have access to python-charmhelpers and python-shelltoolbox.10# automatically have access to python-charmhelpers and python-shelltoolbox.
@@ -14,17 +14,20 @@
14import bootstrap_utils14import bootstrap_utils
15bootstrap_utils.install_extra_repositories('ppa:juju/pkgs')15bootstrap_utils.install_extra_repositories('ppa:juju/pkgs')
1616
17# python-shelltoolbox is installed as a dependency of python-charmhelpers.17# Python dependencies must be installed here so that the charm can import and
18check_call(['apt-get', 'install', '-y', 'python-charmhelpers'])18# use required libraries.
1919PYTHON_DEPENDENCIES = (
2020 'python-charmhelpers', 'python-launchpadlib', 'python-shelltoolbox',
21# These modules depend on charmhelpers and shelltoolbox being installed so they21 'python-tempita',
22)
23check_call(('apt-get', 'install', '-y') + PYTHON_DEPENDENCIES)
24
25
26# These modules depend on the Python dependencies above being installed so they
22# must not be imported until those packages are available.27# must not be imported until those packages are available.
23from charmhelpers import (28from charmhelpers import (
24 get_config,29 get_config,
25 log,30 log,
26 log_entry,
27 log_exit,
28)31)
29from shelltoolbox import (32from shelltoolbox import (
30 apt_get_install,33 apt_get_install,
@@ -36,27 +39,41 @@
36 config_json,39 config_json,
37 fetch_api,40 fetch_api,
38 fetch_gui,41 fetch_gui,
42 log_hook,
43 get_staging_dependencies,
39 save_or_create_certificates,44 save_or_create_certificates,
40 setup_gui,45 setup_gui,
41 setup_nginx,46 setup_nginx,
42)47)
4348
4449
50CONFIG_FILES = ('haproxy.conf', 'nginx.conf')
51REPOSITORIES = ('ppa:juju-gui/ppa',)
45DEB_DEPENDENCIES = (52DEB_DEPENDENCIES = (
46 'bzr', 'curl', 'imagemagick', 'make', 'nginx', 'nodejs', 'npm', 'openssl',53 'curl', 'haproxy', 'nginx', 'openssl',
47 'python-launchpadlib', 'zookeeper',
48)54)
4955
5056
51def get_dependencies():57def get_dependencies():
58 """Install deb dependencies."""
52 log('Installing dependencies.')59 log('Installing dependencies.')
53 cmd_log(install_extra_repositories('ppa:chris-lea/node.js'))60 cmd_log(install_extra_repositories(*REPOSITORIES))
54 cmd_log(apt_get_install(*DEB_DEPENDENCIES))61 cmd_log(apt_get_install(*DEB_DEPENDENCIES))
5562
5663
64def setup_services():
65 """Set up haproxy and nginx upstart configuration files."""
66 log('Setting up haproxy and nginx start up scripts.')
67 source_dir = os.path.join(os.path.dirname(__file__), '..', 'config')
68 for config_file in CONFIG_FILES:
69 shutil.copy(os.path.join(source_dir, config_file), '/etc/init/')
70
71
57def main():72def main():
58 config = get_config()73 config = get_config()
59 get_dependencies()74 get_dependencies()
75 if config.get('staging'):
76 get_staging_dependencies()
60 release_tarball = fetch_gui(77 release_tarball = fetch_gui(
61 config['juju-gui-source'], config['command-log-file'])78 config['juju-gui-source'], config['command-log-file'])
62 setup_gui(release_tarball)79 setup_gui(release_tarball)
@@ -65,16 +82,10 @@
65 config['ssl-cert-path'], config.get('ssl-cert-contents'),82 config['ssl-cert-path'], config.get('ssl-cert-contents'),
66 config.get('ssl-key-contents'))83 config.get('ssl-key-contents'))
67 fetch_api(config['juju-api-branch'])84 fetch_api(config['juju-api-branch'])
85 setup_services()
68 config_json.set(config)86 config_json.set(config)
6987
7088
71if __name__ == '__main__':89if __name__ == '__main__':
72 log_entry()90 with log_hook():
73 try:
74 main()91 main()
75 except CalledProcessError as e:
76 log('Exception caught:')
77 log(e.output)
78 raise
79 finally:
80 log_exit()
8192
=== modified file 'hooks/start'
--- hooks/start 2013-01-23 16:41:38 +0000
+++ hooks/start 2013-03-13 22:47:20 +0000
@@ -4,47 +4,38 @@
4from charmhelpers import (4from charmhelpers import (
5 get_config,5 get_config,
6 log,6 log,
7 log_entry,
8 log_exit,
9 open_port,7 open_port,
10)8)
119
12from utils import (10from utils import (
11 log_hook,
13 start_agent,12 start_agent,
14 start_gui,13 start_gui,
15 start_improv,14 start_improv,
16)15)
1716
1817
19def open_ports(juju_api_port):18def open_ports():
20 """Expose Juju GUI web server and websocket server ports."""19 """Expose Juju GUI web server (HTTP and HTTPS) ports."""
21 log('Exposing services.')20 log('Exposing services.')
22 # Open the Juju GUI web server HTTP and HTTPS ports.
23 open_port(80)21 open_port(80)
24 open_port(443)22 open_port(443)
25 # Open the Juju websocket server port.
26 open_port(juju_api_port)
2723
2824
29def main():25def main():
30 config = get_config()26 config = get_config()
31 juju_api_port = config['juju-api-port']
32 staging = config.get('staging')27 staging = config.get('staging')
33 start_gui(
34 juju_api_port, config['juju-gui-console-enabled'],
35 config['login-help'], config['read-only'], staging,
36 config['ssl-cert-path'])
37 if staging:28 if staging:
38 start_improv(juju_api_port, config['staging-environment'],29 start_improv(config['staging-environment'], config['ssl-cert-path'])
39 config['ssl-cert-path'])
40 else:30 else:
41 start_agent(juju_api_port, config['ssl-cert-path'])31 start_agent(config['ssl-cert-path'])
42 open_ports(juju_api_port)32 start_gui(
33 config['juju-gui-console-enabled'], config['login-help'],
34 config['read-only'], staging, config['ssl-cert-path'],
35 config['serve-tests'], secure=config['secure'])
36 open_ports()
4337
4438
45if __name__ == '__main__':39if __name__ == '__main__':
46 log_entry()40 with log_hook():
47 try:
48 main()41 main()
49 finally:
50 log_exit()
5142
=== modified file 'hooks/stop'
--- hooks/stop 2012-12-19 09:57:45 +0000
+++ hooks/stop 2013-03-13 22:47:20 +0000
@@ -1,14 +1,13 @@
1#!/usr/bin/env python21#!/usr/bin/env python2
2#-*- python -*-2#-*- python -*-
33
4from charmhelpers import (4from charmhelpers import get_config
5 get_config,5
6 log_entry,6from utils import (
7 log_exit,7 log_hook,
8 stop,
8)9)
910
10from utils import stop
11
1211
13def main():12def main():
14 config = get_config()13 config = get_config()
@@ -16,8 +15,5 @@
1615
1716
18if __name__ == '__main__':17if __name__ == '__main__':
19 log_entry()18 with log_hook():
20 try:
21 main()19 main()
22 finally:
23 log_exit()
2420
=== modified file 'hooks/utils.py'
--- hooks/utils.py 2013-01-23 16:41:38 +0000
+++ hooks/utils.py 2013-03-13 22:47:20 +0000
@@ -2,6 +2,7 @@
22
3__all__ = [3__all__ = [
4 'AGENT',4 'AGENT',
5 'API_PORT',
5 'bzr_checkout',6 'bzr_checkout',
6 'cmd_log',7 'cmd_log',
7 'CURRENT_DIR',8 'CURRENT_DIR',
@@ -9,11 +10,16 @@
9 'fetch_gui',10 'fetch_gui',
10 'first_path_in_dir',11 'first_path_in_dir',
11 'get_release_file_url',12 'get_release_file_url',
13 'get_staging_dependencies',
12 'get_zookeeper_address',14 'get_zookeeper_address',
13 'GUI',15 'HAPROXY',
14 'IMPROV',16 'IMPROV',
15 'JUJU_DIR',17 'JUJU_DIR',
16 'JUJU_GUI_DIR',18 'JUJU_GUI_DIR',
19 'JUJU_GUI_SITE',
20 'JUJU_PEM',
21 'log_hook',
22 'NGINX',
17 'parse_source',23 'parse_source',
18 'render_to_file',24 'render_to_file',
19 'save_or_create_certificates',25 'save_or_create_certificates',
@@ -23,18 +29,26 @@
23 'start_gui',29 'start_gui',
24 'start_improv',30 'start_improv',
25 'stop',31 'stop',
32 'WEB_PORT',
26]33]
2734
35from contextlib import contextmanager
28import json36import json
29import os37import os
30import logging38import logging
39import shutil
40from subprocess import CalledProcessError
31import tempfile41import tempfile
42import tempita
3243
33from launchpadlib.launchpad import Launchpad44from launchpadlib.launchpad import Launchpad
34from shelltoolbox import (45from shelltoolbox import (
46 apt_get_install,
35 command,47 command,
36 environ,48 environ,
49 install_extra_repositories,
37 run,50 run,
51 script_name,
38 search_file,52 search_file,
39 Serializer,53 Serializer,
40 su,54 su,
@@ -51,11 +65,25 @@
5165
52AGENT = 'juju-api-agent'66AGENT = 'juju-api-agent'
53IMPROV = 'juju-api-improv'67IMPROV = 'juju-api-improv'
54GUI = 'juju-gui'68HAPROXY = 'haproxy'
69NGINX = 'nginx'
70
71API_PORT = 8080
72WEB_PORT = 8000
73
55CURRENT_DIR = os.getcwd()74CURRENT_DIR = os.getcwd()
56JUJU_DIR = os.path.join(CURRENT_DIR, 'juju')75JUJU_DIR = os.path.join(CURRENT_DIR, 'juju')
57JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui')76JUJU_GUI_DIR = os.path.join(CURRENT_DIR, 'juju-gui')
58JUJU_GUI_SITE = '/etc/nginx/sites-available/juju-gui'77JUJU_GUI_SITE = '/etc/nginx/sites-available/juju-gui'
78JUJU_PEM = 'juju.includes-private-key.pem'
79BUILD_REPOSITORIES = ('ppa:chris-lea/node.js',)
80DEB_BUILD_DEPENDENCIES = (
81 'bzr', 'imagemagick', 'make', 'nodejs', 'npm',
82)
83DEB_STAGE_DEPENDENCIES = (
84 'zookeeper',
85)
86
5987
60# Store the configuration from on invocation to the next.88# Store the configuration from on invocation to the next.
61config_json = Serializer('/tmp/config.json')89config_json = Serializer('/tmp/config.json')
@@ -63,6 +91,19 @@
63bzr_checkout = command('bzr', 'co', '--lightweight')91bzr_checkout = command('bzr', 'co', '--lightweight')
6492
6593
94def _get_build_dependencies():
95 """Install deb dependencies for building."""
96 log('Installing build dependencies.')
97 cmd_log(install_extra_repositories(*BUILD_REPOSITORIES))
98 cmd_log(apt_get_install(*DEB_BUILD_DEPENDENCIES))
99
100
101def get_staging_dependencies():
102 """Install deb dependencies for the stage (improv) environment."""
103 log('Installing stage dependencies.')
104 cmd_log(apt_get_install(*DEB_STAGE_DEPENDENCIES))
105
106
66def first_path_in_dir(directory):107def first_path_in_dir(directory):
67 """Return the full path of the first file/dir in *directory*."""108 """Return the full path of the first file/dir in *directory*."""
68 return os.path.join(directory, os.listdir(directory)[0])109 return os.path.join(directory, os.listdir(directory)[0])
@@ -118,6 +159,25 @@
118 return line.split('=')[1].strip('"')159 return line.split('=')[1].strip('"')
119160
120161
162@contextmanager
163def log_hook():
164 """Log when an hook starts and stops its execution.
165
166 Also log to stdout possible CalledProcessError exceptions raised executing
167 the hook.
168 """
169 script = script_name()
170 log(">>> Entering {}".format(script))
171 try:
172 yield
173 except CalledProcessError as err:
174 log('Exception caught:')
175 log(err.output)
176 raise
177 finally:
178 log("<<< Exiting {}".format(script))
179
180
121def parse_source(source):181def parse_source(source):
122 """Parse the ``juju-gui-source`` option.182 """Parse the ``juju-gui-source`` option.
123183
@@ -138,19 +198,21 @@
138 return 'stable', source198 return 'stable', source
139199
140200
141def render_to_file(template, context, destination):201def render_to_file(template_name, context, destination):
142 """Render the given *template* into *destination* using *context*.202 """Render the given *template_name* into *destination* using *context*.
143203
144 The arguments *template* is the name or path of the template file: it may204 The tempita template language is used to render contents
145 be either a path relative to ``../config`` or an absolute path.205 (see http://pythonpaste.org/tempita/).
206 The argument *template_name* is the name or path of the template file:
207 it may be either a path relative to ``../config`` or an absolute path.
146 The argument *destination* is a file path.208 The argument *destination* is a file path.
147 The argument *context* is a dict-like object.209 The argument *context* is a dict-like object.
148 """210 """
149 template_path = os.path.join(211 template_path = os.path.join(
150 os.path.dirname(__file__), '..', 'config', template)212 os.path.dirname(__file__), '..', 'config', template_name)
151 contents = open(template_path).read()213 template = tempita.Template.from_filename(template_path)
152 with open(destination, 'w') as stream:214 with open(destination, 'w') as stream:
153 stream.write(contents % context)215 stream.write(template.substitute(context))
154216
155217
156results_log = None218results_log = None
@@ -165,7 +227,7 @@
165 filename=config['command-log-file'],227 filename=config['command-log-file'],
166 level=logging.INFO,228 level=logging.INFO,
167 format="%(asctime)s: %(name)s@%(levelname)s %(message)s")229 format="%(asctime)s: %(name)s@%(levelname)s %(message)s")
168 results_log = logging.getLogger(GUI)230 results_log = logging.getLogger('juju-gui')
169231
170232
171def cmd_log(results):233def cmd_log(results):
@@ -179,15 +241,15 @@
179 results_log.info('\n' + results)241 results_log.info('\n' + results)
180242
181243
182def start_improv(juju_api_port, staging_env, ssl_cert_path,244def start_improv(staging_env, ssl_cert_path,
183 config_path='/etc/init/juju-api-improv.conf'):245 config_path='/etc/init/juju-api-improv.conf'):
184 """Start a simulated juju environment using ``improv.py``."""246 """Start a simulated juju environment using ``improv.py``."""
185 log('Setting up staging start up script.')247 log('Setting up staging start up script.')
186 context = {248 context = {
187 'juju_dir': JUJU_DIR,249 'juju_dir': JUJU_DIR,
188 'port': juju_api_port,250 'keys': ssl_cert_path,
251 'port': API_PORT,
189 'staging_env': staging_env,252 'staging_env': staging_env,
190 'keys': ssl_cert_path,
191 }253 }
192 render_to_file('juju-api-improv.conf.template', context, config_path)254 render_to_file('juju-api-improv.conf.template', context, config_path)
193 log('Starting the staging backend.')255 log('Starting the staging backend.')
@@ -195,8 +257,7 @@
195 service_control(IMPROV, START)257 service_control(IMPROV, START)
196258
197259
198def start_agent(juju_api_port, ssl_cert_path,260def start_agent(ssl_cert_path, config_path='/etc/init/juju-api-agent.conf'):
199 config_path='/etc/init/juju-api-agent.conf'):
200 """Start the Juju agent and connect to the current environment."""261 """Start the Juju agent and connect to the current environment."""
201 # Retrieve the Zookeeper address from the start up script.262 # Retrieve the Zookeeper address from the start up script.
202 unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..'))263 unit_dir = os.path.realpath(os.path.join(CURRENT_DIR, '..'))
@@ -205,9 +266,9 @@
205 log('Setting up API agent start up script.')266 log('Setting up API agent start up script.')
206 context = {267 context = {
207 'juju_dir': JUJU_DIR,268 'juju_dir': JUJU_DIR,
208 'port': juju_api_port,269 'keys': ssl_cert_path,
270 'port': API_PORT,
209 'zookeeper': zookeeper,271 'zookeeper': zookeeper,
210 'keys': ssl_cert_path,
211 }272 }
212 render_to_file('juju-api-agent.conf.template', context, config_path)273 render_to_file('juju-api-agent.conf.template', context, config_path)
213 log('Starting API agent.')274 log('Starting API agent.')
@@ -216,26 +277,37 @@
216277
217278
218def start_gui(279def start_gui(
219 juju_api_port, console_enabled, login_help, readonly, in_staging,280 console_enabled, login_help, readonly, in_staging, ssl_cert_path,
220 ssl_cert_path, config_path='/etc/init/juju-gui.conf',281 serve_tests, haproxy_path='/etc/haproxy/haproxy.cfg',
221 nginx_path=JUJU_GUI_SITE, config_js_path=None):282 nginx_path=JUJU_GUI_SITE, config_js_path=None, secure=True):
222 """Set up and start the Juju GUI server."""283 """Set up and start the Juju GUI server."""
223 with su('root'):284 with su('root'):
224 run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR)285 run('chown', '-R', 'ubuntu:', JUJU_GUI_DIR)
225 build_dir = JUJU_GUI_DIR + '/build-'286 # XXX 2013-02-05 frankban bug=1116320:
226 build_dir += 'debug' if in_staging else 'prod'287 # External insecure resources are still loaded when testing in the
227 log('Setting up Juju GUI start up script.')288 # debug environment. For now, switch to the production environment if
228 render_to_file('juju-gui.conf.template', {}, config_path)289 # the charm is configured to serve tests.
290 if in_staging and not serve_tests:
291 build_dirname = 'build-debug'
292 else:
293 build_dirname = 'build-prod'
294 build_dir = os.path.join(JUJU_GUI_DIR, build_dirname)
229 log('Generating the Juju GUI configuration file.')295 log('Generating the Juju GUI configuration file.')
230 user, password = ('admin', 'admin') if in_staging else (None, None)296 user, password = ('admin', 'admin') if in_staging else (None, None)
297 if secure:
298 protocol = 'wss'
299 else:
300 log('Running in insecure mode! Port 80 will serve unencrypted.')
301 protocol = 'ws'
231 context = {302 context = {
303 'raw_protocol': protocol,
232 'address': unit_get('public-address'),304 'address': unit_get('public-address'),
233 'console_enabled': json.dumps(console_enabled),305 'console_enabled': json.dumps(console_enabled),
234 'login_help': json.dumps(login_help),306 'login_help': json.dumps(login_help),
235 'password': json.dumps(password),307 'password': json.dumps(password),
236 'port': juju_api_port,
237 'readonly': json.dumps(readonly),308 'readonly': json.dumps(readonly),
238 'user': json.dumps(user),309 'user': json.dumps(user),
310 'protocol': json.dumps(protocol)
239 }311 }
240 if config_js_path is None:312 if config_js_path is None:
241 config_js_path = os.path.join(313 config_js_path = os.path.join(
@@ -243,21 +315,37 @@
243 render_to_file('config.js.template', context, config_js_path)315 render_to_file('config.js.template', context, config_js_path)
244 log('Generating the nginx site configuration file.')316 log('Generating the nginx site configuration file.')
245 context = {317 context = {
318 'port': WEB_PORT,
319 'serve_tests': serve_tests,
246 'server_root': build_dir,320 'server_root': build_dir,
247 'ssl_cert_path': ssl_cert_path.rstrip('/'),321 'tests_root': os.path.join(JUJU_GUI_DIR, 'test', ''),
248 }322 }
249 render_to_file('nginx.conf.template', context, nginx_path)323 render_to_file('nginx-site.template', context, nginx_path)
324 log('Generating haproxy configuration file.')
325 context = {
326 'api_pem': JUJU_PEM,
327 'api_port': API_PORT,
328 'ssl_cert_path': ssl_cert_path,
329 # Use the same certificate for both HTTPS and Websocket connections.
330 # In the long term, we want separate certs to be used here.
331 'web_pem': JUJU_PEM,
332 'web_port': WEB_PORT,
333 'secure': secure
334 }
335 render_to_file('haproxy.cfg.template', context, haproxy_path)
250 log('Starting Juju GUI.')336 log('Starting Juju GUI.')
251 with su('root'):337 with su('root'):
252 # Start the Juju GUI.338 # Start the Juju GUI.
253 service_control(GUI, START)339 service_control(NGINX, START)
340 service_control(HAPROXY, START)
254341
255342
256def stop(in_staging):343def stop(in_staging):
257 """Stop the Juju API agent."""344 """Stop the Juju API agent."""
258 with su('root'):345 with su('root'):
259 log('Stopping Juju GUI.')346 log('Stopping Juju GUI.')
260 service_control(GUI, STOP)347 service_control(HAPROXY, STOP)
348 service_control(NGINX, STOP)
261 if in_staging:349 if in_staging:
262 log('Stopping the staging backend.')350 log('Stopping the staging backend.')
263 service_control(IMPROV, STOP)351 service_control(IMPROV, STOP)
@@ -271,6 +359,9 @@
271 # Retrieve a Juju GUI release.359 # Retrieve a Juju GUI release.
272 origin, version_or_branch = parse_source(juju_gui_source)360 origin, version_or_branch = parse_source(juju_gui_source)
273 if origin == 'branch':361 if origin == 'branch':
362 # Make sure we have the dependencies necessary for us to actually make
363 # a build.
364 _get_build_dependencies()
274 # Create a release starting from a branch.365 # Create a release starting from a branch.
275 juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source')366 juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source')
276 log('Retrieving Juju GUI source checkout from %s.' % version_or_branch)367 log('Retrieving Juju GUI source checkout from %s.' % version_or_branch)
@@ -279,7 +370,7 @@
279 log('Preparing a Juju GUI release.')370 log('Preparing a Juju GUI release.')
280 logdir = os.path.dirname(logpath)371 logdir = os.path.dirname(logpath)
281 fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir)372 fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir)
282 log('Output from "make distfile" sent to', name)373 log('Output from "make distfile" sent to %s' % name)
283 with environ(NO_BZR='1'):374 with environ(NO_BZR='1'):
284 run('make', '-C', juju_gui_source_dir, 'distfile',375 run('make', '-C', juju_gui_source_dir, 'distfile',
285 stdout=fd, stderr=fd)376 stdout=fd, stderr=fd)
@@ -338,6 +429,9 @@
338429
339 If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them430 If both *ssl_cert_contents* and *ssl_key_contents* are provided, use them
340 as certificates; otherwise, generate them.431 as certificates; otherwise, generate them.
432
433 Also create a pem file, suitable for use in the haproxy configuration,
434 concatenating the key and the certificate files.
341 """435 """
342 crt_path = os.path.join(ssl_cert_path, 'juju.crt')436 crt_path = os.path.join(ssl_cert_path, 'juju.crt')
343 key_path = os.path.join(ssl_cert_path, 'juju.key')437 key_path = os.path.join(ssl_cert_path, 'juju.key')
@@ -358,3 +452,10 @@
358 # These are arbitrary test values for the certificate.452 # These are arbitrary test values for the certificate.
359 '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com',453 '/C=GB/ST=Juju/L=GUI/O=Ubuntu/CN=juju.ubuntu.com',
360 '-keyout', key_path, '-out', crt_path))454 '-keyout', key_path, '-out', crt_path))
455 # Generate the pem file.
456 pem_path = os.path.join(ssl_cert_path, JUJU_PEM)
457 if os.path.exists(pem_path):
458 os.remove(pem_path)
459 with open(pem_path, 'w') as pem_file:
460 shutil.copyfileobj(open(key_path), pem_file)
461 shutil.copyfileobj(open(crt_path), pem_file)
361462
=== modified file 'revision'
--- revision 2013-01-23 16:41:57 +0000
+++ revision 2013-03-13 22:47:20 +0000
@@ -1,1 +1,1 @@
125129
22
=== modified file 'tests/deploy.test'
--- tests/deploy.test 2013-01-22 11:40:17 +0000
+++ tests/deploy.test 2013-03-13 22:47:20 +0000
@@ -6,7 +6,6 @@
6import urlparse6import urlparse
77
8from charmhelpers import make_charm_config_file8from charmhelpers import make_charm_config_file
9from selenium.common import exceptions
10from selenium.webdriver import Firefox9from selenium.webdriver import Firefox
11from selenium.webdriver.support import ui10from selenium.webdriver.support import ui
12from shelltoolbox import command11from shelltoolbox import command
@@ -33,16 +32,15 @@
33 # Create a Selenium browser instance.32 # Create a Selenium browser instance.
34 selenium = self.selenium = Firefox()33 selenium = self.selenium = Firefox()
35 self.addCleanup(selenium.quit)34 self.addCleanup(selenium.quit)
36 self.wait = ui.WebDriverWait(selenium, 20)
3735
38 def tearDown(self):36 def tearDown(self):
39 juju('destroy-service', self.charm)37 juju('destroy-service', self.charm)
4038
41 def assertEnvironmentIsConnected(self):39 def assertEnvironmentIsConnected(self):
42 """Assert the GUI environment is connected to the Juju API agent."""40 """Assert the GUI environment is connected to the Juju API agent."""
43 def connected(driver):41 self.wait_for_script(
44 return driver.execute_script('return app.env.get("connected");')42 'return app.env.get("connected");',
45 self.wait.until(connected, 'Environment not connected.')43 error='Environment not connected.')
4644
47 def make_config_file(self, options=None):45 def make_config_file(self, options=None):
48 """Create a charm config file adding, if required, the Juju GUI source.46 """Create a charm config file adding, if required, the Juju GUI source.
@@ -59,6 +57,19 @@
59 options.setdefault('juju-gui-source', JUJU_GUI_SOURCE)57 options.setdefault('juju-gui-source', JUJU_GUI_SOURCE)
60 return make_charm_config_file({self.charm: options})58 return make_charm_config_file({self.charm: options})
6159
60 def handle_browser_warning(self):
61 """Overstep the browser warning dialog if required."""
62 self.wait_for_script(
63 'return window.isBrowserSupported',
64 error='Function isBrowserSupported not found.')
65 script = 'return window.isBrowserSupported(navigator.userAgent)'
66 supported = self.selenium.execute_script(script)
67 if not supported:
68 continue_button = self.wait_for_css_selector(
69 '#browser-warning input',
70 error='Browser warning dialog not found.')
71 continue_button.click()
72
62 def navigate_to(self, hostname, path='/'):73 def navigate_to(self, hostname, path='/'):
63 """Load a page using the current Selenium driver.74 """Load a page using the current Selenium driver.
6475
@@ -72,16 +83,41 @@
72 def page_ready(driver):83 def page_ready(driver):
73 driver.get(url)84 driver.get(url)
74 return driver.title == 'Juju Admin'85 return driver.title == 'Juju Admin'
75 self.wait.until(page_ready, 'Juju GUI not found.')86 self.wait_for(page_ready, error='Juju GUI not found.')
87
88 def wait_for(self, condition, error=None, timeout=20):
89 """Wait for condition to be True.
90
91 The argument condition is a callable accepting a driver object.
92 Fail printing the provided error if timeout is exceeded.
93 Otherwise, return the value returned by the condition call.
94 """
95 wait = ui.WebDriverWait(self.selenium, timeout)
96 return wait.until(condition, error)
97
98 def wait_for_css_selector(self, selector, error=None, timeout=20):
99 """Wait until the provided CSS selector is found.
100
101 Fail printing the provided error if timeout is exceeded.
102 Otherwise, return the value returned by the script.
103 """
104 condition = lambda driver: driver.find_elements_by_css_selector(
105 selector)
106 elements = self.wait_for(condition, error=error, timeout=timeout)
107 return elements[0]
108
109 def wait_for_script(self, script, error=None, timeout=20):
110 """Wait for the given JavaScript snippet to return a True value.
111
112 Fail printing the provided error if timeout is exceeded.
113 Otherwise, return the value returned by the script.
114 """
115 condition = lambda driver: driver.execute_script(script)
116 return self.wait_for(condition, error=error, timeout=timeout)
76117
77 def login(self, password):118 def login(self, password):
78 """Log in to access the Juju GUI using the provided *password*."""119 """Log in to access the Juju GUI using the provided *password*."""
79 def form_displayed(driver):120 form = self.wait_for_css_selector('form', 'Login form not found.')
80 try:
81 return driver.find_element_by_css_selector('form')
82 except exceptions.NoSuchElementException:
83 return False
84 form = self.wait.until(form_displayed, 'Login form not found.')
85 passwd = form.find_element_by_css_selector('input[type=password]')121 passwd = form.find_element_by_css_selector('input[type=password]')
86 passwd.send_keys(password)122 passwd.send_keys(password)
87 submit = form.find_element_by_css_selector('input[type=submit]')123 submit = form.find_element_by_css_selector('input[type=submit]')
@@ -91,7 +127,7 @@
91 """Return the set of services' names displayed in the current page."""127 """Return the set of services' names displayed in the current page."""
92 def services_found(driver):128 def services_found(driver):
93 return driver.find_elements_by_css_selector('.service .name')129 return driver.find_elements_by_css_selector('.service .name')
94 services = self.wait.until(services_found, 'Services not displayed.')130 services = self.wait_for(services_found, 'Services not displayed.')
95 return set([element.text for element in services])131 return set([element.text for element in services])
96132
97 def stop_services(self, hostname, services):133 def stop_services(self, hostname, services):
@@ -129,30 +165,21 @@
129 hostname = self.deploy()165 hostname = self.deploy()
130 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.166 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.
131 self.addCleanup(167 self.addCleanup(
132 self.stop_services, hostname, ['juju-api-agent', 'juju-gui'])168 self.stop_services,
133 self.navigate_to(hostname)169 hostname, ['haproxy', 'nginx', 'juju-api-agent'])
134 self.assertEnvironmentIsConnected()170 self.navigate_to(hostname)
135171 self.handle_browser_warning()
136 def test_customized_api_port(self):172 self.assertEnvironmentIsConnected()
137 # It is possible to customize the port used by the websocket server.
138 api_port = 8081
139 hostname = self.deploy({'juju-api-port': api_port})
140 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.
141 self.addCleanup(
142 self.stop_services, hostname, ['juju-api-agent', 'juju-gui'])
143 self.navigate_to(hostname)
144 self.assertEnvironmentIsConnected()
145 ws_url = self.selenium.execute_script(
146 'return app.env.get("socket_url");')
147 self.assertIn(str(api_port), ws_url)
148173
149 def test_staging(self):174 def test_staging(self):
150 # Ensure the Juju GUI and improv services are correctly set up.175 # Ensure the Juju GUI and improv services are correctly set up.
151 hostname = self.deploy({'staging': 'true'})176 hostname = self.deploy({'staging': 'true'})
152 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.177 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.
153 self.addCleanup(178 self.addCleanup(
154 self.stop_services, hostname, ['juju-api-improv', 'juju-gui'])179 self.stop_services,
180 hostname, ['haproxy', 'nginx', 'juju-api-improv'])
155 self.navigate_to(hostname)181 self.navigate_to(hostname)
182 self.handle_browser_warning()
156 self.assertEnvironmentIsConnected()183 self.assertEnvironmentIsConnected()
157 # The staging environment contains five deployed services.184 # The staging environment contains five deployed services.
158 self.assertSetEqual(set(STAGING_SERVICES), self.get_service_names())185 self.assertSetEqual(set(STAGING_SERVICES), self.get_service_names())
@@ -162,8 +189,10 @@
162 hostname = self.deploy({'juju-gui-source': JUJU_GUI_TEST_BRANCH})189 hostname = self.deploy({'juju-gui-source': JUJU_GUI_TEST_BRANCH})
163 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.190 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.
164 self.addCleanup(191 self.addCleanup(
165 self.stop_services, hostname, ['juju-api-agent', 'juju-gui'])192 self.stop_services,
193 hostname, ['haproxy', 'nginx', 'juju-api-agent'])
166 self.navigate_to(hostname)194 self.navigate_to(hostname)
195 self.handle_browser_warning()
167 self.assertEnvironmentIsConnected()196 self.assertEnvironmentIsConnected()
168197
169198
@@ -192,8 +221,10 @@
192 hostname = self.deploy_to()221 hostname = self.deploy_to()
193 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.222 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.
194 self.addCleanup(223 self.addCleanup(
195 self.stop_services, hostname, ['juju-api-agent', 'juju-gui'])224 self.stop_services,
225 hostname, ['haproxy', 'nginx', 'juju-api-agent'])
196 self.navigate_to(hostname)226 self.navigate_to(hostname)
227 self.handle_browser_warning()
197 self.assertEnvironmentIsConnected()228 self.assertEnvironmentIsConnected()
198229
199230
200231
=== modified file 'tests/test_utils.py'
--- tests/test_utils.py 2013-01-23 16:41:38 +0000
+++ tests/test_utils.py 2013-03-13 22:47:20 +0000
@@ -4,16 +4,23 @@
4import os4import os
5import shutil5import shutil
6from simplejson import dumps6from simplejson import dumps
7from subprocess import CalledProcessError
7import tempfile8import tempfile
9import tempita
8import unittest10import unittest
911
10import charmhelpers12import charmhelpers
13
11from utils import (14from utils import (
12 _get_by_attr,15 _get_by_attr,
16 API_PORT,
13 cmd_log,17 cmd_log,
14 first_path_in_dir,18 first_path_in_dir,
15 get_release_file_url,19 get_release_file_url,
16 get_zookeeper_address,20 get_zookeeper_address,
21 JUJU_GUI_DIR,
22 JUJU_PEM,
23 log_hook,
17 parse_source,24 parse_source,
18 render_to_file,25 render_to_file,
19 save_or_create_certificates,26 save_or_create_certificates,
@@ -21,6 +28,7 @@
21 start_gui,28 start_gui,
22 start_improv,29 start_improv,
23 stop,30 stop,
31 WEB_PORT,
24)32)
25# Import the whole utils package for monkey patching.33# Import the whole utils package for monkey patching.
26import utils34import utils
@@ -261,6 +269,49 @@
261 self.assertEqual(self.zookeeper_address, address)269 self.assertEqual(self.zookeeper_address, address)
262270
263271
272class LogHookTest(unittest.TestCase):
273
274 def setUp(self):
275 # Monkeypatch the charmhelpers log function.
276 self.output = []
277 self.original = utils.log
278 utils.log = self.output.append
279
280 def tearDown(self):
281 # Restore the original charmhelpers log function.
282 utils.log = self.original
283
284 def test_logging(self):
285 # The function emits log messages on entering and exiting the hook.
286 with log_hook():
287 self.output.append('executing hook')
288 self.assertEqual(3, len(self.output))
289 enter_message, executing_message, exit_message = self.output
290 self.assertIn('>>> Entering', enter_message)
291 self.assertEqual('executing hook', executing_message)
292 self.assertIn('<<< Exiting', exit_message)
293
294 def test_subprocess_error(self):
295 # If a CalledProcessError exception is raised, the command output is
296 # logged.
297 with self.assertRaises(CalledProcessError) as cm:
298 with log_hook():
299 raise CalledProcessError(2, 'command', 'output')
300 exception = cm.exception
301 self.assertIsInstance(exception, CalledProcessError)
302 self.assertEqual(2, exception.returncode)
303 self.assertEqual('output', self.output[-2])
304
305 def test_error(self):
306 # Possible errors are re-raised by the context manager.
307 with self.assertRaises(TypeError) as cm:
308 with log_hook():
309 raise TypeError
310 exception = cm.exception
311 self.assertIsInstance(exception, TypeError)
312 self.assertIn('<<< Exiting', self.output[-1])
313
314
264class ParseSourceTest(unittest.TestCase):315class ParseSourceTest(unittest.TestCase):
265316
266 def test_latest_stable_release(self):317 def test_latest_stable_release(self):
@@ -295,9 +346,9 @@
295 def setUp(self):346 def setUp(self):
296 self.destination_file = tempfile.NamedTemporaryFile()347 self.destination_file = tempfile.NamedTemporaryFile()
297 self.addCleanup(self.destination_file.close)348 self.addCleanup(self.destination_file.close)
298 self.template_contents = '%(foo)s, %(bar)s'349 self.template = tempita.Template('{{foo}}, {{bar}}')
299 with tempfile.NamedTemporaryFile(delete=False) as template_file:350 with tempfile.NamedTemporaryFile(delete=False) as template_file:
300 template_file.write(self.template_contents)351 template_file.write(self.template.content)
301 self.template_path = template_file.name352 self.template_path = template_file.name
302 self.addCleanup(os.remove, self.template_path)353 self.addCleanup(os.remove, self.template_path)
303354
@@ -305,7 +356,7 @@
305 # Ensure the template is correctly rendered using the given context.356 # Ensure the template is correctly rendered using the given context.
306 context = {'foo': 'spam', 'bar': 'eggs'}357 context = {'foo': 'spam', 'bar': 'eggs'}
307 render_to_file(self.template_path, context, self.destination_file.name)358 render_to_file(self.template_path, context, self.destination_file.name)
308 expected = self.template_contents % context359 expected = self.template.substitute(context)
309 self.assertEqual(expected, self.destination_file.read())360 self.assertEqual(expected, self.destination_file.read())
310361
311362
@@ -331,6 +382,12 @@
331 self.assertIn('mycert', open(self.cert_file).read())382 self.assertIn('mycert', open(self.cert_file).read())
332 self.assertIn('mykey', open(self.key_file).read())383 self.assertIn('mykey', open(self.key_file).read())
333384
385 def test_pem_file(self):
386 # Ensure the pem file is created concatenating the key and cert files.
387 save_or_create_certificates(self.cert_path, 'Certificate', 'Key')
388 pem_file = os.path.join(self.cert_path, JUJU_PEM)
389 self.assertEqual('KeyCertificate', open(pem_file).read())
390
334391
335class CmdLogTest(unittest.TestCase):392class CmdLogTest(unittest.TestCase):
336 def setUp(self):393 def setUp(self):
@@ -401,12 +458,11 @@
401 charmhelpers.command = self.command458 charmhelpers.command = self.command
402459
403 def test_start_improv(self):460 def test_start_improv(self):
404 port = '1234'
405 staging_env = 'large'461 staging_env = 'large'
406 start_improv(port, staging_env, self.ssl_cert_path,462 start_improv(
407 self.destination_file.name)463 staging_env, self.ssl_cert_path, self.destination_file.name)
408 conf = self.destination_file.read()464 conf = self.destination_file.read()
409 self.assertTrue('--port %s' % port in conf)465 self.assertTrue('--port %s' % API_PORT in conf)
410 self.assertTrue(staging_env + '.json' in conf)466 self.assertTrue(staging_env + '.json' in conf)
411 self.assertTrue(self.ssl_cert_path in conf)467 self.assertTrue(self.ssl_cert_path in conf)
412 self.assertEqual(self.svc_ctl_call_count, 1)468 self.assertEqual(self.svc_ctl_call_count, 1)
@@ -414,10 +470,9 @@
414 self.assertEqual(self.actions, [charmhelpers.START])470 self.assertEqual(self.actions, [charmhelpers.START])
415471
416 def test_start_agent(self):472 def test_start_agent(self):
417 port = '1234'473 start_agent(self.ssl_cert_path, self.destination_file.name)
418 start_agent(port, self.ssl_cert_path, self.destination_file.name)
419 conf = self.destination_file.read()474 conf = self.destination_file.read()
420 self.assertTrue('--port %s' % port in conf)475 self.assertTrue('--port %s' % API_PORT in conf)
421 self.assertTrue('JUJU_ZOOKEEPER=%s' % self.fake_zk_address in conf)476 self.assertTrue('JUJU_ZOOKEEPER=%s' % self.fake_zk_address in conf)
422 self.assertTrue(self.ssl_cert_path in conf)477 self.assertTrue(self.ssl_cert_path in conf)
423 self.assertEqual(self.svc_ctl_call_count, 1)478 self.assertEqual(self.svc_ctl_call_count, 1)
@@ -425,41 +480,71 @@
425 self.assertEqual(self.actions, [charmhelpers.START])480 self.assertEqual(self.actions, [charmhelpers.START])
426481
427 def test_start_gui(self):482 def test_start_gui(self):
428 port = '1234'483 config_js_file = self.destination_file
484 haproxy_file = tempfile.NamedTemporaryFile()
485 self.addCleanup(haproxy_file.close)
429 nginx_file = tempfile.NamedTemporaryFile()486 nginx_file = tempfile.NamedTemporaryFile()
430 self.addCleanup(nginx_file.close)487 self.addCleanup(nginx_file.close)
431 config_js_file = tempfile.NamedTemporaryFile()488 ssl_cert_path = '/tmp/certificates/'
432 self.addCleanup(config_js_file.close)
433 start_gui(489 start_gui(
434 port, False, 'This is login help.', True, True,490 False, 'This is login help.', True, True, ssl_cert_path, True,
435 '/tmp/certificates/', self.destination_file.name, nginx_file.name,491 haproxy_path=haproxy_file.name, nginx_path=nginx_file.name,
436 config_js_file.name)492 config_js_path=config_js_file.name)
437 conf = self.destination_file.read()493 self.assertEqual(self.svc_ctl_call_count, 2)
438 self.assertTrue('/usr/sbin/nginx' in conf)494 self.assertEqual(self.service_names, ['nginx', 'haproxy'])
495 self.assertEqual(self.actions, [charmhelpers.START] * 2)
496 haproxy_conf = haproxy_file.read()
497 self.assertIn('ca-base {0}'.format(ssl_cert_path), haproxy_conf)
498 self.assertIn('crt-base {0}'.format(ssl_cert_path), haproxy_conf)
499 self.assertIn('ws1 127.0.0.1:{0}'.format(API_PORT), haproxy_conf)
500 self.assertIn('web1 127.0.0.1:{0}'.format(WEB_PORT), haproxy_conf)
501 self.assertIn('ca-file {0}'.format(JUJU_PEM), haproxy_conf)
502 self.assertIn('crt {0}'.format(JUJU_PEM), haproxy_conf)
503 self.assertIn('redirect scheme https', haproxy_conf)
439 js_conf = config_js_file.read()504 js_conf = config_js_file.read()
505 self.assertIn('consoleEnabled: false', js_conf)
440 self.assertIn('user: "admin"', js_conf)506 self.assertIn('user: "admin"', js_conf)
441 self.assertIn('password: "admin"', js_conf)507 self.assertIn('password: "admin"', js_conf)
442 self.assertIn('login_help: "This is login help."', js_conf)508 self.assertIn('login_help: "This is login help."', js_conf)
443 self.assertIn('readOnly: true', js_conf)509 self.assertIn('readOnly: true', js_conf)
510 self.assertIn("socket_url: 'wss://", js_conf)
511 self.assertIn('socket_protocol: "wss"', js_conf)
444 nginx_conf = nginx_file.read()512 nginx_conf = nginx_file.read()
445 self.assertTrue('juju-gui/build-debug' in nginx_conf)513 self.assertIn('juju-gui/build-', nginx_conf)
446 self.assertIn('/tmp/certificates/juju.crt', nginx_conf)514 self.assertIn('listen 127.0.0.1:{0}'.format(WEB_PORT), nginx_conf)
447 self.assertIn('/tmp/certificates/juju.key', nginx_conf)515 self.assertIn('alias {0}/test/;'.format(JUJU_GUI_DIR), nginx_conf)
448 self.assertEqual(self.svc_ctl_call_count, 1)516
449 self.assertEqual(self.service_names, ['juju-gui'])517 def test_start_gui_insecure(self):
450 self.assertEqual(self.actions, [charmhelpers.START])518 config_js_file = self.destination_file
519 haproxy_file = tempfile.NamedTemporaryFile()
520 self.addCleanup(haproxy_file.close)
521 nginx_file = tempfile.NamedTemporaryFile()
522 self.addCleanup(nginx_file.close)
523 ssl_cert_path = '/tmp/certificates/'
524 start_gui(
525 False, 'This is login help.', True, True, ssl_cert_path, True,
526 haproxy_path=haproxy_file.name, nginx_path=nginx_file.name,
527 config_js_path=config_js_file.name, secure=False)
528 js_conf = config_js_file.read()
529 self.assertIn("socket_url: 'ws://", js_conf)
530 self.assertIn('socket_protocol: "ws"', js_conf)
531 haproxy_conf = haproxy_file.read()
532 # The insecure approach eliminates the https redirect.
533 self.assertNotIn('redirect scheme https', haproxy_conf)
451534
452 def test_stop_staging(self):535 def test_stop_staging(self):
453 stop(True)536 stop(True)
454 self.assertEqual(self.svc_ctl_call_count, 2)537 self.assertEqual(self.svc_ctl_call_count, 3)
455 self.assertEqual(self.service_names, ['juju-gui', 'juju-api-improv'])538 self.assertEqual(
456 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])539 self.service_names, ['haproxy', 'nginx', 'juju-api-improv'])
540 self.assertEqual(self.actions, [charmhelpers.STOP] * 3)
457541
458 def test_stop_production(self):542 def test_stop_production(self):
459 stop(False)543 stop(False)
460 self.assertEqual(self.svc_ctl_call_count, 2)544 self.assertEqual(self.svc_ctl_call_count, 3)
461 self.assertEqual(self.service_names, ['juju-gui', 'juju-api-agent'])545 self.assertEqual(
462 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])546 self.service_names, ['haproxy', 'nginx', 'juju-api-agent'])
547 self.assertEqual(self.actions, [charmhelpers.STOP] * 3)
463548
464549
465if __name__ == '__main__':550if __name__ == '__main__':

Subscribers

People subscribed via source and target branches

to all changes: