Merge lp:~frankban/charms/precise/juju-gui/bug-1088618-serve-releases into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- bug-1088618-serve-releases
- Merge into trunk
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 18 | ||||||||
Proposed branch: | lp:~frankban/charms/precise/juju-gui/bug-1088618-serve-releases | ||||||||
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk | ||||||||
Diff against target: |
905 lines (+468/-106) 10 files modified
README.md (+2/-0) config.yaml (+11/-3) config/nginx.conf.template (+12/-8) hooks/config-changed (+28/-29) hooks/install (+13/-7) hooks/start (+2/-1) hooks/stop (+7/-1) hooks/utils.py (+140/-43) tests/deploy.test (+13/-3) tests/test_utils.py (+240/-11) |
||||||||
To merge this branch: | bzr merge lp:~frankban/charms/precise/juju-gui/bug-1088618-serve-releases | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+140745@code.launchpad.net |
Commit message
Description of the change
Updated the charm to serve Juju GUI releases.
I had a pre-implementation call with Gary and
some really useful help from Brad.
Details:
Replaced the juju-gui-branch config option with a new juju-gui-source
one: here you can specify where to deploy the GUI from (stable, trunk, a
branch, a specific version of stable/trunk).
Added a bunch of functions in utils.py: they help parsing the
juju-gui-source option, retrieving the URL of a release, etc.
Added tests for the utility functions above.
Changed the fetch/build function in utils.py, so that now a release
tarball is always used to install the GUI. It can be downloaded from
Launchpad or created using "make distfile" from a juju-gui checkout.
Also reorganized the fetch/build code: separated each of the fetch/build
functions into two different functions (one for the GUI, one for the API).
Updated the install and the config-changed hooks to conform to the
new fetch/setup functions (mentioned above).
Updated the config-changed hook to reflect changes to the options,
install hook and utils.py.
Added one functional test specific to deploying from a branch.
The other tests now default to the latest stable release.
Created and uploaded Juju GUI trunk/stable releases.
Updated the stop hook: now the function in utils.py is called with the
argument it expects.
Overall code lint and clean up.
All those changes result in a big diff (~820 lines), sorry about that.
Francesco Banconi (frankban) wrote : | # |
Gary Poster (gary) wrote : | # |
This looks great Francesco. :-)
I suggest that you ignore my bzr: idea (see below). We can add that if
we want it later.
I'm running tests for Nicola's branch now. I'll give your branch a try
after that, and then give an official blessing.
https:/
File hooks/utils.py (right):
https:/
hooks/utils.py:131: if source.startswith('lp:') or
source.startswith('http://
I wonder if we should support bzr: too. Might be nice. No biggie.
https:/
File tests/test_utils.py (right):
https:/
tests/test_
instances.
typo: returns
Gary Poster (gary) wrote : | # |
Good news and bad news. The good news is that the deployment took three
minutes instead of 20 for me. Rock! The bad news is that the tests did
not pass for me. I have to run now, but this is what I see so far.
$ time JUJU_REPOSITORY
/tmp --timeout 40m --no-bootstrap
2012-12-19 19:30:10,417 jitsu.test:INFO Running unit test: deploy.test
test_api_agent (__main_
test_branch_source (__main_
test_customized
test_staging (__main_
test_api_agent (__main_
If I see any helpful log messages I'll pass them on later. Anyway,
"Land with changes": please verify that the tests pass for you before
landing.
Thanks
Gary
Gary Poster (gary) wrote : | # |
On 2012/12/20 01:02:21, gary.poster wrote:
> Good news and bad news. The good news is that the deployment took
three minutes
> instead of 20 for me. Rock! The bad news is that the tests did not
pass for
> me. I have to run now, but this is what I see so far.
> $ time JUJU_REPOSITORY
/tmp
> --timeout 40m --no-bootstrap
> 2012-12-19 19:30:10,417 jitsu.test:INFO Running unit test: deploy.test
> test_api_agent (__main_
> test_branch_source (__main_
> test_customized
> test_staging (__main_
> test_api_agent (__main_
> If I see any helpful log messages I'll pass them on later. Anyway,
"Land with
> changes": please verify that the tests pass for you before landing.
> Thanks
> Gary
The unit tests all passed just fine, for what it is worth.
Unfortunately, the test logs that jitsu collected for the functional
tests don't appear to be informative.
The tests took 40 minutes and a few seconds, because the
DeployToTest.
test length time with jitsu test.
2012-12-19 19:30:10,417 jitsu.test:INFO Running unit test: deploy.test
test_api_agent (__main_
test_branch_source (__main_
test_customized
test_staging (__main_
test_api_agent (__main_
jitsu.test:WARNING Error running unit test deploy.test: 124
I have a vague suspicion that your branch of the charm doesn't like
installing on top of itself, but there could be many other explanations.
I really want to know what you do to debug these functional tests. I
did not get pdbs to work locally (stdout/stdin are not passed through
for this to work, AFAICT), and the logs collected by jitsu test left a
lot to be desired. I wanted to investigate the charm log, but we don't
get that from jitsu test. Maybe we ought to incorporate that into our
own test infrastructure? I have a weekly retrospective card to discuss
this, and see if you or anyone else have any good techniques, or ideas
about what we can do to improve jitsu test for debugging.
Thanks again
Gary
Kapil Thangavelu (hazmat) wrote : | # |
On Wed, Dec 19, 2012 at 9:34 PM, Gary Poster <email address hidden>wrote:
>
> I really want to know what you do to debug these functional tests. I
> did not get pdbs to work locally (stdout/stdin are not passed through
> for this to work, AFAICT), and the logs collected by jitsu test left a
> lot to be desired. I wanted to investigate the charm log, but we don't
> get that from jitsu test. Maybe we ought to incorporate that into our
> own test infrastructure? I have a weekly retrospective card to discuss
> this, and see if you or anyone else have any good techniques, or ideas
> about what we can do to improve jitsu test for debugging.
>
it should definitely include the charm.log if it doesn't thats a bug imo.
fwiw, lp:charmrunner has a tool for collecting *all* of the data available
in an environment (recorder.py)
Francesco Banconi (frankban) wrote : | # |
On 2012/12/20 03:32:06, gary.poster wrote:
> The unit tests all passed just fine, for what it is worth.
Cool.
19:30:10,417 jitsu.test:INFO Running unit test: deploy.test
> test_api_agent (__main_
> test_branch_source (__main_
> test_customized
> test_staging (__main_
> test_api_agent (__main_
> jitsu.test:WARNING Error running unit test deploy.test: 124
> I have a vague suspicion that your branch of the charm doesn't like
installing
> on top of itself, but there could be many other explanations.
It seems so looking at this output. This is really weird because the
tests always
pass when I run them. The last test in that output seems to be truncated
by the
timeout command used by jitsu test.
> I really want to know what you do to debug these functional tests. I
did not
> get pdbs to work locally (stdout/stdin are not passed through for this
to work,
> AFAICT), and the logs collected by jitsu test left a lot to be
desired.
I usually run jitsu test in a shell:
JUJU_REPOSITORY
--timeout 60m --logdir /home/frankban/
In parallel, in another shell, I run "juju debug-log", so that I can see
the output of all the hooks while they are run.
> I wanted to investigate the charm log, but we don't get that from
jitsu test.
The missing charm log exposes a problem. To simplify, here is what jitsu
test does:
for each *.test file in tests/:
bootstrap a juju env
execute the file (using the shell command "timeout --kill-after 2m
TIMEOUT file"
for each machine in the environment:
gather logs using rsync: everything in /var/log/juju and
/var/lib/
destroy the environment
In our tests, unittest.
This allows us to reuse the same machine in subsequent tests, where we
usually re-deploy
the service, testing different configurations. And this is a good idea
IMHO.
But this also raises a problem: "destroy service" removes the service
directory in
/var/lib/
Some ideas:
1) jitsu test seems to suggest you to run tests in separate files. This
would solve
the missing charm.log problem, but IMHO it's ugly:
- we can no longer use the --no-bootstrap option: we still need to
re-deploy the charm
multiple times, and we need a machine in a clean state;
- even if we can decrease the timeout for each test, the overall time
elapsed will be
a lot increased: for each test, we'd have to wait for the juju env to
be bootstrapped,
and for one or two machines to be started on ec2;
- re-deploying the charm in the same machine adds value to our tests,
because we also
check that our charm correctly cleans things up (e.g. services).
AFAIK, juju reuses
machines, at least when the provider is ec2, so this represents a real
use case;
- IMHO the functional tests, the way they are written now, are compact
and easy to read:
splitting them into different files doesn't seem something that should
be imposed
by the t...
- 38. By Francesco Banconi
-
Fixed typo.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
Gary Poster (gary) wrote : | # |
Thank you for the detailed debugging information, Francesco. I will experiment with it and it will be good to share it with everyone.
Since the tests pass for you, "Land as is." Hopefully I'll get my local issues sorted.
Gary
On Dec 20, 2012, at 5:25 AM, iFrancesco Banconi <email address hidden> wrote:
> Please take a look.
>
> https:/
>
> --
> https:/
> Your team Juju GUI Hackers is requested to review the proposed merge of lp:~frankban/charms/precise/juju-gui/bug-1088618-serve-releases into lp:~juju-gui/charms/precise/juju-gui/trunk.
>
> --
> Mailing list: https:/
> Post to : <email address hidden>
> Unsubscribe : https:/
> More help : https:/
Nicola Larosa (teknico) wrote : | # |
Land with changes.
Very nice refactoring job and new code, thanks.
A few small changes below, plus one code placement objection. Also, I
personally prefer that ".tar.gz" extension over the ".tgz" one, but I
guess it's personal taste.
https:/
File hooks/config-
https:/
hooks/config-
handled below.
The two lines above could be replaced by:
# Fetch new sources if needed, and restart GUI and API services
https:/
File hooks/utils.py (right):
https:/
hooks/utils.py:310: """Set up nginx."""
Well, nginx has nothing to do with the API environment, has it? It looks
like its setup code should be part of setup_gui, and setup_api should be
empty, for now. Shortly we will copy the nginx SSL certificate and
private key to the API environment, that's one thing that should go in
setup_api.
https:/
File tests/test_utils.py (right):
https:/
tests/test_
by launchpadlib."""
"Simulate..."
Gary Poster (gary) wrote : | # |
I was just reviewing where we were and had two comments. No change to
my previous review/blessing. :-)
Gary
https:/
File hooks/config-
https:/
hooks/config-
handled below.
On 2012/12/20 13:09:14, teknico wrote:
> The two lines above could be replaced by:
> # Fetch new sources if needed, and restart GUI and API services
I think the point of the comment is to reassure readers that these
things will be restarted, just not in this code block. "below" means
"not in this code block."
The comment worked for me. Would this have been clearer for you? "# The
juju_gui_
whether we restart the GUI and the API, respectively, at the end of the
function." Or something. Other ideas?
https:/
File hooks/utils.py (right):
https:/
hooks/utils.py:310: """Set up nginx."""
On 2012/12/20 13:09:14, teknico wrote:
> Well, nginx has nothing to do with the API environment, has it? It
looks like
> its setup code should be part of setup_gui, and setup_api should be
empty, for
> now. Shortly we will copy the nginx SSL certificate and private key to
the API
> environment, that's one thing that should go in setup_api.
Good point.
- 39. By Francesco Banconi
-
Changes per review.
Francesco Banconi (frankban) wrote : | # |
Hi Nicola,
thanks for the review.
https:/
File hooks/config-
https:/
hooks/config-
handled below.
On 2012/12/20 13:09:14, teknico wrote:
> The two lines above could be replaced by:
> # Fetch new sources if needed, and restart GUI and API services
This pre-existed and I think it's actually correct, the services are not
restarted here.
https:/
File hooks/utils.py (right):
https:/
hooks/utils.py:310: """Set up nginx."""
On 2012/12/20 13:09:14, teknico wrote:
> Well, nginx has nothing to do with the API environment, has it? It
looks like
> its setup code should be part of setup_gui, and setup_api should be
empty, for
> now. Shortly we will copy the nginx SSL certificate and private key to
the API
> environment, that's one thing that should go in setup_api.
Good catch, I'll rename this function.
https:/
File tests/test_utils.py (right):
https:/
tests/test_
by launchpadlib."""
On 2012/12/20 13:09:14, teknico wrote:
> "Simulate..."
Done.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
Nicola Larosa (teknico) wrote : | # |
https:/
File hooks/config-
https:/
hooks/config-
handled below.
gary.poster wrote:
> I think the point of the comment is to reassure readers that these
things
> will be restarted, just not in this code block. "below" means "not in
> this code block."
Oh, I see. I mistook "below" to mean "in the code below, within this
function".
> The comment worked for me. Would this have been clearer for you?
> "# The juju_gui_
> control whether we restart the GUI and the API, respectively, at the
end
> of the function." Or something. Other ideas?
Yes, maybe with s/at the end of the function./after this function./, or
similar.
frankban wrote:
> This pre-existed and I think it's actually correct, the services are
not
> restarted here.
Yes, it preexisted, but it was placed *after* a code block, making its
interpretation less ambiguous.
Francesco Banconi (frankban) wrote : | # |
https:/
File hooks/config-
https:/
hooks/config-
handled below.
On 2012/12/20 13:38:35, teknico wrote:
> Yes, it preexisted, but it was placed *after* a code block, making its
> interpretation less ambiguous.
Ok, replacing this comment with the one Gary suggested.
- 40. By Francesco Banconi
-
Fixed comment.
- 41. By Francesco Banconi
-
Merged trunk and resolved conflicts.
- 42. By Francesco Banconi
-
Turn off TLS.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Updated the charm to serve Juju GUI releases.
I had a pre-implementation call with Gary and
some really useful help from Brad.
Details:
Replaced the juju-gui-branch config option with a new juju-gui-source
one: here you can specify where to deploy the GUI from (stable, trunk, a
branch, a specific version of stable/trunk).
Added a bunch of functions in utils.py: they help parsing the
juju-gui-source option, retrieving the URL of a release, etc.
Added tests for the utility functions above.
Changed the fetch/build function in utils.py, so that now a release
tarball is always used to install the GUI. It can be downloaded from
Launchpad or created using "make distfile" from a juju-gui checkout.
Also reorganized the fetch/build code: separated each of the fetch/build
functions into two different functions (one for the GUI, one for the
API).
Updated the install and the config-changed hooks to conform to the
new fetch/setup functions (mentioned above).
Updated the config-changed hook to reflect changes to the options,
install hook and utils.py.
Added one functional test specific to deploying from a branch.
The other tests now default to the latest stable release.
Created and uploaded Juju GUI trunk/stable releases.
Updated the stop hook: now the function in utils.py is called with the
argument it expects.
Overall code lint and clean up.
All those changes result in a big diff (~820 lines), sorry about that.
R=gary.poster, teknico
CC=
https:/
Preview Diff
1 | === modified file 'README.md' | |||
2 | --- README.md 2012-12-19 11:49:41 +0000 | |||
3 | +++ README.md 2012-12-20 18:04:24 +0000 | |||
4 | @@ -58,7 +58,9 @@ | |||
5 | 58 | machine: 1 | 58 | machine: 1 |
6 | 59 | open-ports: | 59 | open-ports: |
7 | 60 | - 80/tcp | 60 | - 80/tcp |
8 | 61 | <!--- Uncomment when TLS connections are re-enabled. | ||
9 | 61 | - 443/tcp | 62 | - 443/tcp |
10 | 63 | --> | ||
11 | 62 | - 8080/tcp | 64 | - 8080/tcp |
12 | 63 | public-address: ec2-204-236-250-8.compute-1.amazonaws.com | 65 | public-address: ec2-204-236-250-8.compute-1.amazonaws.com |
13 | 64 | 66 | ||
14 | 65 | 67 | ||
15 | === modified file 'config.yaml' | |||
16 | --- config.yaml 2012-12-18 17:46:09 +0000 | |||
17 | +++ config.yaml 2012-12-20 18:04:24 +0000 | |||
18 | @@ -1,9 +1,17 @@ | |||
19 | 1 | options: | 1 | options: |
21 | 2 | juju-gui-branch: | 2 | juju-gui-source: |
22 | 3 | description: | | 3 | description: | |
24 | 4 | The Juju GUI Bazaar branch containing the source code to be deployed. | 4 | Where to install Juju GUI from. Possible values are: |
25 | 5 | - 'stable' (default): the latest stable release will be deployed; | ||
26 | 6 | - 'trunk': the latest trunk release will be deployed; | ||
27 | 7 | - a stable version (e.g '0.1.0'): the specified stable version will be | ||
28 | 8 | deployed; | ||
29 | 9 | - a trunk version (e.g '0.1.0+build.1'): the specified trunk version | ||
30 | 10 | will be deployed; | ||
31 | 11 | - a Bazaar branch (e.g. 'lp:juju-gui'): a release will be created and | ||
32 | 12 | deployed from the specified Bazaar branch. | ||
33 | 5 | type: string | 13 | type: string |
35 | 6 | default: lp:juju-gui | 14 | default: stable |
36 | 7 | juju-api-branch: | 15 | juju-api-branch: |
37 | 8 | description: | | 16 | description: | |
38 | 9 | The Juju API Bazaar branch (implementing the websocket server). | 17 | The Juju API Bazaar branch (implementing the websocket server). |
39 | 10 | 18 | ||
40 | === modified file 'config/nginx.conf.template' | |||
41 | --- config/nginx.conf.template 2012-12-19 17:24:21 +0000 | |||
42 | +++ config/nginx.conf.template 2012-12-20 18:04:24 +0000 | |||
43 | @@ -1,16 +1,20 @@ | |||
49 | 1 | server { | 1 | # Uncomment to switch back to TLS connections. |
50 | 2 | listen 80; | 2 | # server { |
51 | 3 | server_name _; | 3 | # listen 80; |
52 | 4 | return 301 https://$host$request_uri; | 4 | # server_name _; |
53 | 5 | } | 5 | # return 301 https://$host$request_uri; |
54 | 6 | # } | ||
55 | 6 | 7 | ||
56 | 7 | server { | 8 | server { |
58 | 8 | listen 443 default_server ssl; | 9 | # Uncomment to switch back to TLS connections. |
59 | 10 | # listen 443 default_server ssl; | ||
60 | 11 | listen 80; # Delete this line when TLS connections are re-enabled. | ||
61 | 9 | server_name _; | 12 | server_name _; |
62 | 10 | root %(server_root)s; | 13 | root %(server_root)s; |
63 | 11 | index index.html; | 14 | index index.html; |
66 | 12 | ssl_certificate /etc/ssl/private/juju-gui/server.pem; | 15 | # Uncomment to switch back to TLS connections. |
67 | 13 | ssl_certificate_key /etc/ssl/private/juju-gui/server.key; | 16 | # ssl_certificate /etc/ssl/private/juju-gui/server.pem; |
68 | 17 | # ssl_certificate_key /etc/ssl/private/juju-gui/server.key; | ||
69 | 14 | 18 | ||
70 | 15 | # Serve static assets. | 19 | # Serve static assets. |
71 | 16 | location ^~ /juju-ui/ { | 20 | location ^~ /juju-ui/ { |
72 | 17 | 21 | ||
73 | === modified file 'hooks/config-changed' | |||
74 | --- hooks/config-changed 2012-12-19 17:24:21 +0000 | |||
75 | +++ hooks/config-changed 2012-12-20 18:04:24 +0000 | |||
76 | @@ -4,8 +4,6 @@ | |||
77 | 4 | # Copyright 2012 Canonical Ltd. This software is licensed under the | 4 | # Copyright 2012 Canonical Ltd. This software is licensed under the |
78 | 5 | # GNU Affero General Public License version 3 (see the file LICENSE). | 5 | # GNU Affero General Public License version 3 (see the file LICENSE). |
79 | 6 | 6 | ||
80 | 7 | import os.path | ||
81 | 8 | from shutil import rmtree | ||
82 | 9 | from subprocess import CalledProcessError | 7 | from subprocess import CalledProcessError |
83 | 10 | import sys | 8 | import sys |
84 | 11 | 9 | ||
85 | @@ -24,11 +22,13 @@ | |||
86 | 24 | 22 | ||
87 | 25 | from utils import ( | 23 | from utils import ( |
88 | 26 | AGENT, | 24 | AGENT, |
89 | 27 | build, | ||
90 | 28 | config_json, | 25 | config_json, |
92 | 29 | fetch, | 26 | fetch_api, |
93 | 27 | fetch_gui, | ||
94 | 30 | GUI, | 28 | GUI, |
95 | 31 | IMPROV, | 29 | IMPROV, |
96 | 30 | setup_gui, | ||
97 | 31 | setup_nginx, | ||
98 | 32 | start_agent, | 32 | start_agent, |
99 | 33 | start_gui, | 33 | start_gui, |
100 | 34 | start_improv, | 34 | start_improv, |
101 | @@ -39,33 +39,33 @@ | |||
102 | 39 | # Handle all configuration file changes. | 39 | # Handle all configuration file changes. |
103 | 40 | log('Updating configuration.') | 40 | log('Updating configuration.') |
104 | 41 | 41 | ||
105 | 42 | added_or_changed = diff.added_or_changed | ||
106 | 43 | juju_api_port = config.get('juju-api-port') | ||
107 | 42 | staging = config.get('staging') | 44 | staging = config.get('staging') |
119 | 43 | staging_environment = config.get('staging-environment') | 45 | |
120 | 44 | juju_api_port = config.get('juju-api-port') | 46 | # The juju_gui_source_changed and juju_api_branch_changed variables |
121 | 45 | added_or_changed = diff.added_or_changed | 47 | # control whether we restart the GUI and the API, respectively, at the |
122 | 46 | 48 | # end of the function. | |
123 | 47 | # Fetch new branches? | 49 | juju_gui_source_changed = False |
124 | 48 | gui_branch = None | 50 | juju_api_branch_changed = False |
125 | 49 | api_branch = None | 51 | |
126 | 50 | if 'juju-gui-branch' in added_or_changed: | 52 | # Fetch new sources? |
127 | 51 | if os.path.exists('juju-gui'): | 53 | if 'juju-gui-source' in added_or_changed: |
128 | 52 | rmtree('juju-gui') | 54 | juju_gui_source_changed = True |
129 | 53 | gui_branch = config['juju-gui-branch'] | 55 | release_tarball = fetch_gui( |
130 | 56 | config['juju-gui-source'], config['command-log-file']) | ||
131 | 57 | setup_gui(release_tarball) | ||
132 | 58 | setup_nginx(config['ssl-cert-path']) | ||
133 | 54 | if 'juju-api-branch' in added_or_changed: | 59 | if 'juju-api-branch' in added_or_changed: |
141 | 55 | if os.path.exists('juju'): | 60 | juju_api_branch_changed = True |
142 | 56 | rmtree('juju') | 61 | fetch_api(config['juju-api-branch']) |
136 | 57 | api_branch = config['juju-api-branch'] | ||
137 | 58 | if gui_branch or api_branch: | ||
138 | 59 | fetch(gui_branch, api_branch) | ||
139 | 60 | build(config['command-log-file'], config['ssl-cert-path']) | ||
140 | 61 | # Restarting of the gui and api services is handled below. | ||
143 | 62 | 62 | ||
144 | 63 | # Handle changes to the improv server configuration. | 63 | # Handle changes to the improv server configuration. |
145 | 64 | if staging: | 64 | if staging: |
146 | 65 | staging_properties = set( | 65 | staging_properties = set( |
147 | 66 | ['staging', 'staging-environment', 'juju-api-port']) | 66 | ['staging', 'staging-environment', 'juju-api-port']) |
148 | 67 | staging_changed = added_or_changed & staging_properties | 67 | staging_changed = added_or_changed & staging_properties |
150 | 68 | if staging_changed or (api_branch is not None): | 68 | if staging_changed or juju_api_branch_changed: |
151 | 69 | if 'staging' in added_or_changed: | 69 | if 'staging' in added_or_changed: |
152 | 70 | # 'staging' went from False to True, so the agent server is | 70 | # 'staging' went from False to True, so the agent server is |
153 | 71 | # running and must be stopped. | 71 | # running and must be stopped. |
154 | @@ -76,14 +76,13 @@ | |||
155 | 76 | current_api = IMPROV | 76 | current_api = IMPROV |
156 | 77 | log('Stopping %s.' % current_api) | 77 | log('Stopping %s.' % current_api) |
157 | 78 | service_control(current_api, STOP) | 78 | service_control(current_api, STOP) |
158 | 79 | |||
159 | 80 | # Now the improv server can be cleanly started. | 79 | # Now the improv server can be cleanly started. |
162 | 81 | log('Starting or restarting improv') | 80 | log('Starting or restarting staging.') |
163 | 82 | start_improv(juju_api_port, staging_environment) | 81 | start_improv(juju_api_port, config.get('staging-environment')) |
164 | 83 | else: | 82 | else: |
165 | 84 | agent_properties = set(['juju-api-port', 'staging']) | 83 | agent_properties = set(['juju-api-port', 'staging']) |
166 | 85 | agent_changed = added_or_changed & agent_properties | 84 | agent_changed = added_or_changed & agent_properties |
168 | 86 | if agent_changed or (api_branch is not None): | 85 | if agent_changed or juju_api_branch_changed: |
169 | 87 | if 'staging' in added_or_changed: | 86 | if 'staging' in added_or_changed: |
170 | 88 | # If 'staging' transitions to False we need to stop the backend | 87 | # If 'staging' transitions to False we need to stop the backend |
171 | 89 | # and start the agent. | 88 | # and start the agent. |
172 | @@ -93,15 +92,15 @@ | |||
173 | 93 | # updated -- bounce it. | 92 | # updated -- bounce it. |
174 | 94 | current_api = AGENT | 93 | current_api = AGENT |
175 | 95 | service_control(current_api, STOP) | 94 | service_control(current_api, STOP) |
176 | 95 | log('Starting or restarting Juju API agent.') | ||
177 | 96 | start_agent(juju_api_port) | 96 | start_agent(juju_api_port) |
178 | 97 | 97 | ||
179 | 98 | # Handle changes to the juju-gui configuration. | 98 | # Handle changes to the juju-gui configuration. |
180 | 99 | gui_properties = set( | 99 | gui_properties = set( |
181 | 100 | ['juju-gui-console-enabled', 'juju-api-port', 'staging']) | 100 | ['juju-gui-console-enabled', 'juju-api-port', 'staging']) |
182 | 101 | gui_changed = added_or_changed & gui_properties | 101 | gui_changed = added_or_changed & gui_properties |
184 | 102 | if gui_changed or (gui_branch is not None): | 102 | if gui_changed or juju_gui_source_changed: |
185 | 103 | with su('root'): | 103 | with su('root'): |
186 | 104 | service_control('nginx', STOP) | ||
187 | 105 | service_control(GUI, STOP) | 104 | service_control(GUI, STOP) |
188 | 106 | console_enabled = config.get('juju-gui-console-enabled') | 105 | console_enabled = config.get('juju-gui-console-enabled') |
189 | 107 | start_gui(juju_api_port, console_enabled, staging) | 106 | start_gui(juju_api_port, console_enabled, staging) |
190 | 108 | 107 | ||
191 | === modified file 'hooks/install' | |||
192 | --- hooks/install 2012-12-18 17:46:09 +0000 | |||
193 | +++ hooks/install 2012-12-20 18:04:24 +0000 | |||
194 | @@ -4,7 +4,7 @@ | |||
195 | 4 | from subprocess import ( | 4 | from subprocess import ( |
196 | 5 | CalledProcessError, | 5 | CalledProcessError, |
197 | 6 | check_call, | 6 | check_call, |
199 | 7 | ) | 7 | ) |
200 | 8 | 8 | ||
201 | 9 | # python-shelltoolbox is installed as a dependency of python-charmhelpers. | 9 | # python-shelltoolbox is installed as a dependency of python-charmhelpers. |
202 | 10 | check_call(['apt-get', 'install', '-y', 'python-charmhelpers']) | 10 | check_call(['apt-get', 'install', '-y', 'python-charmhelpers']) |
203 | @@ -24,16 +24,19 @@ | |||
204 | 24 | ) | 24 | ) |
205 | 25 | 25 | ||
206 | 26 | from utils import ( | 26 | from utils import ( |
207 | 27 | build, | ||
208 | 28 | cmd_log, | 27 | cmd_log, |
209 | 29 | config_json, | 28 | config_json, |
212 | 30 | fetch, | 29 | fetch_api, |
213 | 31 | ) | 30 | fetch_gui, |
214 | 31 | setup_gui, | ||
215 | 32 | setup_nginx, | ||
216 | 33 | ) | ||
217 | 32 | 34 | ||
218 | 33 | 35 | ||
219 | 34 | DEB_DEPENDENCIES = ( | 36 | DEB_DEPENDENCIES = ( |
220 | 35 | 'bzr', 'imagemagick', 'make', 'nginx', 'nodejs', 'npm', 'openssl', | 37 | 'bzr', 'imagemagick', 'make', 'nginx', 'nodejs', 'npm', 'openssl', |
222 | 36 | 'zookeeper') | 38 | 'python-launchpadlib', 'zookeeper', |
223 | 39 | ) | ||
224 | 37 | 40 | ||
225 | 38 | 41 | ||
226 | 39 | def get_dependencies(): | 42 | def get_dependencies(): |
227 | @@ -45,8 +48,11 @@ | |||
228 | 45 | def main(): | 48 | def main(): |
229 | 46 | config = get_config() | 49 | config = get_config() |
230 | 47 | get_dependencies() | 50 | get_dependencies() |
233 | 48 | fetch(config['juju-gui-branch'], config['juju-api-branch']) | 51 | release_tarball = fetch_gui( |
234 | 49 | build(config['command-log-file'], config['ssl-cert-path']) | 52 | config['juju-gui-source'], config['command-log-file']) |
235 | 53 | setup_gui(release_tarball) | ||
236 | 54 | setup_nginx(config['ssl-cert-path']) | ||
237 | 55 | fetch_api(config['juju-api-branch']) | ||
238 | 50 | config_json.set(config) | 56 | config_json.set(config) |
239 | 51 | 57 | ||
240 | 52 | 58 | ||
241 | 53 | 59 | ||
242 | === modified file 'hooks/start' | |||
243 | --- hooks/start 2012-12-19 11:49:41 +0000 | |||
244 | +++ hooks/start 2012-12-20 18:04:24 +0000 | |||
245 | @@ -21,7 +21,8 @@ | |||
246 | 21 | log('Exposing services.') | 21 | log('Exposing services.') |
247 | 22 | # Open the Juju GUI web server HTTP and HTTPS ports. | 22 | # Open the Juju GUI web server HTTP and HTTPS ports. |
248 | 23 | open_port(80) | 23 | open_port(80) |
250 | 24 | open_port(443) | 24 | # Uncomment to switch back to TLS connections. |
251 | 25 | # open_port(443) | ||
252 | 25 | # Open the Juju websocket server port. | 26 | # Open the Juju websocket server port. |
253 | 26 | open_port(juju_api_port) | 27 | open_port(juju_api_port) |
254 | 27 | 28 | ||
255 | 28 | 29 | ||
256 | === modified file 'hooks/stop' | |||
257 | --- hooks/stop 2012-12-18 13:23:42 +0000 | |||
258 | +++ hooks/stop 2012-12-20 18:04:24 +0000 | |||
259 | @@ -2,6 +2,7 @@ | |||
260 | 2 | #-*- python -*- | 2 | #-*- python -*- |
261 | 3 | 3 | ||
262 | 4 | from charmhelpers import ( | 4 | from charmhelpers import ( |
263 | 5 | get_config, | ||
264 | 5 | log_entry, | 6 | log_entry, |
265 | 6 | log_exit, | 7 | log_exit, |
266 | 7 | ) | 8 | ) |
267 | @@ -9,9 +10,14 @@ | |||
268 | 9 | from utils import stop | 10 | from utils import stop |
269 | 10 | 11 | ||
270 | 11 | 12 | ||
271 | 13 | def main(): | ||
272 | 14 | config = get_config() | ||
273 | 15 | stop(config.get('staging')) | ||
274 | 16 | |||
275 | 17 | |||
276 | 12 | if __name__ == '__main__': | 18 | if __name__ == '__main__': |
277 | 13 | log_entry() | 19 | log_entry() |
278 | 14 | try: | 20 | try: |
280 | 15 | stop() | 21 | main() |
281 | 16 | finally: | 22 | finally: |
282 | 17 | log_exit() | 23 | log_exit() |
283 | 18 | 24 | ||
284 | === modified file 'hooks/utils.py' | |||
285 | --- hooks/utils.py 2012-12-19 11:49:41 +0000 | |||
286 | +++ hooks/utils.py 2012-12-20 18:04:24 +0000 | |||
287 | @@ -2,17 +2,27 @@ | |||
288 | 2 | 2 | ||
289 | 3 | __all__ = [ | 3 | __all__ = [ |
290 | 4 | 'AGENT', | 4 | 'AGENT', |
292 | 5 | 'build', | 5 | 'bzr_checkout', |
293 | 6 | 'cmd_log', | 6 | 'cmd_log', |
294 | 7 | 'CURRENT_DIR', | ||
295 | 8 | 'fetch_api', | ||
296 | 9 | 'fetch_gui', | ||
297 | 10 | 'first_path_in_dir', | ||
298 | 11 | 'get_release_file_url', | ||
299 | 7 | 'get_zookeeper_address', | 12 | 'get_zookeeper_address', |
300 | 8 | 'GUI', | 13 | 'GUI', |
301 | 9 | 'IMPROV', | 14 | 'IMPROV', |
302 | 15 | 'JUJU_DIR', | ||
303 | 16 | 'JUJU_GUI_DIR', | ||
304 | 17 | 'parse_source', | ||
305 | 10 | 'render_to_file', | 18 | 'render_to_file', |
306 | 19 | 'setup_gui', | ||
307 | 20 | 'setup_nginx', | ||
308 | 11 | 'start_agent', | 21 | 'start_agent', |
309 | 12 | 'start_gui', | 22 | 'start_gui', |
310 | 13 | 'start_improv', | 23 | 'start_improv', |
311 | 14 | 'stop', | 24 | 'stop', |
313 | 15 | ] | 25 | ] |
314 | 16 | 26 | ||
315 | 17 | import json | 27 | import json |
316 | 18 | import os | 28 | import os |
317 | @@ -20,15 +30,15 @@ | |||
318 | 20 | import shutil | 30 | import shutil |
319 | 21 | import tempfile | 31 | import tempfile |
320 | 22 | 32 | ||
321 | 33 | from launchpadlib.launchpad import Launchpad | ||
322 | 23 | from shelltoolbox import ( | 34 | from shelltoolbox import ( |
323 | 24 | cd, | ||
324 | 25 | command, | 35 | command, |
325 | 26 | environ, | 36 | environ, |
326 | 27 | run, | 37 | run, |
327 | 28 | search_file, | 38 | search_file, |
328 | 29 | Serializer, | 39 | Serializer, |
329 | 30 | su, | 40 | su, |
331 | 31 | ) | 41 | ) |
332 | 32 | from charmhelpers import ( | 42 | from charmhelpers import ( |
333 | 33 | get_config, | 43 | get_config, |
334 | 34 | log, | 44 | log, |
335 | @@ -48,6 +58,50 @@ | |||
336 | 48 | 58 | ||
337 | 49 | # Store the configuration from on invocation to the next. | 59 | # Store the configuration from on invocation to the next. |
338 | 50 | config_json = Serializer('/tmp/config.json') | 60 | config_json = Serializer('/tmp/config.json') |
339 | 61 | # Bazaar checkout command. | ||
340 | 62 | bzr_checkout = command('bzr', 'co', '--lightweight') | ||
341 | 63 | |||
342 | 64 | |||
343 | 65 | def first_path_in_dir(directory): | ||
344 | 66 | """Return the full path of the first file/dir in *directory*.""" | ||
345 | 67 | return os.path.join(directory, os.listdir(directory)[0]) | ||
346 | 68 | |||
347 | 69 | |||
348 | 70 | def _get_by_attr(collection, attr, value): | ||
349 | 71 | """Return the first item in collection having attr == value. | ||
350 | 72 | |||
351 | 73 | Return None if the item is not found. | ||
352 | 74 | """ | ||
353 | 75 | for item in collection: | ||
354 | 76 | if getattr(item, attr) == value: | ||
355 | 77 | return item | ||
356 | 78 | |||
357 | 79 | |||
358 | 80 | def get_release_file_url(project, series_name, release_version): | ||
359 | 81 | """Return the URL of the release file hosted in Launchpad. | ||
360 | 82 | |||
361 | 83 | The returned URL points to a release file for the given project, series | ||
362 | 84 | name and release version. | ||
363 | 85 | The argument *project* is a project object as returned by launchpadlib. | ||
364 | 86 | The arguments *series_name* and *release_version* are strings. If | ||
365 | 87 | *release_version* is None, the URL of the latest release will be returned. | ||
366 | 88 | """ | ||
367 | 89 | series = _get_by_attr(project.series, 'name', series_name) | ||
368 | 90 | if series is None: | ||
369 | 91 | raise ValueError('%r: series not found' % series_name) | ||
370 | 92 | releases = list(series.releases) | ||
371 | 93 | if not releases: | ||
372 | 94 | raise ValueError('%r: series does not contain releases' % series_name) | ||
373 | 95 | if release_version is None: | ||
374 | 96 | release = releases[0] | ||
375 | 97 | else: | ||
376 | 98 | release = _get_by_attr(releases, 'version', release_version) | ||
377 | 99 | if not release: | ||
378 | 100 | raise ValueError('%r: release not found' % release_version) | ||
379 | 101 | files = [i for i in release.files if str(i).endswith('.tgz')] | ||
380 | 102 | if not files: | ||
381 | 103 | raise ValueError('%r: file not found' % release_version) | ||
382 | 104 | return files[0].file_link | ||
383 | 51 | 105 | ||
384 | 52 | 106 | ||
385 | 53 | def get_zookeeper_address(agent_file_path): | 107 | def get_zookeeper_address(agent_file_path): |
386 | @@ -62,6 +116,26 @@ | |||
387 | 62 | return line.split('=')[1].strip('"') | 116 | return line.split('=')[1].strip('"') |
388 | 63 | 117 | ||
389 | 64 | 118 | ||
390 | 119 | def parse_source(source): | ||
391 | 120 | """Parse the ``juju-gui-source`` option. | ||
392 | 121 | |||
393 | 122 | Return a tuple of two elements representing info on how to deploy Juju GUI. | ||
394 | 123 | Examples: | ||
395 | 124 | - ('stable', None): latest stable release; | ||
396 | 125 | - ('stable', '0.1.0'): stable release v0.1.0; | ||
397 | 126 | - ('trunk', None): latest trunk release; | ||
398 | 127 | - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1; | ||
399 | 128 | - ('branch', 'lp:juju-gui'): release is made from a branch. | ||
400 | 129 | """ | ||
401 | 130 | if source in ('stable', 'trunk'): | ||
402 | 131 | return source, None | ||
403 | 132 | if source.startswith('lp:') or source.startswith('http://'): | ||
404 | 133 | return 'branch', source | ||
405 | 134 | if 'build' in source: | ||
406 | 135 | return 'trunk', source | ||
407 | 136 | return 'stable', source | ||
408 | 137 | |||
409 | 138 | |||
410 | 65 | def render_to_file(template, context, destination): | 139 | def render_to_file(template, context, destination): |
411 | 66 | """Render the given *template* into *destination* using *context*. | 140 | """Render the given *template* into *destination* using *context*. |
412 | 67 | 141 | ||
413 | @@ -112,9 +186,7 @@ | |||
414 | 112 | 'port': juju_api_port, | 186 | 'port': juju_api_port, |
415 | 113 | 'staging_env': staging_env, | 187 | 'staging_env': staging_env, |
416 | 114 | } | 188 | } |
420 | 115 | render_to_file( | 189 | render_to_file('juju-api-improv.conf.template', context, config_path) |
418 | 116 | 'juju-api-improv.conf.template', context, | ||
419 | 117 | config_path) | ||
421 | 118 | log('Starting the staging backend.') | 190 | log('Starting the staging backend.') |
422 | 119 | with su('root'): | 191 | with su('root'): |
423 | 120 | service_control(IMPROV, START) | 192 | service_control(IMPROV, START) |
424 | @@ -132,9 +204,7 @@ | |||
425 | 132 | 'port': juju_api_port, | 204 | 'port': juju_api_port, |
426 | 133 | 'zookeeper': zookeeper, | 205 | 'zookeeper': zookeeper, |
427 | 134 | } | 206 | } |
431 | 135 | render_to_file( | 207 | render_to_file('juju-api-agent.conf.template', context, config_path) |
429 | 136 | 'juju-api-agent.conf.template', context, | ||
430 | 137 | config_path) | ||
432 | 138 | log('Starting API agent.') | 208 | log('Starting API agent.') |
433 | 139 | with su('root'): | 209 | with su('root'): |
434 | 140 | service_control(AGENT, START) | 210 | service_control(AGENT, START) |
435 | @@ -150,8 +220,7 @@ | |||
436 | 150 | build_dir = JUJU_GUI_DIR + '/build-' | 220 | build_dir = JUJU_GUI_DIR + '/build-' |
437 | 151 | build_dir += 'debug' if staging else 'prod' | 221 | build_dir += 'debug' if staging else 'prod' |
438 | 152 | log('Setting up Juju GUI start up script.') | 222 | log('Setting up Juju GUI start up script.') |
441 | 153 | render_to_file( | 223 | render_to_file('juju-gui.conf.template', {}, config_path) |
440 | 154 | 'juju-gui.conf.template', {}, config_path) | ||
442 | 155 | log('Generating the Juju GUI configuration file.') | 224 | log('Generating the Juju GUI configuration file.') |
443 | 156 | context = { | 225 | context = { |
444 | 157 | 'address': unit_get('public-address'), | 226 | 'address': unit_get('public-address'), |
445 | @@ -161,29 +230,24 @@ | |||
446 | 161 | if config_js_path is None: | 230 | if config_js_path is None: |
447 | 162 | config_js_path = os.path.join( | 231 | config_js_path = os.path.join( |
448 | 163 | build_dir, 'juju-ui', 'assets', 'config.js') | 232 | build_dir, 'juju-ui', 'assets', 'config.js') |
452 | 164 | render_to_file( | 233 | render_to_file('config.js.template', context, config_js_path) |
450 | 165 | 'config.js.template', context, | ||
451 | 166 | config_js_path) | ||
453 | 167 | log('Generating the nginx site configuration file.') | 234 | log('Generating the nginx site configuration file.') |
454 | 168 | context = { | 235 | context = { |
455 | 169 | 'server_root': build_dir | 236 | 'server_root': build_dir |
456 | 170 | } | 237 | } |
459 | 171 | render_to_file( | 238 | render_to_file('nginx.conf.template', context, nginx_path) |
458 | 172 | 'nginx.conf.template', context, nginx_path) | ||
460 | 173 | log('Starting Juju GUI.') | 239 | log('Starting Juju GUI.') |
461 | 174 | with su('root'): | 240 | with su('root'): |
464 | 175 | # Stop nginx so it will restart cleanly with the gui. | 241 | # Start the Juju GUI. |
463 | 176 | service_control('nginx', STOP) | ||
465 | 177 | service_control(GUI, START) | 242 | service_control(GUI, START) |
466 | 178 | 243 | ||
467 | 179 | 244 | ||
469 | 180 | def stop(): | 245 | def stop(staging): |
470 | 181 | """Stop the Juju API agent.""" | 246 | """Stop the Juju API agent.""" |
471 | 182 | config = get_config() | ||
472 | 183 | with su('root'): | 247 | with su('root'): |
473 | 184 | log('Stopping Juju GUI.') | 248 | log('Stopping Juju GUI.') |
474 | 185 | service_control(GUI, STOP) | 249 | service_control(GUI, STOP) |
476 | 186 | if config.get('staging'): | 250 | if staging: |
477 | 187 | log('Stopping the staging backend.') | 251 | log('Stopping the staging backend.') |
478 | 188 | service_control(IMPROV, STOP) | 252 | service_control(IMPROV, STOP) |
479 | 189 | else: | 253 | else: |
480 | @@ -191,27 +255,60 @@ | |||
481 | 191 | service_control(AGENT, STOP) | 255 | service_control(AGENT, STOP) |
482 | 192 | 256 | ||
483 | 193 | 257 | ||
505 | 194 | def fetch(juju_gui_branch, juju_api_branch): | 258 | def fetch_gui(juju_gui_source, logpath): |
506 | 195 | """Install required dependencies and retrieve Juju/Juju GUI branches.""" | 259 | """Retrieve the Juju GUI release/branch.""" |
507 | 196 | log('Retrieving source checkouts.') | 260 | # Retrieve a Juju GUI release. |
508 | 197 | bzr_checkout = command('bzr', 'co', '--lightweight') | 261 | origin, version_or_branch = parse_source(juju_gui_source) |
509 | 198 | if juju_gui_branch is not None: | 262 | if origin == 'branch': |
510 | 199 | cmd_log(run('rm', '-rf', 'juju-gui')) | 263 | # Create a release starting from a branch. |
511 | 200 | cmd_log(bzr_checkout(juju_gui_branch, 'juju-gui')) | 264 | juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source') |
512 | 201 | if juju_api_branch is not None: | 265 | log('Retrieving Juju GUI source checkouts.') |
513 | 202 | cmd_log(run('rm', '-rf', 'juju')) | 266 | cmd_log(run('rm', '-rf', juju_gui_source_dir)) |
514 | 203 | cmd_log(bzr_checkout(juju_api_branch, 'juju')) | 267 | cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir)) |
515 | 204 | 268 | log('Preparing a Juju GUI release.') | |
516 | 205 | 269 | logdir = os.path.dirname(logpath) | |
517 | 206 | def build(logpath, ssl_cert_path): | 270 | fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir) |
518 | 207 | """Set up Juju GUI and nginx.""" | 271 | log('Output from "make distfile" sent to', name) |
519 | 208 | log('Building Juju GUI.') | 272 | with environ(NO_BZR='1'): |
520 | 209 | with environ(NO_BZR='1'): | 273 | run('make', '-C', juju_gui_source_dir, 'distfile', |
521 | 210 | with cd('juju-gui'): | 274 | stdout=fd, stderr=fd) |
522 | 211 | logdir = os.path.dirname(logpath) | 275 | release_tarball = first_path_in_dir( |
523 | 212 | fd, name = tempfile.mkstemp(prefix='make-', dir=logdir) | 276 | os.path.join(juju_gui_source_dir, 'releases')) |
524 | 213 | log('Output from "make" sent to', name) | 277 | else: |
525 | 214 | run('make', stdout=fd, stderr=fd) | 278 | # Retrieve a release from Launchpad. |
526 | 279 | log('Retrieving Juju GUI release.') | ||
527 | 280 | launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') | ||
528 | 281 | project = launchpad.projects['juju-gui'] | ||
529 | 282 | file_url = get_release_file_url(project, origin, version_or_branch) | ||
530 | 283 | log('Downloading release file from %s.' % file_url) | ||
531 | 284 | release_tarball = os.path.join(CURRENT_DIR, 'release.tgz') | ||
532 | 285 | cmd_log(run('curl', '-L', '-o', release_tarball, file_url)) | ||
533 | 286 | return release_tarball | ||
534 | 287 | |||
535 | 288 | |||
536 | 289 | def fetch_api(juju_api_branch): | ||
537 | 290 | """Retrieve the Juju branch.""" | ||
538 | 291 | # Retrieve Juju API source checkout. | ||
539 | 292 | log('Retrieving Juju API source checkout.') | ||
540 | 293 | cmd_log(run('rm', '-rf', JUJU_DIR)) | ||
541 | 294 | cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR)) | ||
542 | 295 | |||
543 | 296 | |||
544 | 297 | def setup_gui(release_tarball): | ||
545 | 298 | """Set up Juju GUI.""" | ||
546 | 299 | # Uncompress the release tarball. | ||
547 | 300 | log('Installing Juju GUI.') | ||
548 | 301 | release_dir = os.path.join(CURRENT_DIR, 'release') | ||
549 | 302 | cmd_log(run('rm', '-rf', release_dir)) | ||
550 | 303 | os.mkdir(release_dir) | ||
551 | 304 | uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f') | ||
552 | 305 | cmd_log(uncompress(release_tarball)) | ||
553 | 306 | # Link the Juju GUI dir to the contents of the release tarball. | ||
554 | 307 | cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR)) | ||
555 | 308 | |||
556 | 309 | |||
557 | 310 | def setup_nginx(ssl_cert_path): | ||
558 | 311 | """Set up nginx.""" | ||
559 | 215 | log('Setting up nginx.') | 312 | log('Setting up nginx.') |
560 | 216 | nginx_default_site = '/etc/nginx/sites-enabled/default' | 313 | nginx_default_site = '/etc/nginx/sites-enabled/default' |
561 | 217 | juju_gui_site = '/etc/nginx/sites-available/juju-gui' | 314 | juju_gui_site = '/etc/nginx/sites-available/juju-gui' |
562 | 218 | 315 | ||
563 | === modified file 'tests/deploy.test' | |||
564 | --- tests/deploy.test 2012-12-20 10:52:39 +0000 | |||
565 | +++ tests/deploy.test 2012-12-20 18:04:24 +0000 | |||
566 | @@ -35,7 +35,7 @@ | |||
567 | 35 | 35 | ||
568 | 36 | def setUp(self): | 36 | def setUp(self): |
569 | 37 | self.charm = 'juju-gui' | 37 | self.charm = 'juju-gui' |
571 | 38 | self.port = '443' | 38 | self.port = '80' # Set to 443 when TLS connections are re-enabled. |
572 | 39 | 39 | ||
573 | 40 | def tearDown(self): | 40 | def tearDown(self): |
574 | 41 | juju('destroy-service', self.charm) | 41 | juju('destroy-service', self.charm) |
575 | @@ -53,7 +53,8 @@ | |||
576 | 53 | 53 | ||
577 | 54 | def check_services(self, hostname, ws_port=8080): | 54 | def check_services(self, hostname, ws_port=8080): |
578 | 55 | """Check the services are listening on their tcp ports.""" | 55 | """Check the services are listening on their tcp ports.""" |
580 | 56 | url = 'https://{0}:{1}'.format(hostname, self.port) | 56 | # Use https below when TLS connections are re-enabled. |
581 | 57 | url = 'http://{0}:{1}'.format(hostname, self.port) | ||
582 | 57 | response = open_url(url) | 58 | response = open_url(url) |
583 | 58 | self.assertEqual(200, response.getcode()) | 59 | self.assertEqual(200, response.getcode()) |
584 | 59 | ws_url = 'http://{0}:{1}/ws'.format(hostname, ws_port) | 60 | ws_url = 'http://{0}:{1}/ws'.format(hostname, ws_port) |
585 | @@ -104,7 +105,6 @@ | |||
586 | 104 | 105 | ||
587 | 105 | def test_staging(self): | 106 | def test_staging(self): |
588 | 106 | # Ensure the Juju GUI and improv services are correctly set up. | 107 | # Ensure the Juju GUI and improv services are correctly set up. |
589 | 107 | self.services = ('juju-api-improv', 'juju-gui') | ||
590 | 108 | config = {self.charm: {'staging': 'True'}} | 108 | config = {self.charm: {'staging': 'True'}} |
591 | 109 | config_file = make_charm_config_file(config) | 109 | config_file = make_charm_config_file(config) |
592 | 110 | hostname = self.deploy(config_path=config_file.name) | 110 | hostname = self.deploy(config_path=config_file.name) |
593 | @@ -113,6 +113,16 @@ | |||
594 | 113 | self.stop_services, hostname, ['juju-api-improv', 'juju-gui']) | 113 | self.stop_services, hostname, ['juju-api-improv', 'juju-gui']) |
595 | 114 | self.check_services(hostname) | 114 | self.check_services(hostname) |
596 | 115 | 115 | ||
597 | 116 | def test_branch_source(self): | ||
598 | 117 | # Ensure the Juju GUI is correctly deployed from a Bazaar branch. | ||
599 | 118 | config = {self.charm: {'juju-gui-source': 'lp:juju-gui'}} | ||
600 | 119 | config_file = make_charm_config_file(config) | ||
601 | 120 | hostname = self.deploy(config_path=config_file.name) | ||
602 | 121 | # XXX 2012-11-29 frankban bug=872264: see *stop_services* above. | ||
603 | 122 | self.addCleanup( | ||
604 | 123 | self.stop_services, hostname, ['juju-api-agent', 'juju-gui']) | ||
605 | 124 | self.check_services(hostname) | ||
606 | 125 | |||
607 | 116 | 126 | ||
608 | 117 | class DeployToTest(DeployTestMixin, unittest.TestCase): | 127 | class DeployToTest(DeployTestMixin, unittest.TestCase): |
609 | 118 | 128 | ||
610 | 119 | 129 | ||
611 | === modified file 'tests/test_utils.py' | |||
612 | --- tests/test_utils.py 2012-12-18 13:23:42 +0000 | |||
613 | +++ tests/test_utils.py 2012-12-20 18:04:24 +0000 | |||
614 | @@ -2,14 +2,19 @@ | |||
615 | 2 | 2 | ||
616 | 3 | from contextlib import contextmanager | 3 | from contextlib import contextmanager |
617 | 4 | import os | 4 | import os |
618 | 5 | import shutil | ||
619 | 6 | from simplejson import dumps | ||
620 | 5 | import tempfile | 7 | import tempfile |
621 | 6 | import unittest | 8 | import unittest |
622 | 9 | |||
623 | 7 | import charmhelpers | 10 | import charmhelpers |
624 | 8 | from simplejson import dumps | ||
625 | 9 | |||
626 | 10 | from utils import ( | 11 | from utils import ( |
627 | 12 | _get_by_attr, | ||
628 | 11 | cmd_log, | 13 | cmd_log, |
629 | 14 | first_path_in_dir, | ||
630 | 15 | get_release_file_url, | ||
631 | 12 | get_zookeeper_address, | 16 | get_zookeeper_address, |
632 | 17 | parse_source, | ||
633 | 13 | render_to_file, | 18 | render_to_file, |
634 | 14 | start_agent, | 19 | start_agent, |
635 | 15 | start_gui, | 20 | start_gui, |
636 | @@ -20,6 +25,205 @@ | |||
637 | 20 | import utils | 25 | import utils |
638 | 21 | 26 | ||
639 | 22 | 27 | ||
640 | 28 | class AttrDict(dict): | ||
641 | 29 | """A dict with the ability to access keys as attributes.""" | ||
642 | 30 | |||
643 | 31 | def __getattr__(self, attr): | ||
644 | 32 | if attr in self: | ||
645 | 33 | return self[attr] | ||
646 | 34 | raise AttributeError | ||
647 | 35 | |||
648 | 36 | |||
649 | 37 | class AttrDictTest(unittest.TestCase): | ||
650 | 38 | |||
651 | 39 | def test_key_as_attribute(self): | ||
652 | 40 | # Ensure attributes can be used to retrieve dict values. | ||
653 | 41 | attr_dict = AttrDict(myattr='myvalue') | ||
654 | 42 | self.assertEqual('myvalue', attr_dict.myattr) | ||
655 | 43 | |||
656 | 44 | def test_attribute_not_found(self): | ||
657 | 45 | # An AttributeError is raised if the dict does not contain an attribute | ||
658 | 46 | # corresponding to an existent key. | ||
659 | 47 | with self.assertRaises(AttributeError): | ||
660 | 48 | AttrDict().myattr | ||
661 | 49 | |||
662 | 50 | |||
663 | 51 | class FirstPathInDirTest(unittest.TestCase): | ||
664 | 52 | |||
665 | 53 | def setUp(self): | ||
666 | 54 | self.directory = tempfile.mkdtemp() | ||
667 | 55 | self.addCleanup(shutil.rmtree, self.directory) | ||
668 | 56 | self.path = os.path.join(self.directory, 'file_or_dir') | ||
669 | 57 | |||
670 | 58 | def test_file_path(self): | ||
671 | 59 | # Ensure the full path of a file is correctly returned. | ||
672 | 60 | open(self.path, 'w').close() | ||
673 | 61 | self.assertEqual(self.path, first_path_in_dir(self.directory)) | ||
674 | 62 | |||
675 | 63 | def test_directory_path(self): | ||
676 | 64 | # Ensure the full path of a directory is correctly returned. | ||
677 | 65 | os.mkdir(self.path) | ||
678 | 66 | self.assertEqual(self.path, first_path_in_dir(self.directory)) | ||
679 | 67 | |||
680 | 68 | def test_empty_directory(self): | ||
681 | 69 | # An IndexError is raised if the directory is empty. | ||
682 | 70 | self.assertRaises(IndexError, first_path_in_dir, self.directory) | ||
683 | 71 | |||
684 | 72 | |||
685 | 73 | def make_collection(attr, values): | ||
686 | 74 | """Create a collection of objects having an attribute named *attr*. | ||
687 | 75 | |||
688 | 76 | The value of the *attr* attribute, for each instance, is taken from | ||
689 | 77 | the *values* sequence. | ||
690 | 78 | """ | ||
691 | 79 | return [AttrDict({attr: value}) for value in values] | ||
692 | 80 | |||
693 | 81 | |||
694 | 82 | class MakeCollectionTest(unittest.TestCase): | ||
695 | 83 | |||
696 | 84 | def test_factory(self): | ||
697 | 85 | # Ensure the factory returns the expected object instances. | ||
698 | 86 | instances = make_collection('myattr', range(5)) | ||
699 | 87 | self.assertEqual(5, len(instances)) | ||
700 | 88 | for num, instance in enumerate(instances): | ||
701 | 89 | self.assertEqual(num, instance.myattr) | ||
702 | 90 | |||
703 | 91 | |||
704 | 92 | class GetByAttrTest(unittest.TestCase): | ||
705 | 93 | |||
706 | 94 | attr = 'myattr' | ||
707 | 95 | collection = make_collection(attr, range(5)) | ||
708 | 96 | |||
709 | 97 | def test_item_found(self): | ||
710 | 98 | # Ensure an object instance is correctly returned if found in | ||
711 | 99 | # the collection. | ||
712 | 100 | item = _get_by_attr(self.collection, self.attr, 3) | ||
713 | 101 | self.assertEqual(3, item.myattr) | ||
714 | 102 | |||
715 | 103 | def test_value_not_found(self): | ||
716 | 104 | # None is returned if the collection does not contain the requested | ||
717 | 105 | # item. | ||
718 | 106 | item = _get_by_attr(self.collection, self.attr, '__does_not_exist__') | ||
719 | 107 | self.assertIsNone(item) | ||
720 | 108 | |||
721 | 109 | def test_attr_not_found(self): | ||
722 | 110 | # An AttributeError is raised if items in collection does not have the | ||
723 | 111 | # required attribute. | ||
724 | 112 | with self.assertRaises(AttributeError): | ||
725 | 113 | _get_by_attr(self.collection, 'another_attr', 0) | ||
726 | 114 | |||
727 | 115 | |||
728 | 116 | class FileStub(object): | ||
729 | 117 | """Simulate a Launchpad hosted file returned by launchpadlib.""" | ||
730 | 118 | |||
731 | 119 | def __init__(self, file_link): | ||
732 | 120 | self.file_link = file_link | ||
733 | 121 | |||
734 | 122 | def __str__(self): | ||
735 | 123 | return self.file_link | ||
736 | 124 | |||
737 | 125 | |||
738 | 126 | class GetReleaseFileUrlTest(unittest.TestCase): | ||
739 | 127 | |||
740 | 128 | project = AttrDict( | ||
741 | 129 | series=( | ||
742 | 130 | AttrDict( | ||
743 | 131 | name='stable', | ||
744 | 132 | releases=( | ||
745 | 133 | AttrDict( | ||
746 | 134 | version='0.1.1', | ||
747 | 135 | files=( | ||
748 | 136 | FileStub('http://example.com/0.1.1.dmg'), | ||
749 | 137 | FileStub('http://example.com/0.1.1.tgz'), | ||
750 | 138 | ), | ||
751 | 139 | ), | ||
752 | 140 | AttrDict( | ||
753 | 141 | version='0.1.0', | ||
754 | 142 | files=( | ||
755 | 143 | FileStub('http://example.com/0.1.0.dmg'), | ||
756 | 144 | FileStub('http://example.com/0.1.0.tgz'), | ||
757 | 145 | ), | ||
758 | 146 | ), | ||
759 | 147 | ), | ||
760 | 148 | ), | ||
761 | 149 | AttrDict( | ||
762 | 150 | name='trunk', | ||
763 | 151 | releases=( | ||
764 | 152 | AttrDict( | ||
765 | 153 | version='0.1.1+build.1', | ||
766 | 154 | files=( | ||
767 | 155 | FileStub('http://example.com/0.1.1+build.1.dmg'), | ||
768 | 156 | FileStub('http://example.com/0.1.1+build.1.tgz'), | ||
769 | 157 | ), | ||
770 | 158 | ), | ||
771 | 159 | AttrDict( | ||
772 | 160 | version='0.1.0+build.1', | ||
773 | 161 | files=( | ||
774 | 162 | FileStub('http://example.com/0.1.0+build.1.dmg'), | ||
775 | 163 | FileStub('http://example.com/0.1.0+build.1.tgz'), | ||
776 | 164 | ), | ||
777 | 165 | ), | ||
778 | 166 | ), | ||
779 | 167 | ), | ||
780 | 168 | ), | ||
781 | 169 | ) | ||
782 | 170 | |||
783 | 171 | def test_latest_stable_release(self): | ||
784 | 172 | # Ensure the correct URL is returned for the latest stable release. | ||
785 | 173 | url = get_release_file_url(self.project, 'stable', None) | ||
786 | 174 | self.assertEqual('http://example.com/0.1.1.tgz', url) | ||
787 | 175 | |||
788 | 176 | def test_latest_trunk_release(self): | ||
789 | 177 | # Ensure the correct URL is returned for the latest trunk release. | ||
790 | 178 | url = get_release_file_url(self.project, 'trunk', None) | ||
791 | 179 | self.assertEqual('http://example.com/0.1.1+build.1.tgz', url) | ||
792 | 180 | |||
793 | 181 | def test_specific_stable_release(self): | ||
794 | 182 | # Ensure the correct URL is returned for a specific version of the | ||
795 | 183 | # stable release. | ||
796 | 184 | url = get_release_file_url(self.project, 'stable', '0.1.0') | ||
797 | 185 | self.assertEqual('http://example.com/0.1.0.tgz', url) | ||
798 | 186 | |||
799 | 187 | def test_specific_trunk_release(self): | ||
800 | 188 | # Ensure the correct URL is returned for a specific version of the | ||
801 | 189 | # trunk release. | ||
802 | 190 | url = get_release_file_url(self.project, 'trunk', '0.1.0+build.1') | ||
803 | 191 | self.assertEqual('http://example.com/0.1.0+build.1.tgz', url) | ||
804 | 192 | |||
805 | 193 | def test_series_not_found(self): | ||
806 | 194 | # A ValueError is raised if the series cannot be found. | ||
807 | 195 | with self.assertRaises(ValueError) as cm: | ||
808 | 196 | get_release_file_url(self.project, 'unstable', None) | ||
809 | 197 | self.assertIn('series not found', str(cm.exception)) | ||
810 | 198 | |||
811 | 199 | def test_no_releases(self): | ||
812 | 200 | # A ValueError is raised if the series does not contain releases. | ||
813 | 201 | project = AttrDict(series=[AttrDict(name='stable', releases=[])]) | ||
814 | 202 | with self.assertRaises(ValueError) as cm: | ||
815 | 203 | get_release_file_url(project, 'stable', None) | ||
816 | 204 | self.assertIn('series does not contain releases', str(cm.exception)) | ||
817 | 205 | |||
818 | 206 | def test_release_not_found(self): | ||
819 | 207 | # A ValueError is raised if the release cannot be found. | ||
820 | 208 | with self.assertRaises(ValueError) as cm: | ||
821 | 209 | get_release_file_url(self.project, 'stable', '2.0') | ||
822 | 210 | self.assertIn('release not found', str(cm.exception)) | ||
823 | 211 | |||
824 | 212 | def test_file_not_found(self): | ||
825 | 213 | # A ValueError is raised if the hosted file cannot be found. | ||
826 | 214 | project = AttrDict( | ||
827 | 215 | series=[ | ||
828 | 216 | AttrDict( | ||
829 | 217 | name='stable', | ||
830 | 218 | releases=[AttrDict(version='0.1.0', files=[])], | ||
831 | 219 | ), | ||
832 | 220 | ], | ||
833 | 221 | ) | ||
834 | 222 | with self.assertRaises(ValueError) as cm: | ||
835 | 223 | get_release_file_url(project, 'stable', None) | ||
836 | 224 | self.assertIn('file not found', str(cm.exception)) | ||
837 | 225 | |||
838 | 226 | |||
839 | 23 | class GetZookeeperAddressTest(unittest.TestCase): | 227 | class GetZookeeperAddressTest(unittest.TestCase): |
840 | 24 | 228 | ||
841 | 25 | def setUp(self): | 229 | def setUp(self): |
842 | @@ -36,6 +240,35 @@ | |||
843 | 36 | self.assertEqual(self.zookeeper_address, address) | 240 | self.assertEqual(self.zookeeper_address, address) |
844 | 37 | 241 | ||
845 | 38 | 242 | ||
846 | 243 | class ParseSourceTest(unittest.TestCase): | ||
847 | 244 | |||
848 | 245 | def test_latest_stable_release(self): | ||
849 | 246 | # Ensure the latest stable release is correctly parsed. | ||
850 | 247 | expected = ('stable', None) | ||
851 | 248 | self.assertTupleEqual(expected, parse_source('stable')) | ||
852 | 249 | |||
853 | 250 | def test_latest_trunk_release(self): | ||
854 | 251 | # Ensure the latest trunk release is correctly parsed. | ||
855 | 252 | expected = ('trunk', None) | ||
856 | 253 | self.assertTupleEqual(expected, parse_source('trunk')) | ||
857 | 254 | |||
858 | 255 | def test_stable_release(self): | ||
859 | 256 | # Ensure a specific stable release is correctly parsed. | ||
860 | 257 | expected = ('stable', '0.1.0') | ||
861 | 258 | self.assertTupleEqual(expected, parse_source('0.1.0')) | ||
862 | 259 | |||
863 | 260 | def test_trunk_release(self): | ||
864 | 261 | # Ensure a specific trunk release is correctly parsed. | ||
865 | 262 | expected = ('trunk', '0.1.0+build.1') | ||
866 | 263 | self.assertTupleEqual(expected, parse_source('0.1.0+build.1')) | ||
867 | 264 | |||
868 | 265 | def test_bzr_branch(self): | ||
869 | 266 | # Ensure a Bazaar branch is correctly parsed. | ||
870 | 267 | sources = ('lp:example', 'http://bazaar.launchpad.net/example') | ||
871 | 268 | for source in sources: | ||
872 | 269 | self.assertTupleEqual(('branch', source), parse_source(source)) | ||
873 | 270 | |||
874 | 271 | |||
875 | 39 | class RenderToFileTest(unittest.TestCase): | 272 | class RenderToFileTest(unittest.TestCase): |
876 | 40 | 273 | ||
877 | 41 | def setUp(self): | 274 | def setUp(self): |
878 | @@ -155,22 +388,18 @@ | |||
879 | 155 | self.assertTrue('/usr/sbin/nginx' in conf) | 388 | self.assertTrue('/usr/sbin/nginx' in conf) |
880 | 156 | nginx_conf = nginx_file.read() | 389 | nginx_conf = nginx_file.read() |
881 | 157 | self.assertTrue('juju-gui/build-debug' in nginx_conf) | 390 | self.assertTrue('juju-gui/build-debug' in nginx_conf) |
885 | 158 | self.assertEqual(self.svc_ctl_call_count, 2) | 391 | self.assertEqual(self.svc_ctl_call_count, 1) |
886 | 159 | self.assertEqual(self.service_names, ['nginx', 'juju-gui']) | 392 | self.assertEqual(self.service_names, ['juju-gui']) |
887 | 160 | self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.START]) | 393 | self.assertEqual(self.actions, [charmhelpers.START]) |
888 | 161 | 394 | ||
889 | 162 | def test_stop_staging(self): | 395 | def test_stop_staging(self): |
893 | 163 | mock_config = {'staging': True} | 396 | stop(True) |
891 | 164 | charmhelpers.command = lambda *args: lambda: dumps(mock_config) | ||
892 | 165 | stop() | ||
894 | 166 | self.assertEqual(self.svc_ctl_call_count, 2) | 397 | self.assertEqual(self.svc_ctl_call_count, 2) |
895 | 167 | self.assertEqual(self.service_names, ['juju-gui', 'juju-api-improv']) | 398 | self.assertEqual(self.service_names, ['juju-gui', 'juju-api-improv']) |
896 | 168 | self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP]) | 399 | self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP]) |
897 | 169 | 400 | ||
898 | 170 | def test_stop_production(self): | 401 | def test_stop_production(self): |
902 | 171 | mock_config = {'staging': False} | 402 | stop(False) |
900 | 172 | charmhelpers.command = lambda *args: lambda: dumps(mock_config) | ||
901 | 173 | stop() | ||
903 | 174 | self.assertEqual(self.svc_ctl_call_count, 2) | 403 | self.assertEqual(self.svc_ctl_call_count, 2) |
904 | 175 | self.assertEqual(self.service_names, ['juju-gui', 'juju-api-agent']) | 404 | self.assertEqual(self.service_names, ['juju-gui', 'juju-api-agent']) |
905 | 176 | self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP]) | 405 | self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP]) |
Reviewers: mp+140745_ code.launchpad. net,
Message:
Please take a look.
Description:
Updated the charm to serve Juju GUI releases.
I had a pre-implementation call with Gary and
some really useful help from Brad.
Details:
Replaced the juju-gui-branch config option with a new juju-gui-source
one: here you can specify where to deploy the GUI from (stable, trunk, a
branch, a specific version of stable/trunk).
Added a bunch of functions in utils.py: they help parsing the
juju-gui-source option, retrieving the URL of a release, etc.
Added tests for the utility functions above.
Changed the fetch/build function in utils.py, so that now a release
tarball is always used to install the GUI. It can be downloaded from
Launchpad or created using "make distfile" from a juju-gui checkout.
Also reorganized the fetch/build code: separated each of the fetch/build
functions into two different functions (one for the GUI, one for the
API).
Updated the install and the config-changed hooks to conform to the
new fetch/setup functions (mentioned above).
Updated the config-changed hook to reflect changes to the options,
install hook and utils.py.
Added one functional test specific to deploying from a branch.
The other tests now default to the latest stable release.
Created and uploaded Juju GUI trunk/stable releases.
Updated the stop hook: now the function in utils.py is called with the
argument it expects.
Overall code lint and clean up.
All those changes result in a big diff (~820 lines), sorry about that.
https:/ /code.launchpad .net/~frankban/ charms/ precise/ juju-gui/ bug-1088618- serve-releases/ +merge/ 140745
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/6977043/
Affected files: changed
A [revision details]
M config.yaml
M hooks/config-
M hooks/install
M hooks/stop
M hooks/utils.py
M tests/deploy.test
M tests/test_utils.py