Merge lp:~frankban/charms/precise/juju-gui/bug-1088618-serve-releases into lp:~juju-gui/charms/precise/juju-gui/trunk

Proposed by Francesco Banconi
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
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+140745@code.launchpad.net

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.

https://codereview.appspot.com/6977043/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

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:
   A [revision details]
   M config.yaml
   M hooks/config-changed
   M hooks/install
   M hooks/stop
   M hooks/utils.py
   M tests/deploy.test
   M tests/test_utils.py

Revision history for this message
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://codereview.appspot.com/6977043/diff/1/hooks/utils.py
File hooks/utils.py (right):

https://codereview.appspot.com/6977043/diff/1/hooks/utils.py#newcode131
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://codereview.appspot.com/6977043/diff/1/tests/test_utils.py
File tests/test_utils.py (right):

https://codereview.appspot.com/6977043/diff/1/tests/test_utils.py#newcode85
tests/test_utils.py:85: # Ensure the factory return the expected object
instances.
typo: returns

https://codereview.appspot.com/6977043/

Revision history for this message
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=~/dev/repo/ ~/bin/jitsu test juju-gui --logdir
/tmp --timeout 40m --no-bootstrap
2012-12-19 19:30:10,417 jitsu.test:INFO Running unit test: deploy.test
test_api_agent (__main__.DeployTest) ... ok
test_branch_source (__main__.DeployTest) ... ERROR
test_customized_api_port (__main__.DeployTest) ... ERROR
test_staging (__main__.DeployTest) ... ERROR
test_api_agent (__main__.DeployToTest) ...

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

https://codereview.appspot.com/6977043/

Revision history for this message
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=~/dev/repo/ ~/bin/jitsu test juju-gui --logdir
/tmp
> --timeout 40m --no-bootstrap
> 2012-12-19 19:30:10,417 jitsu.test:INFO Running unit test: deploy.test
> test_api_agent (__main__.DeployTest) ... ok
> test_branch_source (__main__.DeployTest) ... ERROR
> test_customized_api_port (__main__.DeployTest) ... ERROR
> test_staging (__main__.DeployTest) ... ERROR
> test_api_agent (__main__.DeployToTest) ...

> 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_api_agent test hung and I used a maximum 40 minute
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__.DeployTest) ... ok
test_branch_source (__main__.DeployTest) ... ERROR
test_customized_api_port (__main__.DeployTest) ... ERROR
test_staging (__main__.DeployTest) ... ERROR
test_api_agent (__main__.DeployToTest) ... 2012-12-19 20:10:10,421
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

https://codereview.appspot.com/6977043/

Revision history for this message
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)

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (3.9 KiB)

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__.DeployTest) ... ok
> test_branch_source (__main__.DeployTest) ... ERROR
> test_customized_api_port (__main__.DeployTest) ... ERROR
> test_staging (__main__.DeployTest) ... ERROR
> test_api_agent (__main__.DeployToTest) ... 2012-12-19 20:10:10,421
> 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=/home/frankban/devel/juju/store/ jitsu test juju-gui
--timeout 60m --logdir /home/frankban/devel/juju/guicharm/log/
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/juju/units/*/charm.log
   destroy the environment

In our tests, unittest.TestCase.tearDown destroys the juju-gui service.
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/juju/units/, and that directory includes charm.log.

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...

Read more...

38. By Francesco Banconi

Fixed typo.

Revision history for this message
Francesco Banconi (frankban) wrote :
Revision history for this message
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://codereview.appspot.com/6977043/
>
> --
> https://code.launchpad.net/~frankban/charms/precise/juju-gui/bug-1088618-serve-releases/+merge/140745
> 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://launchpad.net/~yellow
> Post to : <email address hidden>
> Unsubscribe : https://launchpad.net/~yellow
> More help : https://help.launchpad.net/ListHelp

Revision history for this message
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://codereview.appspot.com/6977043/diff/8001/hooks/config-changed
File hooks/config-changed (right):

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed#newcode50
hooks/config-changed:50: # Restarting of the gui and api services is
handled below.
The two lines above could be replaced by:
# Fetch new sources if needed, and restart GUI and API services

https://codereview.appspot.com/6977043/diff/8001/hooks/utils.py
File hooks/utils.py (right):

https://codereview.appspot.com/6977043/diff/8001/hooks/utils.py#newcode310
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://codereview.appspot.com/6977043/diff/8001/tests/test_utils.py
File tests/test_utils.py (right):

https://codereview.appspot.com/6977043/diff/8001/tests/test_utils.py#newcode117
tests/test_utils.py:117: """Simulates a Launchpad hosted file returned
by launchpadlib."""
"Simulate..."

https://codereview.appspot.com/6977043/

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

I was just reviewing where we were and had two comments. No change to
my previous review/blessing. :-)

Gary

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed
File hooks/config-changed (right):

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed#newcode50
hooks/config-changed:50: # Restarting of the gui and api services is
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_source_changed and juju_api_branch_changed variables control
whether we restart the GUI and the API, respectively, at the end of the
function." Or something. Other ideas?

https://codereview.appspot.com/6977043/diff/8001/hooks/utils.py
File hooks/utils.py (right):

https://codereview.appspot.com/6977043/diff/8001/hooks/utils.py#newcode310
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.

https://codereview.appspot.com/6977043/

39. By Francesco Banconi

Changes per review.

Revision history for this message
Francesco Banconi (frankban) wrote :

Hi Nicola,

thanks for the review.

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed
File hooks/config-changed (right):

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed#newcode50
hooks/config-changed:50: # Restarting of the gui and api services is
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://codereview.appspot.com/6977043/diff/8001/hooks/utils.py
File hooks/utils.py (right):

https://codereview.appspot.com/6977043/diff/8001/hooks/utils.py#newcode310
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://codereview.appspot.com/6977043/diff/8001/tests/test_utils.py
File tests/test_utils.py (right):

https://codereview.appspot.com/6977043/diff/8001/tests/test_utils.py#newcode117
tests/test_utils.py:117: """Simulates a Launchpad hosted file returned
by launchpadlib."""
On 2012/12/20 13:09:14, teknico wrote:
> "Simulate..."

Done.

https://codereview.appspot.com/6977043/

Revision history for this message
Francesco Banconi (frankban) wrote :
Revision history for this message
Nicola Larosa (teknico) wrote :

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed
File hooks/config-changed (right):

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed#newcode50
hooks/config-changed:50: # Restarting of the gui and api services is
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_source_changed and juju_api_branch_changed variables
> 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.

https://codereview.appspot.com/6977043/

Revision history for this message
Francesco Banconi (frankban) wrote :

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed
File hooks/config-changed (right):

https://codereview.appspot.com/6977043/diff/8001/hooks/config-changed#newcode50
hooks/config-changed:50: # Restarting of the gui and api services is
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.

https://codereview.appspot.com/6977043/

40. By Francesco Banconi

Fixed comment.

41. By Francesco Banconi

Merged trunk and resolved conflicts.

42. By Francesco Banconi

Turn off TLS.

Revision history for this message
Francesco Banconi (frankban) wrote :
Revision history for this message
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://codereview.appspot.com/6977043

https://codereview.appspot.com/6977043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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 machine: 1
6 open-ports:
7 - 80/tcp
8+ <!--- Uncomment when TLS connections are re-enabled.
9 - 443/tcp
10+ -->
11 - 8080/tcp
12 public-address: ec2-204-236-250-8.compute-1.amazonaws.com
13
14
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 options:
20- juju-gui-branch:
21+ juju-gui-source:
22 description: |
23- The Juju GUI Bazaar branch containing the source code to be deployed.
24+ Where to install Juju GUI from. Possible values are:
25+ - 'stable' (default): the latest stable release will be deployed;
26+ - 'trunk': the latest trunk release will be deployed;
27+ - a stable version (e.g '0.1.0'): the specified stable version will be
28+ deployed;
29+ - a trunk version (e.g '0.1.0+build.1'): the specified trunk version
30+ will be deployed;
31+ - a Bazaar branch (e.g. 'lp:juju-gui'): a release will be created and
32+ deployed from the specified Bazaar branch.
33 type: string
34- default: lp:juju-gui
35+ default: stable
36 juju-api-branch:
37 description: |
38 The Juju API Bazaar branch (implementing the websocket server).
39
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 @@
44-server {
45- listen 80;
46- server_name _;
47- return 301 https://$host$request_uri;
48-}
49+# Uncomment to switch back to TLS connections.
50+# server {
51+# listen 80;
52+# server_name _;
53+# return 301 https://$host$request_uri;
54+# }
55
56 server {
57- listen 443 default_server ssl;
58+ # Uncomment to switch back to TLS connections.
59+ # listen 443 default_server ssl;
60+ listen 80; # Delete this line when TLS connections are re-enabled.
61 server_name _;
62 root %(server_root)s;
63 index index.html;
64- ssl_certificate /etc/ssl/private/juju-gui/server.pem;
65- ssl_certificate_key /etc/ssl/private/juju-gui/server.key;
66+ # Uncomment to switch back to TLS connections.
67+ # ssl_certificate /etc/ssl/private/juju-gui/server.pem;
68+ # ssl_certificate_key /etc/ssl/private/juju-gui/server.key;
69
70 # Serve static assets.
71 location ^~ /juju-ui/ {
72
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 # Copyright 2012 Canonical Ltd. This software is licensed under the
78 # GNU Affero General Public License version 3 (see the file LICENSE).
79
80-import os.path
81-from shutil import rmtree
82 from subprocess import CalledProcessError
83 import sys
84
85@@ -24,11 +22,13 @@
86
87 from utils import (
88 AGENT,
89- build,
90 config_json,
91- fetch,
92+ fetch_api,
93+ fetch_gui,
94 GUI,
95 IMPROV,
96+ setup_gui,
97+ setup_nginx,
98 start_agent,
99 start_gui,
100 start_improv,
101@@ -39,33 +39,33 @@
102 # Handle all configuration file changes.
103 log('Updating configuration.')
104
105+ added_or_changed = diff.added_or_changed
106+ juju_api_port = config.get('juju-api-port')
107 staging = config.get('staging')
108- staging_environment = config.get('staging-environment')
109- juju_api_port = config.get('juju-api-port')
110- added_or_changed = diff.added_or_changed
111-
112- # Fetch new branches?
113- gui_branch = None
114- api_branch = None
115- if 'juju-gui-branch' in added_or_changed:
116- if os.path.exists('juju-gui'):
117- rmtree('juju-gui')
118- gui_branch = config['juju-gui-branch']
119+
120+ # The juju_gui_source_changed and juju_api_branch_changed variables
121+ # control whether we restart the GUI and the API, respectively, at the
122+ # end of the function.
123+ juju_gui_source_changed = False
124+ juju_api_branch_changed = False
125+
126+ # Fetch new sources?
127+ if 'juju-gui-source' in added_or_changed:
128+ juju_gui_source_changed = True
129+ release_tarball = fetch_gui(
130+ config['juju-gui-source'], config['command-log-file'])
131+ setup_gui(release_tarball)
132+ setup_nginx(config['ssl-cert-path'])
133 if 'juju-api-branch' in added_or_changed:
134- if os.path.exists('juju'):
135- rmtree('juju')
136- api_branch = config['juju-api-branch']
137- if gui_branch or api_branch:
138- fetch(gui_branch, api_branch)
139- build(config['command-log-file'], config['ssl-cert-path'])
140- # Restarting of the gui and api services is handled below.
141+ juju_api_branch_changed = True
142+ fetch_api(config['juju-api-branch'])
143
144 # Handle changes to the improv server configuration.
145 if staging:
146 staging_properties = set(
147 ['staging', 'staging-environment', 'juju-api-port'])
148 staging_changed = added_or_changed & staging_properties
149- if staging_changed or (api_branch is not None):
150+ if staging_changed or juju_api_branch_changed:
151 if 'staging' in added_or_changed:
152 # 'staging' went from False to True, so the agent server is
153 # running and must be stopped.
154@@ -76,14 +76,13 @@
155 current_api = IMPROV
156 log('Stopping %s.' % current_api)
157 service_control(current_api, STOP)
158-
159 # Now the improv server can be cleanly started.
160- log('Starting or restarting improv')
161- start_improv(juju_api_port, staging_environment)
162+ log('Starting or restarting staging.')
163+ start_improv(juju_api_port, config.get('staging-environment'))
164 else:
165 agent_properties = set(['juju-api-port', 'staging'])
166 agent_changed = added_or_changed & agent_properties
167- if agent_changed or (api_branch is not None):
168+ if agent_changed or juju_api_branch_changed:
169 if 'staging' in added_or_changed:
170 # If 'staging' transitions to False we need to stop the backend
171 # and start the agent.
172@@ -93,15 +92,15 @@
173 # updated -- bounce it.
174 current_api = AGENT
175 service_control(current_api, STOP)
176+ log('Starting or restarting Juju API agent.')
177 start_agent(juju_api_port)
178
179 # Handle changes to the juju-gui configuration.
180 gui_properties = set(
181 ['juju-gui-console-enabled', 'juju-api-port', 'staging'])
182 gui_changed = added_or_changed & gui_properties
183- if gui_changed or (gui_branch is not None):
184+ if gui_changed or juju_gui_source_changed:
185 with su('root'):
186- service_control('nginx', STOP)
187 service_control(GUI, STOP)
188 console_enabled = config.get('juju-gui-console-enabled')
189 start_gui(juju_api_port, console_enabled, staging)
190
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 from subprocess import (
196 CalledProcessError,
197 check_call,
198- )
199+)
200
201 # python-shelltoolbox is installed as a dependency of python-charmhelpers.
202 check_call(['apt-get', 'install', '-y', 'python-charmhelpers'])
203@@ -24,16 +24,19 @@
204 )
205
206 from utils import (
207- build,
208 cmd_log,
209 config_json,
210- fetch,
211- )
212+ fetch_api,
213+ fetch_gui,
214+ setup_gui,
215+ setup_nginx,
216+)
217
218
219 DEB_DEPENDENCIES = (
220 'bzr', 'imagemagick', 'make', 'nginx', 'nodejs', 'npm', 'openssl',
221- 'zookeeper')
222+ 'python-launchpadlib', 'zookeeper',
223+)
224
225
226 def get_dependencies():
227@@ -45,8 +48,11 @@
228 def main():
229 config = get_config()
230 get_dependencies()
231- fetch(config['juju-gui-branch'], config['juju-api-branch'])
232- build(config['command-log-file'], config['ssl-cert-path'])
233+ release_tarball = fetch_gui(
234+ config['juju-gui-source'], config['command-log-file'])
235+ setup_gui(release_tarball)
236+ setup_nginx(config['ssl-cert-path'])
237+ fetch_api(config['juju-api-branch'])
238 config_json.set(config)
239
240
241
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 log('Exposing services.')
247 # Open the Juju GUI web server HTTP and HTTPS ports.
248 open_port(80)
249- open_port(443)
250+ # Uncomment to switch back to TLS connections.
251+ # open_port(443)
252 # Open the Juju websocket server port.
253 open_port(juju_api_port)
254
255
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 #-*- python -*-
261
262 from charmhelpers import (
263+ get_config,
264 log_entry,
265 log_exit,
266 )
267@@ -9,9 +10,14 @@
268 from utils import stop
269
270
271+def main():
272+ config = get_config()
273+ stop(config.get('staging'))
274+
275+
276 if __name__ == '__main__':
277 log_entry()
278 try:
279- stop()
280+ main()
281 finally:
282 log_exit()
283
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
289 __all__ = [
290 'AGENT',
291- 'build',
292+ 'bzr_checkout',
293 'cmd_log',
294+ 'CURRENT_DIR',
295+ 'fetch_api',
296+ 'fetch_gui',
297+ 'first_path_in_dir',
298+ 'get_release_file_url',
299 'get_zookeeper_address',
300 'GUI',
301 'IMPROV',
302+ 'JUJU_DIR',
303+ 'JUJU_GUI_DIR',
304+ 'parse_source',
305 'render_to_file',
306+ 'setup_gui',
307+ 'setup_nginx',
308 'start_agent',
309 'start_gui',
310 'start_improv',
311 'stop',
312- ]
313+]
314
315 import json
316 import os
317@@ -20,15 +30,15 @@
318 import shutil
319 import tempfile
320
321+from launchpadlib.launchpad import Launchpad
322 from shelltoolbox import (
323- cd,
324 command,
325 environ,
326 run,
327 search_file,
328 Serializer,
329 su,
330- )
331+)
332 from charmhelpers import (
333 get_config,
334 log,
335@@ -48,6 +58,50 @@
336
337 # Store the configuration from on invocation to the next.
338 config_json = Serializer('/tmp/config.json')
339+# Bazaar checkout command.
340+bzr_checkout = command('bzr', 'co', '--lightweight')
341+
342+
343+def first_path_in_dir(directory):
344+ """Return the full path of the first file/dir in *directory*."""
345+ return os.path.join(directory, os.listdir(directory)[0])
346+
347+
348+def _get_by_attr(collection, attr, value):
349+ """Return the first item in collection having attr == value.
350+
351+ Return None if the item is not found.
352+ """
353+ for item in collection:
354+ if getattr(item, attr) == value:
355+ return item
356+
357+
358+def get_release_file_url(project, series_name, release_version):
359+ """Return the URL of the release file hosted in Launchpad.
360+
361+ The returned URL points to a release file for the given project, series
362+ name and release version.
363+ The argument *project* is a project object as returned by launchpadlib.
364+ The arguments *series_name* and *release_version* are strings. If
365+ *release_version* is None, the URL of the latest release will be returned.
366+ """
367+ series = _get_by_attr(project.series, 'name', series_name)
368+ if series is None:
369+ raise ValueError('%r: series not found' % series_name)
370+ releases = list(series.releases)
371+ if not releases:
372+ raise ValueError('%r: series does not contain releases' % series_name)
373+ if release_version is None:
374+ release = releases[0]
375+ else:
376+ release = _get_by_attr(releases, 'version', release_version)
377+ if not release:
378+ raise ValueError('%r: release not found' % release_version)
379+ files = [i for i in release.files if str(i).endswith('.tgz')]
380+ if not files:
381+ raise ValueError('%r: file not found' % release_version)
382+ return files[0].file_link
383
384
385 def get_zookeeper_address(agent_file_path):
386@@ -62,6 +116,26 @@
387 return line.split('=')[1].strip('"')
388
389
390+def parse_source(source):
391+ """Parse the ``juju-gui-source`` option.
392+
393+ Return a tuple of two elements representing info on how to deploy Juju GUI.
394+ Examples:
395+ - ('stable', None): latest stable release;
396+ - ('stable', '0.1.0'): stable release v0.1.0;
397+ - ('trunk', None): latest trunk release;
398+ - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1;
399+ - ('branch', 'lp:juju-gui'): release is made from a branch.
400+ """
401+ if source in ('stable', 'trunk'):
402+ return source, None
403+ if source.startswith('lp:') or source.startswith('http://'):
404+ return 'branch', source
405+ if 'build' in source:
406+ return 'trunk', source
407+ return 'stable', source
408+
409+
410 def render_to_file(template, context, destination):
411 """Render the given *template* into *destination* using *context*.
412
413@@ -112,9 +186,7 @@
414 'port': juju_api_port,
415 'staging_env': staging_env,
416 }
417- render_to_file(
418- 'juju-api-improv.conf.template', context,
419- config_path)
420+ render_to_file('juju-api-improv.conf.template', context, config_path)
421 log('Starting the staging backend.')
422 with su('root'):
423 service_control(IMPROV, START)
424@@ -132,9 +204,7 @@
425 'port': juju_api_port,
426 'zookeeper': zookeeper,
427 }
428- render_to_file(
429- 'juju-api-agent.conf.template', context,
430- config_path)
431+ render_to_file('juju-api-agent.conf.template', context, config_path)
432 log('Starting API agent.')
433 with su('root'):
434 service_control(AGENT, START)
435@@ -150,8 +220,7 @@
436 build_dir = JUJU_GUI_DIR + '/build-'
437 build_dir += 'debug' if staging else 'prod'
438 log('Setting up Juju GUI start up script.')
439- render_to_file(
440- 'juju-gui.conf.template', {}, config_path)
441+ render_to_file('juju-gui.conf.template', {}, config_path)
442 log('Generating the Juju GUI configuration file.')
443 context = {
444 'address': unit_get('public-address'),
445@@ -161,29 +230,24 @@
446 if config_js_path is None:
447 config_js_path = os.path.join(
448 build_dir, 'juju-ui', 'assets', 'config.js')
449- render_to_file(
450- 'config.js.template', context,
451- config_js_path)
452+ render_to_file('config.js.template', context, config_js_path)
453 log('Generating the nginx site configuration file.')
454 context = {
455 'server_root': build_dir
456 }
457- render_to_file(
458- 'nginx.conf.template', context, nginx_path)
459+ render_to_file('nginx.conf.template', context, nginx_path)
460 log('Starting Juju GUI.')
461 with su('root'):
462- # Stop nginx so it will restart cleanly with the gui.
463- service_control('nginx', STOP)
464+ # Start the Juju GUI.
465 service_control(GUI, START)
466
467
468-def stop():
469+def stop(staging):
470 """Stop the Juju API agent."""
471- config = get_config()
472 with su('root'):
473 log('Stopping Juju GUI.')
474 service_control(GUI, STOP)
475- if config.get('staging'):
476+ if staging:
477 log('Stopping the staging backend.')
478 service_control(IMPROV, STOP)
479 else:
480@@ -191,27 +255,60 @@
481 service_control(AGENT, STOP)
482
483
484-def fetch(juju_gui_branch, juju_api_branch):
485- """Install required dependencies and retrieve Juju/Juju GUI branches."""
486- log('Retrieving source checkouts.')
487- bzr_checkout = command('bzr', 'co', '--lightweight')
488- if juju_gui_branch is not None:
489- cmd_log(run('rm', '-rf', 'juju-gui'))
490- cmd_log(bzr_checkout(juju_gui_branch, 'juju-gui'))
491- if juju_api_branch is not None:
492- cmd_log(run('rm', '-rf', 'juju'))
493- cmd_log(bzr_checkout(juju_api_branch, 'juju'))
494-
495-
496-def build(logpath, ssl_cert_path):
497- """Set up Juju GUI and nginx."""
498- log('Building Juju GUI.')
499- with environ(NO_BZR='1'):
500- with cd('juju-gui'):
501- logdir = os.path.dirname(logpath)
502- fd, name = tempfile.mkstemp(prefix='make-', dir=logdir)
503- log('Output from "make" sent to', name)
504- run('make', stdout=fd, stderr=fd)
505+def fetch_gui(juju_gui_source, logpath):
506+ """Retrieve the Juju GUI release/branch."""
507+ # Retrieve a Juju GUI release.
508+ origin, version_or_branch = parse_source(juju_gui_source)
509+ if origin == 'branch':
510+ # Create a release starting from a branch.
511+ juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source')
512+ log('Retrieving Juju GUI source checkouts.')
513+ cmd_log(run('rm', '-rf', juju_gui_source_dir))
514+ cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir))
515+ log('Preparing a Juju GUI release.')
516+ logdir = os.path.dirname(logpath)
517+ fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir)
518+ log('Output from "make distfile" sent to', name)
519+ with environ(NO_BZR='1'):
520+ run('make', '-C', juju_gui_source_dir, 'distfile',
521+ stdout=fd, stderr=fd)
522+ release_tarball = first_path_in_dir(
523+ os.path.join(juju_gui_source_dir, 'releases'))
524+ else:
525+ # Retrieve a release from Launchpad.
526+ log('Retrieving Juju GUI release.')
527+ launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production')
528+ project = launchpad.projects['juju-gui']
529+ file_url = get_release_file_url(project, origin, version_or_branch)
530+ log('Downloading release file from %s.' % file_url)
531+ release_tarball = os.path.join(CURRENT_DIR, 'release.tgz')
532+ cmd_log(run('curl', '-L', '-o', release_tarball, file_url))
533+ return release_tarball
534+
535+
536+def fetch_api(juju_api_branch):
537+ """Retrieve the Juju branch."""
538+ # Retrieve Juju API source checkout.
539+ log('Retrieving Juju API source checkout.')
540+ cmd_log(run('rm', '-rf', JUJU_DIR))
541+ cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR))
542+
543+
544+def setup_gui(release_tarball):
545+ """Set up Juju GUI."""
546+ # Uncompress the release tarball.
547+ log('Installing Juju GUI.')
548+ release_dir = os.path.join(CURRENT_DIR, 'release')
549+ cmd_log(run('rm', '-rf', release_dir))
550+ os.mkdir(release_dir)
551+ uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f')
552+ cmd_log(uncompress(release_tarball))
553+ # Link the Juju GUI dir to the contents of the release tarball.
554+ cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR))
555+
556+
557+def setup_nginx(ssl_cert_path):
558+ """Set up nginx."""
559 log('Setting up nginx.')
560 nginx_default_site = '/etc/nginx/sites-enabled/default'
561 juju_gui_site = '/etc/nginx/sites-available/juju-gui'
562
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
568 def setUp(self):
569 self.charm = 'juju-gui'
570- self.port = '443'
571+ self.port = '80' # Set to 443 when TLS connections are re-enabled.
572
573 def tearDown(self):
574 juju('destroy-service', self.charm)
575@@ -53,7 +53,8 @@
576
577 def check_services(self, hostname, ws_port=8080):
578 """Check the services are listening on their tcp ports."""
579- url = 'https://{0}:{1}'.format(hostname, self.port)
580+ # Use https below when TLS connections are re-enabled.
581+ url = 'http://{0}:{1}'.format(hostname, self.port)
582 response = open_url(url)
583 self.assertEqual(200, response.getcode())
584 ws_url = 'http://{0}:{1}/ws'.format(hostname, ws_port)
585@@ -104,7 +105,6 @@
586
587 def test_staging(self):
588 # Ensure the Juju GUI and improv services are correctly set up.
589- self.services = ('juju-api-improv', 'juju-gui')
590 config = {self.charm: {'staging': 'True'}}
591 config_file = make_charm_config_file(config)
592 hostname = self.deploy(config_path=config_file.name)
593@@ -113,6 +113,16 @@
594 self.stop_services, hostname, ['juju-api-improv', 'juju-gui'])
595 self.check_services(hostname)
596
597+ def test_branch_source(self):
598+ # Ensure the Juju GUI is correctly deployed from a Bazaar branch.
599+ config = {self.charm: {'juju-gui-source': 'lp:juju-gui'}}
600+ config_file = make_charm_config_file(config)
601+ hostname = self.deploy(config_path=config_file.name)
602+ # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.
603+ self.addCleanup(
604+ self.stop_services, hostname, ['juju-api-agent', 'juju-gui'])
605+ self.check_services(hostname)
606+
607
608 class DeployToTest(DeployTestMixin, unittest.TestCase):
609
610
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
616 from contextlib import contextmanager
617 import os
618+import shutil
619+from simplejson import dumps
620 import tempfile
621 import unittest
622+
623 import charmhelpers
624-from simplejson import dumps
625-
626 from utils import (
627+ _get_by_attr,
628 cmd_log,
629+ first_path_in_dir,
630+ get_release_file_url,
631 get_zookeeper_address,
632+ parse_source,
633 render_to_file,
634 start_agent,
635 start_gui,
636@@ -20,6 +25,205 @@
637 import utils
638
639
640+class AttrDict(dict):
641+ """A dict with the ability to access keys as attributes."""
642+
643+ def __getattr__(self, attr):
644+ if attr in self:
645+ return self[attr]
646+ raise AttributeError
647+
648+
649+class AttrDictTest(unittest.TestCase):
650+
651+ def test_key_as_attribute(self):
652+ # Ensure attributes can be used to retrieve dict values.
653+ attr_dict = AttrDict(myattr='myvalue')
654+ self.assertEqual('myvalue', attr_dict.myattr)
655+
656+ def test_attribute_not_found(self):
657+ # An AttributeError is raised if the dict does not contain an attribute
658+ # corresponding to an existent key.
659+ with self.assertRaises(AttributeError):
660+ AttrDict().myattr
661+
662+
663+class FirstPathInDirTest(unittest.TestCase):
664+
665+ def setUp(self):
666+ self.directory = tempfile.mkdtemp()
667+ self.addCleanup(shutil.rmtree, self.directory)
668+ self.path = os.path.join(self.directory, 'file_or_dir')
669+
670+ def test_file_path(self):
671+ # Ensure the full path of a file is correctly returned.
672+ open(self.path, 'w').close()
673+ self.assertEqual(self.path, first_path_in_dir(self.directory))
674+
675+ def test_directory_path(self):
676+ # Ensure the full path of a directory is correctly returned.
677+ os.mkdir(self.path)
678+ self.assertEqual(self.path, first_path_in_dir(self.directory))
679+
680+ def test_empty_directory(self):
681+ # An IndexError is raised if the directory is empty.
682+ self.assertRaises(IndexError, first_path_in_dir, self.directory)
683+
684+
685+def make_collection(attr, values):
686+ """Create a collection of objects having an attribute named *attr*.
687+
688+ The value of the *attr* attribute, for each instance, is taken from
689+ the *values* sequence.
690+ """
691+ return [AttrDict({attr: value}) for value in values]
692+
693+
694+class MakeCollectionTest(unittest.TestCase):
695+
696+ def test_factory(self):
697+ # Ensure the factory returns the expected object instances.
698+ instances = make_collection('myattr', range(5))
699+ self.assertEqual(5, len(instances))
700+ for num, instance in enumerate(instances):
701+ self.assertEqual(num, instance.myattr)
702+
703+
704+class GetByAttrTest(unittest.TestCase):
705+
706+ attr = 'myattr'
707+ collection = make_collection(attr, range(5))
708+
709+ def test_item_found(self):
710+ # Ensure an object instance is correctly returned if found in
711+ # the collection.
712+ item = _get_by_attr(self.collection, self.attr, 3)
713+ self.assertEqual(3, item.myattr)
714+
715+ def test_value_not_found(self):
716+ # None is returned if the collection does not contain the requested
717+ # item.
718+ item = _get_by_attr(self.collection, self.attr, '__does_not_exist__')
719+ self.assertIsNone(item)
720+
721+ def test_attr_not_found(self):
722+ # An AttributeError is raised if items in collection does not have the
723+ # required attribute.
724+ with self.assertRaises(AttributeError):
725+ _get_by_attr(self.collection, 'another_attr', 0)
726+
727+
728+class FileStub(object):
729+ """Simulate a Launchpad hosted file returned by launchpadlib."""
730+
731+ def __init__(self, file_link):
732+ self.file_link = file_link
733+
734+ def __str__(self):
735+ return self.file_link
736+
737+
738+class GetReleaseFileUrlTest(unittest.TestCase):
739+
740+ project = AttrDict(
741+ series=(
742+ AttrDict(
743+ name='stable',
744+ releases=(
745+ AttrDict(
746+ version='0.1.1',
747+ files=(
748+ FileStub('http://example.com/0.1.1.dmg'),
749+ FileStub('http://example.com/0.1.1.tgz'),
750+ ),
751+ ),
752+ AttrDict(
753+ version='0.1.0',
754+ files=(
755+ FileStub('http://example.com/0.1.0.dmg'),
756+ FileStub('http://example.com/0.1.0.tgz'),
757+ ),
758+ ),
759+ ),
760+ ),
761+ AttrDict(
762+ name='trunk',
763+ releases=(
764+ AttrDict(
765+ version='0.1.1+build.1',
766+ files=(
767+ FileStub('http://example.com/0.1.1+build.1.dmg'),
768+ FileStub('http://example.com/0.1.1+build.1.tgz'),
769+ ),
770+ ),
771+ AttrDict(
772+ version='0.1.0+build.1',
773+ files=(
774+ FileStub('http://example.com/0.1.0+build.1.dmg'),
775+ FileStub('http://example.com/0.1.0+build.1.tgz'),
776+ ),
777+ ),
778+ ),
779+ ),
780+ ),
781+ )
782+
783+ def test_latest_stable_release(self):
784+ # Ensure the correct URL is returned for the latest stable release.
785+ url = get_release_file_url(self.project, 'stable', None)
786+ self.assertEqual('http://example.com/0.1.1.tgz', url)
787+
788+ def test_latest_trunk_release(self):
789+ # Ensure the correct URL is returned for the latest trunk release.
790+ url = get_release_file_url(self.project, 'trunk', None)
791+ self.assertEqual('http://example.com/0.1.1+build.1.tgz', url)
792+
793+ def test_specific_stable_release(self):
794+ # Ensure the correct URL is returned for a specific version of the
795+ # stable release.
796+ url = get_release_file_url(self.project, 'stable', '0.1.0')
797+ self.assertEqual('http://example.com/0.1.0.tgz', url)
798+
799+ def test_specific_trunk_release(self):
800+ # Ensure the correct URL is returned for a specific version of the
801+ # trunk release.
802+ url = get_release_file_url(self.project, 'trunk', '0.1.0+build.1')
803+ self.assertEqual('http://example.com/0.1.0+build.1.tgz', url)
804+
805+ def test_series_not_found(self):
806+ # A ValueError is raised if the series cannot be found.
807+ with self.assertRaises(ValueError) as cm:
808+ get_release_file_url(self.project, 'unstable', None)
809+ self.assertIn('series not found', str(cm.exception))
810+
811+ def test_no_releases(self):
812+ # A ValueError is raised if the series does not contain releases.
813+ project = AttrDict(series=[AttrDict(name='stable', releases=[])])
814+ with self.assertRaises(ValueError) as cm:
815+ get_release_file_url(project, 'stable', None)
816+ self.assertIn('series does not contain releases', str(cm.exception))
817+
818+ def test_release_not_found(self):
819+ # A ValueError is raised if the release cannot be found.
820+ with self.assertRaises(ValueError) as cm:
821+ get_release_file_url(self.project, 'stable', '2.0')
822+ self.assertIn('release not found', str(cm.exception))
823+
824+ def test_file_not_found(self):
825+ # A ValueError is raised if the hosted file cannot be found.
826+ project = AttrDict(
827+ series=[
828+ AttrDict(
829+ name='stable',
830+ releases=[AttrDict(version='0.1.0', files=[])],
831+ ),
832+ ],
833+ )
834+ with self.assertRaises(ValueError) as cm:
835+ get_release_file_url(project, 'stable', None)
836+ self.assertIn('file not found', str(cm.exception))
837+
838+
839 class GetZookeeperAddressTest(unittest.TestCase):
840
841 def setUp(self):
842@@ -36,6 +240,35 @@
843 self.assertEqual(self.zookeeper_address, address)
844
845
846+class ParseSourceTest(unittest.TestCase):
847+
848+ def test_latest_stable_release(self):
849+ # Ensure the latest stable release is correctly parsed.
850+ expected = ('stable', None)
851+ self.assertTupleEqual(expected, parse_source('stable'))
852+
853+ def test_latest_trunk_release(self):
854+ # Ensure the latest trunk release is correctly parsed.
855+ expected = ('trunk', None)
856+ self.assertTupleEqual(expected, parse_source('trunk'))
857+
858+ def test_stable_release(self):
859+ # Ensure a specific stable release is correctly parsed.
860+ expected = ('stable', '0.1.0')
861+ self.assertTupleEqual(expected, parse_source('0.1.0'))
862+
863+ def test_trunk_release(self):
864+ # Ensure a specific trunk release is correctly parsed.
865+ expected = ('trunk', '0.1.0+build.1')
866+ self.assertTupleEqual(expected, parse_source('0.1.0+build.1'))
867+
868+ def test_bzr_branch(self):
869+ # Ensure a Bazaar branch is correctly parsed.
870+ sources = ('lp:example', 'http://bazaar.launchpad.net/example')
871+ for source in sources:
872+ self.assertTupleEqual(('branch', source), parse_source(source))
873+
874+
875 class RenderToFileTest(unittest.TestCase):
876
877 def setUp(self):
878@@ -155,22 +388,18 @@
879 self.assertTrue('/usr/sbin/nginx' in conf)
880 nginx_conf = nginx_file.read()
881 self.assertTrue('juju-gui/build-debug' in nginx_conf)
882- self.assertEqual(self.svc_ctl_call_count, 2)
883- self.assertEqual(self.service_names, ['nginx', 'juju-gui'])
884- self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.START])
885+ self.assertEqual(self.svc_ctl_call_count, 1)
886+ self.assertEqual(self.service_names, ['juju-gui'])
887+ self.assertEqual(self.actions, [charmhelpers.START])
888
889 def test_stop_staging(self):
890- mock_config = {'staging': True}
891- charmhelpers.command = lambda *args: lambda: dumps(mock_config)
892- stop()
893+ stop(True)
894 self.assertEqual(self.svc_ctl_call_count, 2)
895 self.assertEqual(self.service_names, ['juju-gui', 'juju-api-improv'])
896 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])
897
898 def test_stop_production(self):
899- mock_config = {'staging': False}
900- charmhelpers.command = lambda *args: lambda: dumps(mock_config)
901- stop()
902+ stop(False)
903 self.assertEqual(self.svc_ctl_call_count, 2)
904 self.assertEqual(self.service_names, ['juju-gui', 'juju-api-agent'])
905 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])

Subscribers

People subscribed via source and target branches