Merge lp:~frankban/charms/precise/juju-gui/local-releases into lp:~juju-gui/charms/precise/juju-gui/trunk
- Precise Pangolin (12.04)
- local-releases
- Merge into trunk
Status: | Merged | ||||||||
---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 115 | ||||||||
Proposed branch: | lp:~frankban/charms/precise/juju-gui/local-releases | ||||||||
Merge into: | lp:~juju-gui/charms/precise/juju-gui/trunk | ||||||||
Diff against target: |
780 lines (+414/-66) 7 files modified
HACKING.md (+14/-0) README.md (+36/-5) config.yaml (+21/-13) hooks/utils.py (+91/-22) revision (+1/-1) tests/20-functional.test (+22/-11) tests/test_utils.py (+229/-14) |
||||||||
To merge this branch: | bzr merge lp:~frankban/charms/precise/juju-gui/local-releases | ||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+189896@code.launchpad.net |
Commit message
Description of the change
Introduce local GUI releases.
Implemented the new "local" juju-gui-source option.
By default the GUI release is now retrieved from
the charm itself, so that in the deployment process
the charm can avoid connecting to Launchpad.
This should result in the charm being able to be
deployed behind a firewall without the traditional
workarounds. Note that this is not yet demonstrated
and must be QAed in a real firewalled environment.
The process tries is like the following:
- if juju-gui-source is "local", the last tarball
in the releases dir is installed;
- if juju-gui-source is a stable or development version,
the charm checks if that version is present in the
local repository before downloading it from Launchpad.
- the rest of the options should still work as usual
(but some QA could help, e.g. deploying from a branch).
Also updated the charm documentation and added some
missing tests.
Tests: `make unittest` from the branch root
(I am currently running the functional tests).
QA (assuming 0.10.1 is the latest GUI release):
- `juju bootstrap --debug`;
- `make deploy`;
- check the logs: no PPAs are used, the local release
is installed;
- `juju set juju-gui juju-gui-
- check the logs: the release is downloaded from
Launchpad;
- `juju set juju-gui juju-gui-
- check the logs: the charm successfully find
the 0.10.1 release in the local repository and
avoids downloading it from Launchpad.
- keep trying to break the charm in all the ways
you can imagine...
Thank you.
Francesco Banconi (frankban) wrote : | # |
Madison Scott-Clary (makyo) wrote : | # |
LGTM, QA okay.
Gary Poster (gary) wrote : | # |
LGTM with many trivials and one slightly-
Relying on Makyo's QA. Great, thank you!
Gary
https:/
File config.yaml (right):
https:/
config.yaml:20: Where to install Juju GUI from. Possible values are:
Possible values are the following.
[changes below will largely be punctuation and capitalization, shifting
semicolons to periods and capitalizing the start of new sentences.]
https:/
config.yaml:21: - 'local' (default): the latest local release will be
deployed. Releases
Capitalize "The"
https:/
config.yaml:22: are stored in the releases directory of this charm;
...charm; -> ...charm.
https:/
config.yaml:23: - 'stable' the latest stable release will be deployed;
The latest release from the "stable" series will be deployed.
https:/
config.yaml:24: - 'trunk': the latest trunk release will be deployed;
The latest release from the "trunk" series will be deployed. Please
note that this is not a build of the current Juju GUI trunk. For that
functionality, use "lp:juju-gui", as described below.
https:/
config.yaml:25: - a stable version (e.g '0.1.0'): the specified stable
version will be
The...
https:/
config.yaml:28: release will be downloaded from Launchpad;
...Launchpad.
https:/
config.yaml:29: - a trunk version (e.g '0.1.0+build.1'): the specified
trunk version
The...
https:/
config.yaml:32: release will be downloaded from Launchpad;
...Launchpad.
https:/
config.yaml:33: - a Bazaar branch (e.g. 'lp:juju-gui'): a release will
be created and
A release...
https:/
config.yaml:36: revision, e.g. 'lp:juju-gui:42' will checkout revno 42;
...revno 42.
https:/
config.yaml:37: - a 'url:' prefixed url (ex: url:http://...) of a
specific location
- a 'url:' prefixed url: The release found at the given URL (ex:
url:http://... or url:file://...) will be deployed.
https:/
File hooks/utils.py (right):
https:/
hooks/utils.py:165: \.tgz # File extension.
would you mind changing this to
\.(tgz|xz)
per https:/
...No, never mind. We can do that in a separate branch, later. It
would be good to decrease the charm size, now that releases will be
included, but not something we have to tackle right now. Stop me! :-)
https:/
- 127. By Francesco Banconi
-
Changes as per review.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Introduce local GUI releases.
Implemented the new "local" juju-gui-source option.
By default the GUI release is now retrieved from
the charm itself, so that in the deployment process
the charm can avoid connecting to Launchpad.
This should result in the charm being able to be
deployed behind a firewall without the traditional
workarounds. Note that this is not yet demonstrated
and must be QAed in a real firewalled environment.
The process tries is like the following:
- if juju-gui-source is "local", the last tarball
in the releases dir is installed;
- if juju-gui-source is a stable or development version,
the charm checks if that version is present in the
local repository before downloading it from Launchpad.
- the rest of the options should still work as usual
(but some QA could help, e.g. deploying from a branch).
Also updated the charm documentation and added some
missing tests.
Tests: `make unittest` from the branch root
(I am currently running the functional tests).
QA (assuming 0.10.1 is the latest GUI release):
- `juju bootstrap --debug`;
- `make deploy`;
- check the logs: no PPAs are used, the local release
is installed;
- `juju set juju-gui juju-gui-
- check the logs: the release is downloaded from
Launchpad;
- `juju set juju-gui juju-gui-
- check the logs: the charm successfully find
the 0.10.1 release in the local repository and
avoids downloading it from Launchpad.
- keep trying to break the charm in all the ways
you can imagine...
Thank you.
R=matthew.scott, gary.poster
CC=
https:/
https:/
File config.yaml (right):
https:/
config.yaml:20: Where to install Juju GUI from. Possible values are:
On 2013/10/08 18:29:31, gary.poster wrote:
> Possible values are the following.
> [changes below will largely be punctuation and capitalization,
shifting
> semicolons to periods and capitalizing the start of new sentences.]
Done.
https:/
config.yaml:21: - 'local' (default): the latest local release will be
deployed. Releases
On 2013/10/08 18:29:31, gary.poster wrote:
> Capitalize "The"
Done.
https:/
config.yaml:22: are stored in the releases directory of this charm;
On 2013/10/08 18:29:31, gary.poster wrote:
> ...charm; -> ...charm.
Done.
https:/
config.yaml:23: - 'stable' the latest stable release will be deployed;
On 2013/10/08 18:29:31, gary.poster wrote:
> The latest release from the "stable" series will be deployed.
Done.
https:/
config.yaml:24: - 'trunk': the latest trunk release will be deployed;
On 2013/10/08 18:29:31, gary.poster wrote:
> The latest release from the "trunk" series will be deployed. Please
note that
> this is not a build of the current Juju GUI trunk. For that
functionality, use
> "lp:juju-gui", as described below.
Good clarification! Done.
h...
Francesco Banconi (frankban) wrote : | # |
Thank you both for the reviews!
Preview Diff
1 | === modified file 'HACKING.md' |
2 | --- HACKING.md 2013-08-28 14:47:13 +0000 |
3 | +++ HACKING.md 2013-10-09 08:56:52 +0000 |
4 | @@ -161,3 +161,17 @@ |
5 | |
6 | All of this is described in more detail on the Juju site: this is an |
7 | introduction to the process. |
8 | + |
9 | +## Upgrading the local releases repository ## |
10 | + |
11 | +The charm, in the default juju-gui-source configuration ("local"), deploys the |
12 | +GUI from the local releases repository. The repository is the `releases` |
13 | +directory located in the branch source, and contains GUI releases as tarball |
14 | +files. When a new GUI version is released, we want to also update the local |
15 | +repository. To do that, just copy the GUI tarball file to the `releases` |
16 | +directory, and commit a new revision of the charm. |
17 | +It is safe to remove the older files, and it is even recommended: too many |
18 | +releases can slow down the deployment process, especially when deploying from |
19 | +the local charm. |
20 | +Note that at least one release must be always present in the repository, |
21 | +otherwise the deployment process will fail. |
22 | |
23 | === modified file 'README.md' |
24 | --- README.md 2013-08-02 14:19:13 +0000 |
25 | +++ README.md 2013-10-09 08:56:52 +0000 |
26 | @@ -18,7 +18,7 @@ |
27 | |
28 | ## Demo and Staging Servers ## |
29 | |
30 | -The Juju GUI runs the Juju Charm Store on |
31 | +The Juju GUI runs the Juju Charm Store on |
32 | [jujucharms.com](http://jujucharms.com). From there, you can browse charms, |
33 | try the GUI, and build an example environment to export for use elsewhere. |
34 | |
35 | @@ -78,10 +78,29 @@ |
36 | |
37 | ### Deploying behind a firewall ### |
38 | |
39 | -While we may change this in the future to make firewall deploys simpler, this |
40 | -charm pulls the latest release from the Juju GUI page on Launchpad. If you |
41 | -are behind a firewall, however, this may not be available to you. In this case, |
42 | -you can configure the charm to pull the GUI release from a location you specify. |
43 | +When using the default options the charm uses the network connection only for |
44 | +installing Deb packages from the default Ubuntu repositories. For this reason |
45 | +the charm can be deployed behind a firewall in the usual way: |
46 | + |
47 | + juju deploy juju-gui |
48 | + |
49 | +There are situations and customizations in which the charm needs to connect to |
50 | +Launchpad: |
51 | + |
52 | +- juju-gui-source is set to "stable" or "trunk": in this cases the charm pulls |
53 | + the latest stable or development release from Launchpad; |
54 | +- juju-gui-source is set to a branch (e.g. "lp:juju-gui"): in this case the |
55 | + charm retrieves a checkout of the specified branch from Launchpad, and adds |
56 | + an external Launchpad PPA to install build dependencies; |
57 | +- juju-gui-source is set to a specific version number not available in the |
58 | + local store (i.e. in the releases directory of the deployed charm): in this |
59 | + case the release is downloaded from Launchpad; |
60 | +- builtin-server is set to false: in this case the charm adds an external |
61 | + Launchpad PPA to install the legacy server dependencies. |
62 | + |
63 | +If, for any reason, you need to use the legacy server, it is still possible to |
64 | +deploy behind a firewall configuring the charm to pull the GUI release from a |
65 | +location you specify. |
66 | |
67 | For both Juju Core and PyJuju, you must simply do the following steps. Note |
68 | that PyJuju must do these steps, plus another set described further below. |
69 | @@ -117,6 +136,18 @@ |
70 | |
71 | 3. Retry as described in the step 3 above (`juju resolved --retry juju-gui/0`). |
72 | |
73 | +### Upgrading the charm behind a firewall ### |
74 | + |
75 | +When a new version of Juju GUI is released, the charm is updated to include the |
76 | +new release in the local releases repository. Assuming the new version is |
77 | +1.0.1, after upgrading the charm, it is possible to also upgrade to the newer |
78 | +Juju GUI release by running the following: |
79 | + |
80 | + juju set juju-gui-source=1.0.1 |
81 | + |
82 | +In this case the new version will be found in the local repository and |
83 | +therefore the charm will not attempt to connect to Launchpad. |
84 | + |
85 | ### Deploying to a chosen machine ### |
86 | |
87 | The instructions above cause you to use a separate machine to work with the |
88 | |
89 | === modified file 'config.yaml' |
90 | --- config.yaml 2013-10-07 13:26:52 +0000 |
91 | +++ config.yaml 2013-10-09 08:56:52 +0000 |
92 | @@ -17,21 +17,29 @@ |
93 | options: |
94 | juju-gui-source: |
95 | description: | |
96 | - Where to install Juju GUI from. Possible values are: |
97 | - - 'stable' (default): the latest stable release will be deployed; |
98 | - - 'trunk': the latest trunk release will be deployed; |
99 | - - a stable version (e.g '0.1.0'): the specified stable version will be |
100 | - deployed; |
101 | - - a trunk version (e.g '0.1.0+build.1'): the specified trunk version |
102 | - will be deployed; |
103 | - - a Bazaar branch (e.g. 'lp:juju-gui'): a release will be created and |
104 | - deployed from the specified Bazaar branch. 'http://' prefixed branches |
105 | + Where to install Juju GUI from. Possible values the following. |
106 | + - 'local' (default): The latest local release will be deployed. Releases |
107 | + are stored in the releases directory of this charm. |
108 | + - 'stable': The latest release from the "stable" series will be deployed. |
109 | + - 'trunk': The latest release from the "trunk" series will be deployed. |
110 | + Please note that this is not a build of the current Juju GUI trunk. |
111 | + For that functionality, use "lp:juju-gui", as described below. |
112 | + - a stable version (e.g '0.1.0'): The specified stable version will be |
113 | + deployed. A suitable release is looked up in the local releases |
114 | + repository (see the "local" choice above). If not found locally, the |
115 | + release will be downloaded from Launchpad. |
116 | + - a trunk version (e.g '0.1.0+build.1'): The specified trunk version |
117 | + will be deployed. A suitable release is looked up in the local releases |
118 | + repository (see the "local" choice above). If not found locally, the |
119 | + release will be downloaded from Launchpad. |
120 | + - a Bazaar branch (e.g. 'lp:juju-gui'): A release will be created and |
121 | + deployed from the specified Bazaar branch. "http://"" prefixed branches |
122 | work as well. It is also possible to include the specific branch |
123 | - revision, e.g. 'lp:juju-gui:42' will checkout revno 42. |
124 | - - a 'url:' prefixed url (ex: url:http://...) of a specific location |
125 | - to pull a release from. |
126 | + revision, e.g. "lp:juju-gui:42" will checkout revno 42. |
127 | + - a "url:"" prefixed url: The release found at the given URL |
128 | + (ex: url:http://... or url:file://...) will be deployed. |
129 | type: string |
130 | - default: stable |
131 | + default: local |
132 | juju-gui-debug: |
133 | description: | |
134 | Run Juju GUI in debug mode, serving the uncompressed GUI source files. |
135 | |
136 | === modified file 'hooks/utils.py' |
137 | --- hooks/utils.py 2013-10-07 14:17:11 +0000 |
138 | +++ hooks/utils.py 2013-10-09 08:56:52 +0000 |
139 | @@ -27,14 +27,16 @@ |
140 | 'WEB_PORT', |
141 | 'cmd_log', |
142 | 'compute_build_dir', |
143 | + 'download_release', |
144 | 'fetch_api', |
145 | 'fetch_gui_from_branch', |
146 | 'fetch_gui_release', |
147 | 'find_missing_packages', |
148 | 'first_path_in_dir', |
149 | 'get_api_address', |
150 | + 'get_launchpad_release', |
151 | 'get_npm_cache_archive_url', |
152 | - 'get_release_file_url', |
153 | + 'get_release_file_path', |
154 | 'get_zookeeper_address', |
155 | 'install_missing_packages', |
156 | 'legacy_juju', |
157 | @@ -60,6 +62,7 @@ |
158 | ] |
159 | |
160 | from contextlib import contextmanager |
161 | +from distutils.version import LooseVersion |
162 | import errno |
163 | import json |
164 | import os |
165 | @@ -68,7 +71,7 @@ |
166 | import shutil |
167 | from subprocess import CalledProcessError |
168 | import tempfile |
169 | -from urlparse import urlparse |
170 | +import urlparse |
171 | |
172 | import apt |
173 | import tempita |
174 | @@ -110,6 +113,7 @@ |
175 | CONFIG_DIR = os.path.join(CURRENT_DIR, 'config') |
176 | JUJU_AGENT_DIR = os.path.join(BASE_DIR, 'juju') |
177 | JUJU_GUI_DIR = os.path.join(BASE_DIR, 'juju-gui') |
178 | +RELEASES_DIR = os.path.join(CURRENT_DIR, 'releases') |
179 | # Builtin server dependencies. The order of these requirements is important. |
180 | SERVER_DEPENDENCIES = ( |
181 | 'futures-2.1.4.tar.gz', |
182 | @@ -153,6 +157,14 @@ |
183 | (?::(\d+))? # Optional branch revision. |
184 | $ # End of line. |
185 | """, re.VERBOSE) |
186 | +release_expression = re.compile(r""" |
187 | + juju-gui- # Juju GUI prefix. |
188 | + ( |
189 | + \d+\.\d+\.\d+ # Major, minor, and patch version numbers. |
190 | + (?:\+build\.\d+)? # Optional bzr revno for development releases. |
191 | + ) |
192 | + \.tgz # File extension. |
193 | +""", re.VERBOSE) |
194 | |
195 | results_log = None |
196 | |
197 | @@ -200,14 +212,15 @@ |
198 | return item |
199 | |
200 | |
201 | -def get_release_file_url(project, series_name, release_version): |
202 | - """Return the URL of the release file hosted in Launchpad. |
203 | +def get_launchpad_release(project, series_name, release_version): |
204 | + """Return the URL and the name of the release file hosted in Launchpad. |
205 | |
206 | The returned URL points to a release file for the given project, series |
207 | name and release version. |
208 | The argument *project* is a project object as returned by launchpadlib. |
209 | The arguments *series_name* and *release_version* are strings. If |
210 | - *release_version* is None, the URL of the latest release will be returned. |
211 | + *release_version* is None, the URL and file name of the latest release will |
212 | + be returned. |
213 | """ |
214 | series = _get_by_attr(project.series, 'name', series_name) |
215 | if series is None: |
216 | @@ -223,8 +236,10 @@ |
217 | releases = [release] |
218 | for release in releases: |
219 | for file_ in release.files: |
220 | - if str(file_).endswith('.tgz'): |
221 | - return file_.file_link |
222 | + file_url = str(file_) |
223 | + if file_url.endswith('.tgz'): |
224 | + filename = os.path.split(urlparse.urlsplit(file_url).path)[1] |
225 | + return file_.file_link, filename |
226 | raise ValueError('%r: file not found' % release_version) |
227 | |
228 | |
229 | @@ -264,6 +279,7 @@ |
230 | |
231 | Return a tuple of two elements representing info on how to deploy Juju GUI. |
232 | Examples: |
233 | + - ('local', None): latest local release; |
234 | - ('stable', None): latest stable release; |
235 | - ('stable', '0.1.0'): stable release v0.1.0; |
236 | - ('trunk', None): latest trunk release; |
237 | @@ -276,12 +292,12 @@ |
238 | if source.startswith('url:'): |
239 | source = source[4:] |
240 | # Support file paths, including relative paths. |
241 | - if urlparse(source).scheme == '': |
242 | + if urlparse.urlparse(source).scheme == '': |
243 | if not source.startswith('/'): |
244 | source = os.path.join(os.path.abspath(CURRENT_DIR), source) |
245 | source = "file://%s" % source |
246 | return 'url', source |
247 | - if source in ('stable', 'trunk'): |
248 | + if source in ('local', 'stable', 'trunk'): |
249 | return source, None |
250 | match = bzr_url_expression.match(source) |
251 | if match is not None: |
252 | @@ -596,7 +612,7 @@ |
253 | launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') |
254 | project = launchpad.projects['juju-gui'] |
255 | # Find the URL of the most recently created NPM cache archive. |
256 | - npm_cache_url = get_release_file_url(project, 'npm-cache', None) |
257 | + npm_cache_url, _ = get_launchpad_release(project, 'npm-cache', None) |
258 | return npm_cache_url |
259 | |
260 | |
261 | @@ -641,21 +657,74 @@ |
262 | os.path.join(juju_gui_source_dir, 'releases')) |
263 | |
264 | |
265 | +def download_release(url, filename): |
266 | + """Download a Juju GUI release from the given URL. |
267 | + |
268 | + Save the resulting file as filename in the local releases repository. |
269 | + Return the full path of the saved file. |
270 | + """ |
271 | + destination = os.path.join(RELEASES_DIR, filename) |
272 | + log('Downloading release file: {} --> {}.'.format(url, destination)) |
273 | + cmd_log(run('curl', '-L', '-o', destination, url)) |
274 | + return destination |
275 | + |
276 | + |
277 | +def get_release_file_path(version=None): |
278 | + """Return the local path of the release file with the given version. |
279 | + |
280 | + If version is None, return the path of the last release. |
281 | + Raise a ValueError if no releases are found in the local repository. |
282 | + """ |
283 | + version_path_map = {} |
284 | + # Collect the locally stored releases. |
285 | + for filename in os.listdir(RELEASES_DIR): |
286 | + match = release_expression.match(filename) |
287 | + if match is not None: |
288 | + release_version = match.groups()[0] |
289 | + release_path = os.path.join(RELEASES_DIR, filename) |
290 | + version_path_map[release_version] = release_path |
291 | + # We expect the charm to include at least one release file. |
292 | + if not version_path_map: |
293 | + raise ValueError('Error: no releases found in the charm.') |
294 | + if version is None: |
295 | + # Return the path of the last release. |
296 | + last_version = sorted(version_path_map.keys(), key=LooseVersion)[-1] |
297 | + return version_path_map[last_version] |
298 | + # Return the path of the release with the requested version, or None if |
299 | + # the release is not found. |
300 | + return version_path_map.get(version) |
301 | + |
302 | + |
303 | def fetch_gui_release(origin, version): |
304 | - """Retrieve a Juju GUI release.""" |
305 | + """Retrieve a Juju GUI release. Return the release tarball local path. |
306 | + |
307 | + The release file can be retrieved from: |
308 | + - an arbitrary URL (if origin is "url"); |
309 | + - the local releases repository (if origin is "local" or if a release |
310 | + version is specified and the corresponding file is present locally); |
311 | + - Launchpad (in all the other cases). |
312 | + """ |
313 | log('Retrieving Juju GUI release.') |
314 | if origin == 'url': |
315 | - file_url = version |
316 | - else: |
317 | - # Retrieve a release from Launchpad. |
318 | - launchpad = Launchpad.login_anonymously( |
319 | - 'Juju GUI charm', 'production') |
320 | - project = launchpad.projects['juju-gui'] |
321 | - file_url = get_release_file_url(project, origin, version) |
322 | - log('Downloading release file from %s.' % file_url) |
323 | - release_tarball = os.path.join(CURRENT_DIR, 'release.tgz') |
324 | - cmd_log(run('curl', '-L', '-o', release_tarball, file_url)) |
325 | - return release_tarball |
326 | + return download_release(version, 'url-release.tgz') |
327 | + if origin == 'local': |
328 | + path = get_release_file_path() |
329 | + log('Using a local release: {}'.format(path)) |
330 | + return path |
331 | + # Handle "stable" and "trunk" origins. |
332 | + if version is not None: |
333 | + # If the user specified a version, before attempting to download the |
334 | + # requested release from Launchpad, check if that version is already |
335 | + # stored locally. |
336 | + path = get_release_file_path(version) |
337 | + if path is not None: |
338 | + log('Using a local release: {}'.format(path)) |
339 | + return path |
340 | + # Retrieve a release from Launchpad. |
341 | + launchpad = Launchpad.login_anonymously('Juju GUI charm', 'production') |
342 | + project = launchpad.projects['juju-gui'] |
343 | + url, filename = get_launchpad_release(project, origin, version) |
344 | + return download_release(url, filename) |
345 | |
346 | |
347 | def fetch_api(juju_api_branch): |
348 | |
349 | === added directory 'releases' |
350 | === added file 'releases/juju-gui-0.10.1.tgz' |
351 | Binary files releases/juju-gui-0.10.1.tgz 1970-01-01 00:00:00 +0000 and releases/juju-gui-0.10.1.tgz 2013-10-09 08:56:52 +0000 differ |
352 | === modified file 'revision' |
353 | --- revision 2013-10-07 14:18:05 +0000 |
354 | +++ revision 2013-10-09 08:56:52 +0000 |
355 | @@ -1,1 +1,1 @@ |
356 | -89 |
357 | +90 |
358 | |
359 | === modified file 'tests/20-functional.test' |
360 | --- tests/20-functional.test 2013-10-07 16:17:20 +0000 |
361 | +++ tests/20-functional.test 2013-10-09 08:56:52 +0000 |
362 | @@ -168,14 +168,25 @@ |
363 | def tearDown(self): |
364 | juju_destroy_service(self.charm) |
365 | |
366 | - def test_api_agent(self): |
367 | - # Ensure the Juju GUI and API agent services are correctly set up. |
368 | + def test_local_release(self): |
369 | + # Ensure the Juju GUI and API agent services are correctly set up when |
370 | + # deploying the local release. |
371 | unit_info = self.juju_deploy(self.charm) |
372 | hostname = unit_info['public-address'] |
373 | self.navigate_to(hostname) |
374 | self.handle_browser_warning() |
375 | self.assertEnvironmentIsConnected() |
376 | |
377 | + def test_stable_release(self): |
378 | + # Ensure the Juju GUI and API agent services are correctly set up when |
379 | + # deploying the stable release. |
380 | + options = {'juju-gui-source': 'stable'} |
381 | + unit_info = self.juju_deploy(self.charm, options=options) |
382 | + hostname = unit_info['public-address'] |
383 | + self.navigate_to(hostname) |
384 | + self.handle_browser_warning() |
385 | + self.assertEnvironmentIsConnected() |
386 | + |
387 | @unittest.skipUnless(is_legacy_juju, 'staging only works in pyJuju') |
388 | def test_staging(self): |
389 | # Ensure the Juju GUI and improv services are correctly set up. |
390 | @@ -189,8 +200,7 @@ |
391 | |
392 | def test_sandbox(self): |
393 | # The GUI is correctly deployed and set up in sandbox mode. |
394 | - unit_info = self.juju_deploy( |
395 | - self.charm, options={'builtin-server': 'true', 'sandbox': 'true'}) |
396 | + unit_info = self.juju_deploy(self.charm, options={'sandbox': 'true'}) |
397 | hostname = unit_info['public-address'] |
398 | self.navigate_to(hostname) |
399 | self.handle_browser_warning() |
400 | @@ -198,15 +208,16 @@ |
401 | |
402 | def test_branch_source(self): |
403 | # Ensure the Juju GUI is correctly deployed from a Bazaar branch. |
404 | - unit_info = self.juju_deploy( |
405 | - self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH}) |
406 | + options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH} |
407 | + unit_info = self.juju_deploy(self.charm, options=options) |
408 | hostname = unit_info['public-address'] |
409 | self.navigate_to(hostname) |
410 | self.handle_browser_warning() |
411 | self.assertEnvironmentIsConnected() |
412 | |
413 | - def test_cache_headers(self): |
414 | - # Make sure the correct cache headers are sent. |
415 | + def test_legacy_server(self): |
416 | + # The legacy apache + haproxy server configuration works correctly. |
417 | + # Also make sure the correct cache headers are sent. |
418 | options = { |
419 | 'builtin-server': False, |
420 | 'juju-gui-source': JUJU_GUI_TEST_BRANCH, |
421 | @@ -236,8 +247,8 @@ |
422 | |
423 | def test_nrpe_check_available(self): |
424 | # Make sure the check-app-access.sh script's ADDRESS is available. |
425 | - unit_info = self.juju_deploy( |
426 | - self.charm, options={'juju-gui-source': JUJU_GUI_TEST_BRANCH}) |
427 | + options = {'juju-gui-source': JUJU_GUI_TEST_BRANCH} |
428 | + unit_info = self.juju_deploy(self.charm, options=options) |
429 | hostname = unit_info['public-address'] |
430 | conn = httplib.HTTPSConnection(hostname) |
431 | # This request matches the ADDRESS var in the script. |
432 | @@ -252,7 +263,7 @@ |
433 | def setUpClass(cls): |
434 | # Deploy the charm. The resulting service is used by all the tests |
435 | # in this test case. |
436 | - unit_info = juju_deploy(cls.charm, options={'builtin-server': 'true'}) |
437 | + unit_info = juju_deploy(cls.charm) |
438 | cls.hostname = unit_info['public-address'] |
439 | # The counter is used to produce API request identifiers. |
440 | cls.counter = itertools.count() |
441 | |
442 | === modified file 'tests/test_utils.py' |
443 | --- tests/test_utils.py 2013-10-07 14:17:11 +0000 |
444 | +++ tests/test_utils.py 2013-10-09 08:56:52 +0000 |
445 | @@ -39,16 +39,19 @@ |
446 | _get_by_attr, |
447 | cmd_log, |
448 | compute_build_dir, |
449 | + download_release, |
450 | + fetch_gui_release, |
451 | first_path_in_dir, |
452 | get_api_address, |
453 | - get_release_file_url, |
454 | + get_launchpad_release, |
455 | + get_npm_cache_archive_url, |
456 | + get_release_file_path, |
457 | get_zookeeper_address, |
458 | + install_builtin_server, |
459 | + install_missing_packages, |
460 | legacy_juju, |
461 | log_hook, |
462 | parse_source, |
463 | - get_npm_cache_archive_url, |
464 | - install_builtin_server, |
465 | - install_missing_packages, |
466 | remove_apache_setup, |
467 | remove_haproxy_setup, |
468 | render_to_file, |
469 | @@ -94,6 +97,108 @@ |
470 | AttrDict().myattr |
471 | |
472 | |
473 | +@mock.patch('utils.run') |
474 | +@mock.patch('utils.log') |
475 | +@mock.patch('utils.cmd_log', mock.Mock()) |
476 | +class TestDownloadRelease(unittest.TestCase): |
477 | + |
478 | + def test_download(self, mock_log, mock_run): |
479 | + # A release is properly downloaded using curl. |
480 | + url = 'http://download.example.com/release.tgz' |
481 | + filename = 'local-release.tgz' |
482 | + destination = download_release(url, filename) |
483 | + expected_destination = os.path.join(os.getcwd(), 'releases', filename) |
484 | + self.assertEqual(expected_destination, destination) |
485 | + expected_log = 'Downloading release file: {} --> {}.'.format( |
486 | + url, expected_destination) |
487 | + mock_log.assert_called_once_with(expected_log) |
488 | + mock_run.assert_called_once_with( |
489 | + 'curl', '-L', '-o', expected_destination, url) |
490 | + |
491 | + |
492 | +@mock.patch('utils.log', mock.Mock()) |
493 | +class TestFetchGuiRelease(unittest.TestCase): |
494 | + |
495 | + release_path = '/my/release.tgz' |
496 | + |
497 | + @contextmanager |
498 | + def patch_launchpad(self, origin, version): |
499 | + """Mock the functions used to download a release from Launchpad. |
500 | + |
501 | + Ensure all the functions are called correctly. |
502 | + """ |
503 | + url = 'http://launchpad.example.com/release.tgz/file' |
504 | + filename = 'release.tgz' |
505 | + patch_launchpad = mock.patch('utils.Launchpad') |
506 | + patch_get_launchpad_release = mock.patch( |
507 | + 'utils.get_launchpad_release', |
508 | + mock.Mock(return_value=(url, filename)), |
509 | + ) |
510 | + patch_download_release = mock.patch( |
511 | + 'utils.download_release', |
512 | + mock.Mock(return_value=self.release_path), |
513 | + ) |
514 | + with patch_launchpad as mock_launchpad: |
515 | + with patch_get_launchpad_release as mock_get_launchpad_release: |
516 | + with patch_download_release as mock_download_release: |
517 | + yield |
518 | + login = mock_launchpad.login_anonymously |
519 | + login.assert_called_once_with('Juju GUI charm', 'production') |
520 | + mock_get_launchpad_release.assert_called_once_with( |
521 | + login().projects['juju-gui'], origin, version) |
522 | + mock_download_release.assert_called_once_with(url, filename) |
523 | + |
524 | + @mock.patch('utils.download_release') |
525 | + def test_url(self, mock_download_release): |
526 | + # The release is retrieved from an URL. |
527 | + mock_download_release.return_value = self.release_path |
528 | + url = 'http://download.example.com/release.tgz' |
529 | + path = fetch_gui_release('url', url) |
530 | + self.assertEqual(self.release_path, path) |
531 | + mock_download_release.assert_called_once_with(url, 'url-release.tgz') |
532 | + |
533 | + @mock.patch('utils.get_release_file_path') |
534 | + def test_local(self, mock_get_release_file_path): |
535 | + # The last local release is requested. |
536 | + mock_get_release_file_path.return_value = self.release_path |
537 | + path = fetch_gui_release('local', None) |
538 | + self.assertEqual(self.release_path, path) |
539 | + mock_get_release_file_path.assert_called_once_with() |
540 | + |
541 | + @mock.patch('utils.get_release_file_path') |
542 | + def test_version_found(self, mock_get_release_file_path): |
543 | + # A release version is specified and found locally. |
544 | + mock_get_release_file_path.return_value = self.release_path |
545 | + path = fetch_gui_release('stable', '0.1.42') |
546 | + self.assertEqual(self.release_path, path) |
547 | + mock_get_release_file_path.assert_called_once_with('0.1.42') |
548 | + |
549 | + @mock.patch('utils.get_release_file_path') |
550 | + def test_version_not_found(self, mock_get_release_file_path): |
551 | + # A release version is specified but not found locally. |
552 | + mock_get_release_file_path.return_value = None |
553 | + with self.patch_launchpad('stable', '0.1.42'): |
554 | + path = fetch_gui_release('stable', '0.1.42') |
555 | + self.assertEqual(self.release_path, path) |
556 | + mock_get_release_file_path.assert_called_once_with('0.1.42') |
557 | + |
558 | + @mock.patch('utils.get_release_file_path') |
559 | + def test_stable(self, mock_get_release_file_path): |
560 | + # The last stable release is requested. |
561 | + with self.patch_launchpad('stable', None): |
562 | + path = fetch_gui_release('stable', None) |
563 | + self.assertEqual(self.release_path, path) |
564 | + self.assertFalse(mock_get_release_file_path.called) |
565 | + |
566 | + @mock.patch('utils.get_release_file_path') |
567 | + def test_trunk(self, mock_get_release_file_path): |
568 | + # The last development release is requested. |
569 | + with self.patch_launchpad('trunk', None): |
570 | + path = fetch_gui_release('trunk', None) |
571 | + self.assertEqual(self.release_path, path) |
572 | + self.assertFalse(mock_get_release_file_path.called) |
573 | + |
574 | + |
575 | class TestFirstPathInDir(unittest.TestCase): |
576 | |
577 | def setUp(self): |
578 | @@ -184,6 +289,105 @@ |
579 | self.assertRaises(IOError, get_api_address, unit_dir) |
580 | |
581 | |
582 | +class TestGetReleaseFilePath(unittest.TestCase): |
583 | + |
584 | + def setUp(self): |
585 | + self.playground = tempfile.mkdtemp() |
586 | + self.addCleanup(shutil.rmtree, self.playground) |
587 | + |
588 | + def mock_releases_dir(self): |
589 | + """Mock the releases directory.""" |
590 | + return mock.patch('utils.RELEASES_DIR', self.playground) |
591 | + |
592 | + def assert_path(self, filename, path): |
593 | + """Ensure the absolute path of filename equals the given path.""" |
594 | + expected = os.path.join(self.playground, filename) |
595 | + self.assertEqual(expected, path) |
596 | + |
597 | + @contextmanager |
598 | + def assert_error(self): |
599 | + """Ensure the code executed in the context block raises a ValueError. |
600 | + |
601 | + Also check the error message. |
602 | + """ |
603 | + with self.assertRaises(ValueError) as context_manager: |
604 | + yield |
605 | + error = str(context_manager.exception) |
606 | + self.assertEqual('Error: no releases found in the charm.', error) |
607 | + |
608 | + def add(self, filename): |
609 | + """Create a release file in the playground directory.""" |
610 | + path = os.path.join(self.playground, filename) |
611 | + open(path, 'w').close() |
612 | + |
613 | + def test_last_release(self): |
614 | + # The last release is correctly retrieved. |
615 | + self.add('juju-gui-0.12.1.tgz') |
616 | + self.add('juju-gui-1.2.3.tgz') |
617 | + self.add('juju-gui-2.0.0+build.42.tgz') |
618 | + self.add('juju-gui-2.0.1.tgz') |
619 | + with self.mock_releases_dir(): |
620 | + path = get_release_file_path() |
621 | + self.assert_path('juju-gui-2.0.1.tgz', path) |
622 | + |
623 | + def test_ordering(self): |
624 | + # Release versions are correctly ordered. |
625 | + self.add('juju-gui-0.12.1.tgz') |
626 | + self.add('juju-gui-0.9.1.tgz') |
627 | + with self.mock_releases_dir(): |
628 | + path = get_release_file_path() |
629 | + self.assert_path('juju-gui-0.12.1.tgz', path) |
630 | + |
631 | + def test_no_releases(self): |
632 | + # A ValueError is raised if no releases are found. |
633 | + with self.mock_releases_dir(): |
634 | + with self.assert_error(): |
635 | + get_release_file_path() |
636 | + |
637 | + def test_no_releases_with_files(self): |
638 | + # A ValueError is raised if no releases are found. |
639 | + # Extraneous files are ignored while looking for releases. |
640 | + self.add('jujugui-1.2.3.tgz') # Wrong prefix. |
641 | + self.add('juju-gui-1.2.tgz') # Missing patch version number. |
642 | + self.add('juju-gui-1.2.3.bz2') # Wrong file extension. |
643 | + self.add('juju-gui-1.2.3.4.tgz') # Wrong version. |
644 | + self.add('juju-gui-1.2.3.build.42.tgz') # Missing "+" separator. |
645 | + self.add('juju-gui-1.2.3+built.42.tgz') # Typo. |
646 | + self.add('juju-gui-1.2.3+build.42.47.tgz') # Invalid bzr revno. |
647 | + self.add('juju-gui-1.2.3+build.42.bz2') # Wrong file extension again. |
648 | + with self.mock_releases_dir(): |
649 | + with self.assert_error(): |
650 | + print get_release_file_path() |
651 | + |
652 | + def test_stable_version(self): |
653 | + # A specific stable version is correctly retrieved. |
654 | + self.add('juju-gui-1.2.3.tgz') |
655 | + self.add('juju-gui-2.0.1+build.42.tgz') |
656 | + self.add('juju-gui-2.0.1.tgz') |
657 | + self.add('juju-gui-3.2.1.tgz') |
658 | + with self.mock_releases_dir(): |
659 | + path = get_release_file_path('2.0.1') |
660 | + self.assert_path('juju-gui-2.0.1.tgz', path) |
661 | + |
662 | + def test_development_version(self): |
663 | + # A specific development version is correctly retrieved. |
664 | + self.add('juju-gui-1.2.3+build.4247.tgz') |
665 | + self.add('juju-gui-2.42.47+build.4247.tgz') |
666 | + self.add('juju-gui-2.42.47.tgz') |
667 | + self.add('juju-gui-3.42.47+build.4247.tgz') |
668 | + with self.mock_releases_dir(): |
669 | + path = get_release_file_path('2.42.47+build.4247') |
670 | + self.assert_path('juju-gui-2.42.47+build.4247.tgz', path) |
671 | + |
672 | + def test_version_not_found(self): |
673 | + # None is returned if the requested version is not found. |
674 | + self.add('juju-gui-1.2.3.tgz') |
675 | + self.add('juju-GUI-1.42.47.tgz') # This is not a valid release. |
676 | + with self.mock_releases_dir(): |
677 | + path = get_release_file_path('1.42.47') |
678 | + self.assertIsNone(path) |
679 | + |
680 | + |
681 | class TestLegacyJuju(unittest.TestCase): |
682 | |
683 | def setUp(self): |
684 | @@ -261,7 +465,7 @@ |
685 | return self.file_link |
686 | |
687 | |
688 | -class TestGetReleaseFileUrl(unittest.TestCase): |
689 | +class TestGetLaunchpadRelease(unittest.TestCase): |
690 | |
691 | project = AttrDict( |
692 | series=( |
693 | @@ -308,43 +512,48 @@ |
694 | |
695 | def test_latest_stable_release(self): |
696 | # Ensure the correct URL is returned for the latest stable release. |
697 | - url = get_release_file_url(self.project, 'stable', None) |
698 | + url, name = get_launchpad_release(self.project, 'stable', None) |
699 | self.assertEqual('http://example.com/0.1.1.tgz', url) |
700 | + self.assertEqual('0.1.1.tgz', name) |
701 | |
702 | def test_latest_trunk_release(self): |
703 | # Ensure the correct URL is returned for the latest trunk release. |
704 | - url = get_release_file_url(self.project, 'trunk', None) |
705 | + url, name = get_launchpad_release(self.project, 'trunk', None) |
706 | self.assertEqual('http://example.com/0.1.1+build.1.tgz', url) |
707 | + self.assertEqual('0.1.1+build.1.tgz', name) |
708 | |
709 | def test_specific_stable_release(self): |
710 | # Ensure the correct URL is returned for a specific version of the |
711 | # stable release. |
712 | - url = get_release_file_url(self.project, 'stable', '0.1.0') |
713 | + url, name = get_launchpad_release(self.project, 'stable', '0.1.0') |
714 | self.assertEqual('http://example.com/0.1.0.tgz', url) |
715 | + self.assertEqual('0.1.0.tgz', name) |
716 | |
717 | def test_specific_trunk_release(self): |
718 | # Ensure the correct URL is returned for a specific version of the |
719 | # trunk release. |
720 | - url = get_release_file_url(self.project, 'trunk', '0.1.0+build.1') |
721 | + url, name = get_launchpad_release( |
722 | + self.project, 'trunk', '0.1.0+build.1') |
723 | self.assertEqual('http://example.com/0.1.0+build.1.tgz', url) |
724 | + self.assertEqual('0.1.0+build.1.tgz', name) |
725 | |
726 | def test_series_not_found(self): |
727 | # A ValueError is raised if the series cannot be found. |
728 | with self.assertRaises(ValueError) as cm: |
729 | - get_release_file_url(self.project, 'unstable', None) |
730 | + get_launchpad_release(self.project, 'unstable', None) |
731 | self.assertIn('series not found', str(cm.exception)) |
732 | |
733 | def test_no_releases(self): |
734 | # A ValueError is raised if the series does not contain releases. |
735 | project = AttrDict(series=[AttrDict(name='stable', releases=[])]) |
736 | with self.assertRaises(ValueError) as cm: |
737 | - get_release_file_url(project, 'stable', None) |
738 | + get_launchpad_release(project, 'stable', None) |
739 | self.assertIn('series does not contain releases', str(cm.exception)) |
740 | |
741 | def test_release_not_found(self): |
742 | # A ValueError is raised if the release cannot be found. |
743 | with self.assertRaises(ValueError) as cm: |
744 | - get_release_file_url(self.project, 'stable', '2.0') |
745 | + get_launchpad_release(self.project, 'stable', '2.0') |
746 | self.assertIn('release not found', str(cm.exception)) |
747 | |
748 | def test_file_not_found(self): |
749 | @@ -358,7 +567,7 @@ |
750 | ], |
751 | ) |
752 | with self.assertRaises(ValueError) as cm: |
753 | - get_release_file_url(project, 'stable', None) |
754 | + get_launchpad_release(project, 'stable', None) |
755 | self.assertIn('file not found', str(cm.exception)) |
756 | |
757 | def test_file_not_found_in_latest_release(self): |
758 | @@ -378,8 +587,9 @@ |
759 | ), |
760 | ], |
761 | ) |
762 | - url = get_release_file_url(project, 'stable', None) |
763 | + url, name = get_launchpad_release(project, 'stable', None) |
764 | self.assertEqual('http://example.com/0.1.0.tgz', url) |
765 | + self.assertEqual('0.1.0.tgz', name) |
766 | |
767 | |
768 | class TestGetZookeeperAddress(unittest.TestCase): |
769 | @@ -452,6 +662,11 @@ |
770 | # Restore the original utils.CURRENT_DIR. |
771 | utils.CURRENT_DIR = self.original_current_dir |
772 | |
773 | + def test_latest_local_release(self): |
774 | + # Ensure the latest local release is correctly parsed. |
775 | + expected = ('local', None) |
776 | + self.assertTupleEqual(expected, parse_source('local')) |
777 | + |
778 | def test_latest_stable_release(self): |
779 | # Ensure the latest stable release is correctly parsed. |
780 | expected = ('stable', None) |
Reviewers: mp+189896_ code.launchpad. net,
Message:
Please take a look.
Description:
Introduce local GUI releases.
Implemented the new "local" juju-gui-source option.
By default the GUI release is now retrieved from
the charm itself, so that in the deployment process
the charm can avoid connecting to Launchpad.
This should result in the charm being able to be
deployed behind a firewall without the traditional
workarounds. Note that this is not yet demonstrated
and must be QAed in a real firewalled environment.
The process tries is like the following:
- if juju-gui-source is "local", the last tarball
in the releases dir is installed;
- if juju-gui-source is a stable or development version,
the charm checks if that version is present in the
local repository before downloading it from Launchpad.
- the rest of the options should still work as usual
(but some QA could help, e.g. deploying from a branch).
Also updated the charm documentation and added some
missing tests.
Tests: `make unittest` from the branch root
(I am currently running the functional tests).
QA (assuming 0.10.1 is the latest GUI release): source= 0.10.0` ; source= 0.10.1` ;
- `juju bootstrap --debug`;
- `make deploy`;
- check the logs: no PPAs are used, the local release
is installed;
- `juju set juju-gui juju-gui-
- check the logs: the release is downloaded from
Launchpad;
- `juju set juju-gui juju-gui-
- check the logs: the charm successfully find
the 0.10.1 release in the local repository and
avoids downloading it from Launchpad.
- keep trying to break the charm in all the ways
you can imagine...
Thank you.
https:/ /code.launchpad .net/~frankban/ charms/ precise/ juju-gui/ local-releases/ +merge/ 189896
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/14545044/
Affected files (+395, -57 lines): juju-gui- 0.10.1. tgz functional. test
M HACKING.md
M README.md
A [revision details]
M config.yaml
M hooks/utils.py
A releases/
M revision
M tests/20-
M tests/test_utils.py