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
=== modified file 'README.md'
--- README.md 2012-12-19 11:49:41 +0000
+++ README.md 2012-12-20 18:04:24 +0000
@@ -58,7 +58,9 @@
58 machine: 158 machine: 1
59 open-ports:59 open-ports:
60 - 80/tcp60 - 80/tcp
61 <!--- Uncomment when TLS connections are re-enabled.
61 - 443/tcp62 - 443/tcp
63 -->
62 - 8080/tcp64 - 8080/tcp
63 public-address: ec2-204-236-250-8.compute-1.amazonaws.com65 public-address: ec2-204-236-250-8.compute-1.amazonaws.com
6466
6567
=== modified file 'config.yaml'
--- config.yaml 2012-12-18 17:46:09 +0000
+++ config.yaml 2012-12-20 18:04:24 +0000
@@ -1,9 +1,17 @@
1options:1options:
2 juju-gui-branch:2 juju-gui-source:
3 description: |3 description: |
4 The Juju GUI Bazaar branch containing the source code to be deployed.4 Where to install Juju GUI from. Possible values are:
5 - 'stable' (default): the latest stable release will be deployed;
6 - 'trunk': the latest trunk release will be deployed;
7 - a stable version (e.g '0.1.0'): the specified stable version will be
8 deployed;
9 - a trunk version (e.g '0.1.0+build.1'): the specified trunk version
10 will be deployed;
11 - a Bazaar branch (e.g. 'lp:juju-gui'): a release will be created and
12 deployed from the specified Bazaar branch.
5 type: string13 type: string
6 default: lp:juju-gui14 default: stable
7 juju-api-branch:15 juju-api-branch:
8 description: |16 description: |
9 The Juju API Bazaar branch (implementing the websocket server).17 The Juju API Bazaar branch (implementing the websocket server).
1018
=== modified file 'config/nginx.conf.template'
--- config/nginx.conf.template 2012-12-19 17:24:21 +0000
+++ config/nginx.conf.template 2012-12-20 18:04:24 +0000
@@ -1,16 +1,20 @@
1server {1# Uncomment to switch back to TLS connections.
2 listen 80;2# server {
3 server_name _;3# listen 80;
4 return 301 https://$host$request_uri;4# server_name _;
5}5# return 301 https://$host$request_uri;
6# }
67
7server {8server {
8 listen 443 default_server ssl;9 # Uncomment to switch back to TLS connections.
10 # listen 443 default_server ssl;
11 listen 80; # Delete this line when TLS connections are re-enabled.
9 server_name _;12 server_name _;
10 root %(server_root)s;13 root %(server_root)s;
11 index index.html;14 index index.html;
12 ssl_certificate /etc/ssl/private/juju-gui/server.pem;15 # Uncomment to switch back to TLS connections.
13 ssl_certificate_key /etc/ssl/private/juju-gui/server.key;16 # ssl_certificate /etc/ssl/private/juju-gui/server.pem;
17 # ssl_certificate_key /etc/ssl/private/juju-gui/server.key;
1418
15 # Serve static assets.19 # Serve static assets.
16 location ^~ /juju-ui/ {20 location ^~ /juju-ui/ {
1721
=== modified file 'hooks/config-changed'
--- hooks/config-changed 2012-12-19 17:24:21 +0000
+++ hooks/config-changed 2012-12-20 18:04:24 +0000
@@ -4,8 +4,6 @@
4# Copyright 2012 Canonical Ltd. This software is licensed under the4# Copyright 2012 Canonical Ltd. This software is licensed under the
5# GNU Affero General Public License version 3 (see the file LICENSE).5# GNU Affero General Public License version 3 (see the file LICENSE).
66
7import os.path
8from shutil import rmtree
9from subprocess import CalledProcessError7from subprocess import CalledProcessError
10import sys8import sys
119
@@ -24,11 +22,13 @@
2422
25from utils import (23from utils import (
26 AGENT,24 AGENT,
27 build,
28 config_json,25 config_json,
29 fetch,26 fetch_api,
27 fetch_gui,
30 GUI,28 GUI,
31 IMPROV,29 IMPROV,
30 setup_gui,
31 setup_nginx,
32 start_agent,32 start_agent,
33 start_gui,33 start_gui,
34 start_improv,34 start_improv,
@@ -39,33 +39,33 @@
39 # Handle all configuration file changes.39 # Handle all configuration file changes.
40 log('Updating configuration.')40 log('Updating configuration.')
4141
42 added_or_changed = diff.added_or_changed
43 juju_api_port = config.get('juju-api-port')
42 staging = config.get('staging')44 staging = config.get('staging')
43 staging_environment = config.get('staging-environment')45
44 juju_api_port = config.get('juju-api-port')46 # The juju_gui_source_changed and juju_api_branch_changed variables
45 added_or_changed = diff.added_or_changed47 # control whether we restart the GUI and the API, respectively, at the
4648 # end of the function.
47 # Fetch new branches?49 juju_gui_source_changed = False
48 gui_branch = None50 juju_api_branch_changed = False
49 api_branch = None51
50 if 'juju-gui-branch' in added_or_changed:52 # Fetch new sources?
51 if os.path.exists('juju-gui'):53 if 'juju-gui-source' in added_or_changed:
52 rmtree('juju-gui')54 juju_gui_source_changed = True
53 gui_branch = config['juju-gui-branch']55 release_tarball = fetch_gui(
56 config['juju-gui-source'], config['command-log-file'])
57 setup_gui(release_tarball)
58 setup_nginx(config['ssl-cert-path'])
54 if 'juju-api-branch' in added_or_changed:59 if 'juju-api-branch' in added_or_changed:
55 if os.path.exists('juju'):60 juju_api_branch_changed = True
56 rmtree('juju')61 fetch_api(config['juju-api-branch'])
57 api_branch = config['juju-api-branch']
58 if gui_branch or api_branch:
59 fetch(gui_branch, api_branch)
60 build(config['command-log-file'], config['ssl-cert-path'])
61 # Restarting of the gui and api services is handled below.
6262
63 # Handle changes to the improv server configuration.63 # Handle changes to the improv server configuration.
64 if staging:64 if staging:
65 staging_properties = set(65 staging_properties = set(
66 ['staging', 'staging-environment', 'juju-api-port'])66 ['staging', 'staging-environment', 'juju-api-port'])
67 staging_changed = added_or_changed & staging_properties67 staging_changed = added_or_changed & staging_properties
68 if staging_changed or (api_branch is not None):68 if staging_changed or juju_api_branch_changed:
69 if 'staging' in added_or_changed:69 if 'staging' in added_or_changed:
70 # 'staging' went from False to True, so the agent server is70 # 'staging' went from False to True, so the agent server is
71 # running and must be stopped.71 # running and must be stopped.
@@ -76,14 +76,13 @@
76 current_api = IMPROV76 current_api = IMPROV
77 log('Stopping %s.' % current_api)77 log('Stopping %s.' % current_api)
78 service_control(current_api, STOP)78 service_control(current_api, STOP)
79
80 # Now the improv server can be cleanly started.79 # Now the improv server can be cleanly started.
81 log('Starting or restarting improv')80 log('Starting or restarting staging.')
82 start_improv(juju_api_port, staging_environment)81 start_improv(juju_api_port, config.get('staging-environment'))
83 else:82 else:
84 agent_properties = set(['juju-api-port', 'staging'])83 agent_properties = set(['juju-api-port', 'staging'])
85 agent_changed = added_or_changed & agent_properties84 agent_changed = added_or_changed & agent_properties
86 if agent_changed or (api_branch is not None):85 if agent_changed or juju_api_branch_changed:
87 if 'staging' in added_or_changed:86 if 'staging' in added_or_changed:
88 # If 'staging' transitions to False we need to stop the backend87 # If 'staging' transitions to False we need to stop the backend
89 # and start the agent.88 # and start the agent.
@@ -93,15 +92,15 @@
93 # updated -- bounce it.92 # updated -- bounce it.
94 current_api = AGENT93 current_api = AGENT
95 service_control(current_api, STOP)94 service_control(current_api, STOP)
95 log('Starting or restarting Juju API agent.')
96 start_agent(juju_api_port)96 start_agent(juju_api_port)
9797
98 # Handle changes to the juju-gui configuration.98 # Handle changes to the juju-gui configuration.
99 gui_properties = set(99 gui_properties = set(
100 ['juju-gui-console-enabled', 'juju-api-port', 'staging'])100 ['juju-gui-console-enabled', 'juju-api-port', 'staging'])
101 gui_changed = added_or_changed & gui_properties101 gui_changed = added_or_changed & gui_properties
102 if gui_changed or (gui_branch is not None):102 if gui_changed or juju_gui_source_changed:
103 with su('root'):103 with su('root'):
104 service_control('nginx', STOP)
105 service_control(GUI, STOP)104 service_control(GUI, STOP)
106 console_enabled = config.get('juju-gui-console-enabled')105 console_enabled = config.get('juju-gui-console-enabled')
107 start_gui(juju_api_port, console_enabled, staging)106 start_gui(juju_api_port, console_enabled, staging)
108107
=== modified file 'hooks/install'
--- hooks/install 2012-12-18 17:46:09 +0000
+++ hooks/install 2012-12-20 18:04:24 +0000
@@ -4,7 +4,7 @@
4from subprocess import (4from subprocess import (
5 CalledProcessError,5 CalledProcessError,
6 check_call,6 check_call,
7 )7)
88
9# python-shelltoolbox is installed as a dependency of python-charmhelpers.9# python-shelltoolbox is installed as a dependency of python-charmhelpers.
10check_call(['apt-get', 'install', '-y', 'python-charmhelpers'])10check_call(['apt-get', 'install', '-y', 'python-charmhelpers'])
@@ -24,16 +24,19 @@
24)24)
2525
26from utils import (26from utils import (
27 build,
28 cmd_log,27 cmd_log,
29 config_json,28 config_json,
30 fetch,29 fetch_api,
31 )30 fetch_gui,
31 setup_gui,
32 setup_nginx,
33)
3234
3335
34DEB_DEPENDENCIES = (36DEB_DEPENDENCIES = (
35 'bzr', 'imagemagick', 'make', 'nginx', 'nodejs', 'npm', 'openssl',37 'bzr', 'imagemagick', 'make', 'nginx', 'nodejs', 'npm', 'openssl',
36 'zookeeper')38 'python-launchpadlib', 'zookeeper',
39)
3740
3841
39def get_dependencies():42def get_dependencies():
@@ -45,8 +48,11 @@
45def main():48def main():
46 config = get_config()49 config = get_config()
47 get_dependencies()50 get_dependencies()
48 fetch(config['juju-gui-branch'], config['juju-api-branch'])51 release_tarball = fetch_gui(
49 build(config['command-log-file'], config['ssl-cert-path'])52 config['juju-gui-source'], config['command-log-file'])
53 setup_gui(release_tarball)
54 setup_nginx(config['ssl-cert-path'])
55 fetch_api(config['juju-api-branch'])
50 config_json.set(config)56 config_json.set(config)
5157
5258
5359
=== modified file 'hooks/start'
--- hooks/start 2012-12-19 11:49:41 +0000
+++ hooks/start 2012-12-20 18:04:24 +0000
@@ -21,7 +21,8 @@
21 log('Exposing services.')21 log('Exposing services.')
22 # Open the Juju GUI web server HTTP and HTTPS ports.22 # Open the Juju GUI web server HTTP and HTTPS ports.
23 open_port(80)23 open_port(80)
24 open_port(443)24 # Uncomment to switch back to TLS connections.
25 # open_port(443)
25 # Open the Juju websocket server port.26 # Open the Juju websocket server port.
26 open_port(juju_api_port)27 open_port(juju_api_port)
2728
2829
=== modified file 'hooks/stop'
--- hooks/stop 2012-12-18 13:23:42 +0000
+++ hooks/stop 2012-12-20 18:04:24 +0000
@@ -2,6 +2,7 @@
2#-*- python -*-2#-*- python -*-
33
4from charmhelpers import (4from charmhelpers import (
5 get_config,
5 log_entry,6 log_entry,
6 log_exit,7 log_exit,
7)8)
@@ -9,9 +10,14 @@
9from utils import stop10from utils import stop
1011
1112
13def main():
14 config = get_config()
15 stop(config.get('staging'))
16
17
12if __name__ == '__main__':18if __name__ == '__main__':
13 log_entry()19 log_entry()
14 try:20 try:
15 stop()21 main()
16 finally:22 finally:
17 log_exit()23 log_exit()
1824
=== modified file 'hooks/utils.py'
--- hooks/utils.py 2012-12-19 11:49:41 +0000
+++ hooks/utils.py 2012-12-20 18:04:24 +0000
@@ -2,17 +2,27 @@
22
3__all__ = [3__all__ = [
4 'AGENT',4 'AGENT',
5 'build',5 'bzr_checkout',
6 'cmd_log',6 'cmd_log',
7 'CURRENT_DIR',
8 'fetch_api',
9 'fetch_gui',
10 'first_path_in_dir',
11 'get_release_file_url',
7 'get_zookeeper_address',12 'get_zookeeper_address',
8 'GUI',13 'GUI',
9 'IMPROV',14 'IMPROV',
15 'JUJU_DIR',
16 'JUJU_GUI_DIR',
17 'parse_source',
10 'render_to_file',18 'render_to_file',
19 'setup_gui',
20 'setup_nginx',
11 'start_agent',21 'start_agent',
12 'start_gui',22 'start_gui',
13 'start_improv',23 'start_improv',
14 'stop',24 'stop',
15 ]25]
1626
17import json27import json
18import os28import os
@@ -20,15 +30,15 @@
20import shutil30import shutil
21import tempfile31import tempfile
2232
33from launchpadlib.launchpad import Launchpad
23from shelltoolbox import (34from shelltoolbox import (
24 cd,
25 command,35 command,
26 environ,36 environ,
27 run,37 run,
28 search_file,38 search_file,
29 Serializer,39 Serializer,
30 su,40 su,
31 )41)
32from charmhelpers import (42from charmhelpers import (
33 get_config,43 get_config,
34 log,44 log,
@@ -48,6 +58,50 @@
4858
49# Store the configuration from on invocation to the next.59# Store the configuration from on invocation to the next.
50config_json = Serializer('/tmp/config.json')60config_json = Serializer('/tmp/config.json')
61# Bazaar checkout command.
62bzr_checkout = command('bzr', 'co', '--lightweight')
63
64
65def first_path_in_dir(directory):
66 """Return the full path of the first file/dir in *directory*."""
67 return os.path.join(directory, os.listdir(directory)[0])
68
69
70def _get_by_attr(collection, attr, value):
71 """Return the first item in collection having attr == value.
72
73 Return None if the item is not found.
74 """
75 for item in collection:
76 if getattr(item, attr) == value:
77 return item
78
79
80def get_release_file_url(project, series_name, release_version):
81 """Return the URL of the release file hosted in Launchpad.
82
83 The returned URL points to a release file for the given project, series
84 name and release version.
85 The argument *project* is a project object as returned by launchpadlib.
86 The arguments *series_name* and *release_version* are strings. If
87 *release_version* is None, the URL of the latest release will be returned.
88 """
89 series = _get_by_attr(project.series, 'name', series_name)
90 if series is None:
91 raise ValueError('%r: series not found' % series_name)
92 releases = list(series.releases)
93 if not releases:
94 raise ValueError('%r: series does not contain releases' % series_name)
95 if release_version is None:
96 release = releases[0]
97 else:
98 release = _get_by_attr(releases, 'version', release_version)
99 if not release:
100 raise ValueError('%r: release not found' % release_version)
101 files = [i for i in release.files if str(i).endswith('.tgz')]
102 if not files:
103 raise ValueError('%r: file not found' % release_version)
104 return files[0].file_link
51105
52106
53def get_zookeeper_address(agent_file_path):107def get_zookeeper_address(agent_file_path):
@@ -62,6 +116,26 @@
62 return line.split('=')[1].strip('"')116 return line.split('=')[1].strip('"')
63117
64118
119def parse_source(source):
120 """Parse the ``juju-gui-source`` option.
121
122 Return a tuple of two elements representing info on how to deploy Juju GUI.
123 Examples:
124 - ('stable', None): latest stable release;
125 - ('stable', '0.1.0'): stable release v0.1.0;
126 - ('trunk', None): latest trunk release;
127 - ('trunk', '0.1.0+build.1'): trunk release v0.1.0 bzr revision 1;
128 - ('branch', 'lp:juju-gui'): release is made from a branch.
129 """
130 if source in ('stable', 'trunk'):
131 return source, None
132 if source.startswith('lp:') or source.startswith('http://'):
133 return 'branch', source
134 if 'build' in source:
135 return 'trunk', source
136 return 'stable', source
137
138
65def render_to_file(template, context, destination):139def render_to_file(template, context, destination):
66 """Render the given *template* into *destination* using *context*.140 """Render the given *template* into *destination* using *context*.
67141
@@ -112,9 +186,7 @@
112 'port': juju_api_port,186 'port': juju_api_port,
113 'staging_env': staging_env,187 'staging_env': staging_env,
114 }188 }
115 render_to_file(189 render_to_file('juju-api-improv.conf.template', context, config_path)
116 'juju-api-improv.conf.template', context,
117 config_path)
118 log('Starting the staging backend.')190 log('Starting the staging backend.')
119 with su('root'):191 with su('root'):
120 service_control(IMPROV, START)192 service_control(IMPROV, START)
@@ -132,9 +204,7 @@
132 'port': juju_api_port,204 'port': juju_api_port,
133 'zookeeper': zookeeper,205 'zookeeper': zookeeper,
134 }206 }
135 render_to_file(207 render_to_file('juju-api-agent.conf.template', context, config_path)
136 'juju-api-agent.conf.template', context,
137 config_path)
138 log('Starting API agent.')208 log('Starting API agent.')
139 with su('root'):209 with su('root'):
140 service_control(AGENT, START)210 service_control(AGENT, START)
@@ -150,8 +220,7 @@
150 build_dir = JUJU_GUI_DIR + '/build-'220 build_dir = JUJU_GUI_DIR + '/build-'
151 build_dir += 'debug' if staging else 'prod'221 build_dir += 'debug' if staging else 'prod'
152 log('Setting up Juju GUI start up script.')222 log('Setting up Juju GUI start up script.')
153 render_to_file(223 render_to_file('juju-gui.conf.template', {}, config_path)
154 'juju-gui.conf.template', {}, config_path)
155 log('Generating the Juju GUI configuration file.')224 log('Generating the Juju GUI configuration file.')
156 context = {225 context = {
157 'address': unit_get('public-address'),226 'address': unit_get('public-address'),
@@ -161,29 +230,24 @@
161 if config_js_path is None:230 if config_js_path is None:
162 config_js_path = os.path.join(231 config_js_path = os.path.join(
163 build_dir, 'juju-ui', 'assets', 'config.js')232 build_dir, 'juju-ui', 'assets', 'config.js')
164 render_to_file(233 render_to_file('config.js.template', context, config_js_path)
165 'config.js.template', context,
166 config_js_path)
167 log('Generating the nginx site configuration file.')234 log('Generating the nginx site configuration file.')
168 context = {235 context = {
169 'server_root': build_dir236 'server_root': build_dir
170 }237 }
171 render_to_file(238 render_to_file('nginx.conf.template', context, nginx_path)
172 'nginx.conf.template', context, nginx_path)
173 log('Starting Juju GUI.')239 log('Starting Juju GUI.')
174 with su('root'):240 with su('root'):
175 # Stop nginx so it will restart cleanly with the gui.241 # Start the Juju GUI.
176 service_control('nginx', STOP)
177 service_control(GUI, START)242 service_control(GUI, START)
178243
179244
180def stop():245def stop(staging):
181 """Stop the Juju API agent."""246 """Stop the Juju API agent."""
182 config = get_config()
183 with su('root'):247 with su('root'):
184 log('Stopping Juju GUI.')248 log('Stopping Juju GUI.')
185 service_control(GUI, STOP)249 service_control(GUI, STOP)
186 if config.get('staging'):250 if staging:
187 log('Stopping the staging backend.')251 log('Stopping the staging backend.')
188 service_control(IMPROV, STOP)252 service_control(IMPROV, STOP)
189 else:253 else:
@@ -191,27 +255,60 @@
191 service_control(AGENT, STOP)255 service_control(AGENT, STOP)
192256
193257
194def fetch(juju_gui_branch, juju_api_branch):258def fetch_gui(juju_gui_source, logpath):
195 """Install required dependencies and retrieve Juju/Juju GUI branches."""259 """Retrieve the Juju GUI release/branch."""
196 log('Retrieving source checkouts.')260 # Retrieve a Juju GUI release.
197 bzr_checkout = command('bzr', 'co', '--lightweight')261 origin, version_or_branch = parse_source(juju_gui_source)
198 if juju_gui_branch is not None:262 if origin == 'branch':
199 cmd_log(run('rm', '-rf', 'juju-gui'))263 # Create a release starting from a branch.
200 cmd_log(bzr_checkout(juju_gui_branch, 'juju-gui'))264 juju_gui_source_dir = os.path.join(CURRENT_DIR, 'juju-gui-source')
201 if juju_api_branch is not None:265 log('Retrieving Juju GUI source checkouts.')
202 cmd_log(run('rm', '-rf', 'juju'))266 cmd_log(run('rm', '-rf', juju_gui_source_dir))
203 cmd_log(bzr_checkout(juju_api_branch, 'juju'))267 cmd_log(bzr_checkout(version_or_branch, juju_gui_source_dir))
204268 log('Preparing a Juju GUI release.')
205269 logdir = os.path.dirname(logpath)
206def build(logpath, ssl_cert_path):270 fd, name = tempfile.mkstemp(prefix='make-distfile-', dir=logdir)
207 """Set up Juju GUI and nginx."""271 log('Output from "make distfile" sent to', name)
208 log('Building Juju GUI.')272 with environ(NO_BZR='1'):
209 with environ(NO_BZR='1'):273 run('make', '-C', juju_gui_source_dir, 'distfile',
210 with cd('juju-gui'):274 stdout=fd, stderr=fd)
211 logdir = os.path.dirname(logpath)275 release_tarball = first_path_in_dir(
212 fd, name = tempfile.mkstemp(prefix='make-', dir=logdir)276 os.path.join(juju_gui_source_dir, 'releases'))
213 log('Output from "make" sent to', name)277 else:
214 run('make', stdout=fd, stderr=fd)278 # Retrieve a release from Launchpad.
279 log('Retrieving Juju GUI release.')
280 launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production')
281 project = launchpad.projects['juju-gui']
282 file_url = get_release_file_url(project, origin, version_or_branch)
283 log('Downloading release file from %s.' % file_url)
284 release_tarball = os.path.join(CURRENT_DIR, 'release.tgz')
285 cmd_log(run('curl', '-L', '-o', release_tarball, file_url))
286 return release_tarball
287
288
289def fetch_api(juju_api_branch):
290 """Retrieve the Juju branch."""
291 # Retrieve Juju API source checkout.
292 log('Retrieving Juju API source checkout.')
293 cmd_log(run('rm', '-rf', JUJU_DIR))
294 cmd_log(bzr_checkout(juju_api_branch, JUJU_DIR))
295
296
297def setup_gui(release_tarball):
298 """Set up Juju GUI."""
299 # Uncompress the release tarball.
300 log('Installing Juju GUI.')
301 release_dir = os.path.join(CURRENT_DIR, 'release')
302 cmd_log(run('rm', '-rf', release_dir))
303 os.mkdir(release_dir)
304 uncompress = command('tar', '-x', '-z', '-C', release_dir, '-f')
305 cmd_log(uncompress(release_tarball))
306 # Link the Juju GUI dir to the contents of the release tarball.
307 cmd_log(run('ln', '-sf', first_path_in_dir(release_dir), JUJU_GUI_DIR))
308
309
310def setup_nginx(ssl_cert_path):
311 """Set up nginx."""
215 log('Setting up nginx.')312 log('Setting up nginx.')
216 nginx_default_site = '/etc/nginx/sites-enabled/default'313 nginx_default_site = '/etc/nginx/sites-enabled/default'
217 juju_gui_site = '/etc/nginx/sites-available/juju-gui'314 juju_gui_site = '/etc/nginx/sites-available/juju-gui'
218315
=== modified file 'tests/deploy.test'
--- tests/deploy.test 2012-12-20 10:52:39 +0000
+++ tests/deploy.test 2012-12-20 18:04:24 +0000
@@ -35,7 +35,7 @@
3535
36 def setUp(self):36 def setUp(self):
37 self.charm = 'juju-gui'37 self.charm = 'juju-gui'
38 self.port = '443'38 self.port = '80' # Set to 443 when TLS connections are re-enabled.
3939
40 def tearDown(self):40 def tearDown(self):
41 juju('destroy-service', self.charm)41 juju('destroy-service', self.charm)
@@ -53,7 +53,8 @@
5353
54 def check_services(self, hostname, ws_port=8080):54 def check_services(self, hostname, ws_port=8080):
55 """Check the services are listening on their tcp ports."""55 """Check the services are listening on their tcp ports."""
56 url = 'https://{0}:{1}'.format(hostname, self.port)56 # Use https below when TLS connections are re-enabled.
57 url = 'http://{0}:{1}'.format(hostname, self.port)
57 response = open_url(url)58 response = open_url(url)
58 self.assertEqual(200, response.getcode())59 self.assertEqual(200, response.getcode())
59 ws_url = 'http://{0}:{1}/ws'.format(hostname, ws_port)60 ws_url = 'http://{0}:{1}/ws'.format(hostname, ws_port)
@@ -104,7 +105,6 @@
104105
105 def test_staging(self):106 def test_staging(self):
106 # Ensure the Juju GUI and improv services are correctly set up.107 # Ensure the Juju GUI and improv services are correctly set up.
107 self.services = ('juju-api-improv', 'juju-gui')
108 config = {self.charm: {'staging': 'True'}}108 config = {self.charm: {'staging': 'True'}}
109 config_file = make_charm_config_file(config)109 config_file = make_charm_config_file(config)
110 hostname = self.deploy(config_path=config_file.name)110 hostname = self.deploy(config_path=config_file.name)
@@ -113,6 +113,16 @@
113 self.stop_services, hostname, ['juju-api-improv', 'juju-gui'])113 self.stop_services, hostname, ['juju-api-improv', 'juju-gui'])
114 self.check_services(hostname)114 self.check_services(hostname)
115115
116 def test_branch_source(self):
117 # Ensure the Juju GUI is correctly deployed from a Bazaar branch.
118 config = {self.charm: {'juju-gui-source': 'lp:juju-gui'}}
119 config_file = make_charm_config_file(config)
120 hostname = self.deploy(config_path=config_file.name)
121 # XXX 2012-11-29 frankban bug=872264: see *stop_services* above.
122 self.addCleanup(
123 self.stop_services, hostname, ['juju-api-agent', 'juju-gui'])
124 self.check_services(hostname)
125
116126
117class DeployToTest(DeployTestMixin, unittest.TestCase):127class DeployToTest(DeployTestMixin, unittest.TestCase):
118128
119129
=== modified file 'tests/test_utils.py'
--- tests/test_utils.py 2012-12-18 13:23:42 +0000
+++ tests/test_utils.py 2012-12-20 18:04:24 +0000
@@ -2,14 +2,19 @@
22
3from contextlib import contextmanager3from contextlib import contextmanager
4import os4import os
5import shutil
6from simplejson import dumps
5import tempfile7import tempfile
6import unittest8import unittest
9
7import charmhelpers10import charmhelpers
8from simplejson import dumps
9
10from utils import (11from utils import (
12 _get_by_attr,
11 cmd_log,13 cmd_log,
14 first_path_in_dir,
15 get_release_file_url,
12 get_zookeeper_address,16 get_zookeeper_address,
17 parse_source,
13 render_to_file,18 render_to_file,
14 start_agent,19 start_agent,
15 start_gui,20 start_gui,
@@ -20,6 +25,205 @@
20import utils25import utils
2126
2227
28class AttrDict(dict):
29 """A dict with the ability to access keys as attributes."""
30
31 def __getattr__(self, attr):
32 if attr in self:
33 return self[attr]
34 raise AttributeError
35
36
37class AttrDictTest(unittest.TestCase):
38
39 def test_key_as_attribute(self):
40 # Ensure attributes can be used to retrieve dict values.
41 attr_dict = AttrDict(myattr='myvalue')
42 self.assertEqual('myvalue', attr_dict.myattr)
43
44 def test_attribute_not_found(self):
45 # An AttributeError is raised if the dict does not contain an attribute
46 # corresponding to an existent key.
47 with self.assertRaises(AttributeError):
48 AttrDict().myattr
49
50
51class FirstPathInDirTest(unittest.TestCase):
52
53 def setUp(self):
54 self.directory = tempfile.mkdtemp()
55 self.addCleanup(shutil.rmtree, self.directory)
56 self.path = os.path.join(self.directory, 'file_or_dir')
57
58 def test_file_path(self):
59 # Ensure the full path of a file is correctly returned.
60 open(self.path, 'w').close()
61 self.assertEqual(self.path, first_path_in_dir(self.directory))
62
63 def test_directory_path(self):
64 # Ensure the full path of a directory is correctly returned.
65 os.mkdir(self.path)
66 self.assertEqual(self.path, first_path_in_dir(self.directory))
67
68 def test_empty_directory(self):
69 # An IndexError is raised if the directory is empty.
70 self.assertRaises(IndexError, first_path_in_dir, self.directory)
71
72
73def make_collection(attr, values):
74 """Create a collection of objects having an attribute named *attr*.
75
76 The value of the *attr* attribute, for each instance, is taken from
77 the *values* sequence.
78 """
79 return [AttrDict({attr: value}) for value in values]
80
81
82class MakeCollectionTest(unittest.TestCase):
83
84 def test_factory(self):
85 # Ensure the factory returns the expected object instances.
86 instances = make_collection('myattr', range(5))
87 self.assertEqual(5, len(instances))
88 for num, instance in enumerate(instances):
89 self.assertEqual(num, instance.myattr)
90
91
92class GetByAttrTest(unittest.TestCase):
93
94 attr = 'myattr'
95 collection = make_collection(attr, range(5))
96
97 def test_item_found(self):
98 # Ensure an object instance is correctly returned if found in
99 # the collection.
100 item = _get_by_attr(self.collection, self.attr, 3)
101 self.assertEqual(3, item.myattr)
102
103 def test_value_not_found(self):
104 # None is returned if the collection does not contain the requested
105 # item.
106 item = _get_by_attr(self.collection, self.attr, '__does_not_exist__')
107 self.assertIsNone(item)
108
109 def test_attr_not_found(self):
110 # An AttributeError is raised if items in collection does not have the
111 # required attribute.
112 with self.assertRaises(AttributeError):
113 _get_by_attr(self.collection, 'another_attr', 0)
114
115
116class FileStub(object):
117 """Simulate a Launchpad hosted file returned by launchpadlib."""
118
119 def __init__(self, file_link):
120 self.file_link = file_link
121
122 def __str__(self):
123 return self.file_link
124
125
126class GetReleaseFileUrlTest(unittest.TestCase):
127
128 project = AttrDict(
129 series=(
130 AttrDict(
131 name='stable',
132 releases=(
133 AttrDict(
134 version='0.1.1',
135 files=(
136 FileStub('http://example.com/0.1.1.dmg'),
137 FileStub('http://example.com/0.1.1.tgz'),
138 ),
139 ),
140 AttrDict(
141 version='0.1.0',
142 files=(
143 FileStub('http://example.com/0.1.0.dmg'),
144 FileStub('http://example.com/0.1.0.tgz'),
145 ),
146 ),
147 ),
148 ),
149 AttrDict(
150 name='trunk',
151 releases=(
152 AttrDict(
153 version='0.1.1+build.1',
154 files=(
155 FileStub('http://example.com/0.1.1+build.1.dmg'),
156 FileStub('http://example.com/0.1.1+build.1.tgz'),
157 ),
158 ),
159 AttrDict(
160 version='0.1.0+build.1',
161 files=(
162 FileStub('http://example.com/0.1.0+build.1.dmg'),
163 FileStub('http://example.com/0.1.0+build.1.tgz'),
164 ),
165 ),
166 ),
167 ),
168 ),
169 )
170
171 def test_latest_stable_release(self):
172 # Ensure the correct URL is returned for the latest stable release.
173 url = get_release_file_url(self.project, 'stable', None)
174 self.assertEqual('http://example.com/0.1.1.tgz', url)
175
176 def test_latest_trunk_release(self):
177 # Ensure the correct URL is returned for the latest trunk release.
178 url = get_release_file_url(self.project, 'trunk', None)
179 self.assertEqual('http://example.com/0.1.1+build.1.tgz', url)
180
181 def test_specific_stable_release(self):
182 # Ensure the correct URL is returned for a specific version of the
183 # stable release.
184 url = get_release_file_url(self.project, 'stable', '0.1.0')
185 self.assertEqual('http://example.com/0.1.0.tgz', url)
186
187 def test_specific_trunk_release(self):
188 # Ensure the correct URL is returned for a specific version of the
189 # trunk release.
190 url = get_release_file_url(self.project, 'trunk', '0.1.0+build.1')
191 self.assertEqual('http://example.com/0.1.0+build.1.tgz', url)
192
193 def test_series_not_found(self):
194 # A ValueError is raised if the series cannot be found.
195 with self.assertRaises(ValueError) as cm:
196 get_release_file_url(self.project, 'unstable', None)
197 self.assertIn('series not found', str(cm.exception))
198
199 def test_no_releases(self):
200 # A ValueError is raised if the series does not contain releases.
201 project = AttrDict(series=[AttrDict(name='stable', releases=[])])
202 with self.assertRaises(ValueError) as cm:
203 get_release_file_url(project, 'stable', None)
204 self.assertIn('series does not contain releases', str(cm.exception))
205
206 def test_release_not_found(self):
207 # A ValueError is raised if the release cannot be found.
208 with self.assertRaises(ValueError) as cm:
209 get_release_file_url(self.project, 'stable', '2.0')
210 self.assertIn('release not found', str(cm.exception))
211
212 def test_file_not_found(self):
213 # A ValueError is raised if the hosted file cannot be found.
214 project = AttrDict(
215 series=[
216 AttrDict(
217 name='stable',
218 releases=[AttrDict(version='0.1.0', files=[])],
219 ),
220 ],
221 )
222 with self.assertRaises(ValueError) as cm:
223 get_release_file_url(project, 'stable', None)
224 self.assertIn('file not found', str(cm.exception))
225
226
23class GetZookeeperAddressTest(unittest.TestCase):227class GetZookeeperAddressTest(unittest.TestCase):
24228
25 def setUp(self):229 def setUp(self):
@@ -36,6 +240,35 @@
36 self.assertEqual(self.zookeeper_address, address)240 self.assertEqual(self.zookeeper_address, address)
37241
38242
243class ParseSourceTest(unittest.TestCase):
244
245 def test_latest_stable_release(self):
246 # Ensure the latest stable release is correctly parsed.
247 expected = ('stable', None)
248 self.assertTupleEqual(expected, parse_source('stable'))
249
250 def test_latest_trunk_release(self):
251 # Ensure the latest trunk release is correctly parsed.
252 expected = ('trunk', None)
253 self.assertTupleEqual(expected, parse_source('trunk'))
254
255 def test_stable_release(self):
256 # Ensure a specific stable release is correctly parsed.
257 expected = ('stable', '0.1.0')
258 self.assertTupleEqual(expected, parse_source('0.1.0'))
259
260 def test_trunk_release(self):
261 # Ensure a specific trunk release is correctly parsed.
262 expected = ('trunk', '0.1.0+build.1')
263 self.assertTupleEqual(expected, parse_source('0.1.0+build.1'))
264
265 def test_bzr_branch(self):
266 # Ensure a Bazaar branch is correctly parsed.
267 sources = ('lp:example', 'http://bazaar.launchpad.net/example')
268 for source in sources:
269 self.assertTupleEqual(('branch', source), parse_source(source))
270
271
39class RenderToFileTest(unittest.TestCase):272class RenderToFileTest(unittest.TestCase):
40273
41 def setUp(self):274 def setUp(self):
@@ -155,22 +388,18 @@
155 self.assertTrue('/usr/sbin/nginx' in conf)388 self.assertTrue('/usr/sbin/nginx' in conf)
156 nginx_conf = nginx_file.read()389 nginx_conf = nginx_file.read()
157 self.assertTrue('juju-gui/build-debug' in nginx_conf)390 self.assertTrue('juju-gui/build-debug' in nginx_conf)
158 self.assertEqual(self.svc_ctl_call_count, 2)391 self.assertEqual(self.svc_ctl_call_count, 1)
159 self.assertEqual(self.service_names, ['nginx', 'juju-gui'])392 self.assertEqual(self.service_names, ['juju-gui'])
160 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.START])393 self.assertEqual(self.actions, [charmhelpers.START])
161394
162 def test_stop_staging(self):395 def test_stop_staging(self):
163 mock_config = {'staging': True}396 stop(True)
164 charmhelpers.command = lambda *args: lambda: dumps(mock_config)
165 stop()
166 self.assertEqual(self.svc_ctl_call_count, 2)397 self.assertEqual(self.svc_ctl_call_count, 2)
167 self.assertEqual(self.service_names, ['juju-gui', 'juju-api-improv'])398 self.assertEqual(self.service_names, ['juju-gui', 'juju-api-improv'])
168 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])399 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])
169400
170 def test_stop_production(self):401 def test_stop_production(self):
171 mock_config = {'staging': False}402 stop(False)
172 charmhelpers.command = lambda *args: lambda: dumps(mock_config)
173 stop()
174 self.assertEqual(self.svc_ctl_call_count, 2)403 self.assertEqual(self.svc_ctl_call_count, 2)
175 self.assertEqual(self.service_names, ['juju-gui', 'juju-api-agent'])404 self.assertEqual(self.service_names, ['juju-gui', 'juju-api-agent'])
176 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])405 self.assertEqual(self.actions, [charmhelpers.STOP, charmhelpers.STOP])

Subscribers

People subscribed via source and target branches