Merge lp:~frankban/juju-quickstart/apt-update-fixes into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 124
Proposed branch: lp:~frankban/juju-quickstart/apt-update-fixes
Merge into: lp:juju-quickstart
Diff against target: 363 lines (+102/-23)
6 files modified
quickstart/models/bundles.py (+9/-6)
quickstart/platform_support.py (+6/-2)
quickstart/tests/models/test_bundles.py (+48/-6)
quickstart/tests/test_app.py (+26/-4)
quickstart/tests/test_utils.py (+12/-5)
quickstart/utils.py (+1/-0)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/apt-update-fixes
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+252430@code.launchpad.net

Description of the change

Always retrieve bundle from new cs endpoint.

This is a follow up from previous branch,
and also includes the remaining bits before
2.0.1 release.

Even if a legacy bundle is specified, and even
if the legacy bundle data is retrieved for name
validation, always use the new bundle.yaml
charm store endpoint when returning the bundle
contents.

This branch also include a drive by fix to always
run `apt-get update` before installing packages,
even in the case --distro-only is enabled.

Tests: `make check`.

QA:
install bundles with quickstart:
`devenv/bin/juju-quickstart {bundle}`
Try the following bundles:
- devenv/bin/juju-quickstart mediawiki-single
- devenv/bin/juju-quickstart u/bigdata-dev/apache-analytics-sql
- devenv/bin/juju-quickstart bundle:mediawiki/scalable
- devenv/bin/juju-quickstart bundle:~landscape/landscape-dense-maas/landscape-dense-maas
- devenv/bin/juju-quickstart bundle:django/example-single

Those instead should return errors:
- devenv/bin/juju-quickstart mediawiki/trusty
- devenv/bin/juju-quickstart mediawiki-nosuch
- devenv/bin/juju-quickstart no-such
- devenv/bin/juju-quickstart bundle:no/such
- devenv/bin/juju-quickstart bundle:invalid
- devenv/bin/juju-quickstart bundle:~landscape/landscape-dense-maas/landscape

Thank you!

https://codereview.appspot.com/215750043/

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

Reviewers: mp+252430_code.launchpad.net,

Message:
Please take a look.

Description:
Always retrieve bundle from new cs endpoint.

This is a follow up from previous branch,
and also includes the remaining bits before
2.0.1 release.

Even if a legacy bundle is specified, and even
if the legacy bundle data is retrieved for name
validation, always use the new bundle.yaml
charm store endpoint when returning the bundle
contents.

This branch also include a drive by fix to always
run `apt-get update` before installing packages,
even in the case --distro-only is enabled.

Tests: `make check`.

QA:
install bundles with quickstart:
`devenv/bin/juju-quickstart {bundle}`
Try the following bundles:
- devenv/bin/juju-quickstart mediawiki-single
- devenv/bin/juju-quickstart u/bigdata-dev/apache-analytics-sql
- devenv/bin/juju-quickstart bundle:mediawiki/scalable
- devenv/bin/juju-quickstart
bundle:~landscape/landscape-dense-maas/landscape-dense-maas
- devenv/bin/juju-quickstart bundle:django/example-single

Those instead should return errors:
- devenv/bin/juju-quickstart mediawiki/trusty
- devenv/bin/juju-quickstart mediawiki-nosuch
- devenv/bin/juju-quickstart no-such
- devenv/bin/juju-quickstart bundle:no/such
- devenv/bin/juju-quickstart bundle:invalid
- devenv/bin/juju-quickstart
bundle:~landscape/landscape-dense-maas/landscape

Thank you!

https://code.launchpad.net/~frankban/juju-quickstart/apt-update-fixes/+merge/252430

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/215750043/

Affected files (+104, -23 lines):
   A [revision details]
   M quickstart/models/bundles.py
   M quickstart/platform_support.py
   M quickstart/tests/models/test_bundles.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_utils.py
   M quickstart/utils.py

Revision history for this message
Brad Crittenden (bac) wrote :
Revision history for this message
Richard Harding (rharding) wrote :
Revision history for this message
Richard Harding (rharding) wrote :

On 2015/03/10 13:55:46, rharding wrote:
> LGTM no qa atm

QA OK thanks!

https://codereview.appspot.com/215750043/

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

*** Submitted:

Always retrieve bundle from new cs endpoint.

This is a follow up from previous branch,
and also includes the remaining bits before
2.0.1 release.

Even if a legacy bundle is specified, and even
if the legacy bundle data is retrieved for name
validation, always use the new bundle.yaml
charm store endpoint when returning the bundle
contents.

This branch also include a drive by fix to always
run `apt-get update` before installing packages,
even in the case --distro-only is enabled.

Tests: `make check`.

QA:
install bundles with quickstart:
`devenv/bin/juju-quickstart {bundle}`
Try the following bundles:
- devenv/bin/juju-quickstart mediawiki-single
- devenv/bin/juju-quickstart u/bigdata-dev/apache-analytics-sql
- devenv/bin/juju-quickstart bundle:mediawiki/scalable
- devenv/bin/juju-quickstart
bundle:~landscape/landscape-dense-maas/landscape-dense-maas
- devenv/bin/juju-quickstart bundle:django/example-single

Those instead should return errors:
- devenv/bin/juju-quickstart mediawiki/trusty
- devenv/bin/juju-quickstart mediawiki-nosuch
- devenv/bin/juju-quickstart no-such
- devenv/bin/juju-quickstart bundle:no/such
- devenv/bin/juju-quickstart bundle:invalid
- devenv/bin/juju-quickstart
bundle:~landscape/landscape-dense-maas/landscape

Thank you!

R=bac, rharding
CC=
https://codereview.appspot.com/215750043

https://codereview.appspot.com/215750043/

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

Thanks for the quick reviews and QA Brad and Rick!

https://codereview.appspot.com/215750043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/models/bundles.py'
2--- quickstart/models/bundles.py 2015-03-10 10:18:58 +0000
3+++ quickstart/models/bundles.py 2015-03-10 13:22:58 +0000
4@@ -142,7 +142,10 @@
5 """
6 if source.startswith('bundle:'):
7 # The source refers to an old style bundle URL.
8- return _bundle_from_charmworld_url(source)
9+ try:
10+ return _bundle_from_charmworld_url(source)
11+ except charmstore.NotFoundError as err:
12+ raise IOError(bytes(err))
13
14 has_extension = source.endswith('.yaml') or source.endswith('.json')
15 is_remote = source.startswith('http://') or source.startswith('https://')
16@@ -200,6 +203,7 @@
17 content is not valid.
18 Raise a IOError if a problem is encountered while fetching the YAML
19 content from the charm store.
20+ Raise a charmstore.NotFoundError if the bundle is not found.
21 """
22 match = _charmworld_url_expression.match(url)
23 if match is None:
24@@ -224,11 +228,10 @@
25 # in the original YAML is also required.
26 reference = references.Reference(
27 'cs', user, 'bundle', basket, revision)
28- try:
29- data = charmstore.get_legacy_bundle_data(reference)
30- except charmstore.NotFoundError as err:
31- raise IOError(bytes(err))
32- data = _flatten_data(data, name)
33+ # Validate the bundle name is included in the legacy data.
34+ _flatten_data(charmstore.get_legacy_bundle_data(reference), name)
35+ # Retrieve the new bundle data corresponding to the shorter reference.
36+ data = charmstore.get_bundle_data(reference)
37
38 validate(data)
39 # XXX frankban 2015-02-26: remove this when switching to the new bundle
40
41=== modified file 'quickstart/platform_support.py'
42--- quickstart/platform_support.py 2015-01-30 15:27:07 +0000
43+++ quickstart/platform_support.py 2015-03-10 13:22:58 +0000
44@@ -83,10 +83,14 @@
45 print('sudo privileges will be used for the installation of \n'
46 'the following packages: {}\n'
47 'this can take a while...'.format(', '.join(required_packages)))
48+ if distro_only:
49+ retcode, _, error = utils.call('sudo', '/usr/bin/apt-get', 'update')
50+ if retcode:
51+ raise OSError(error.encode('utf-8'))
52 retcode, _, error = utils.call(
53 'sudo', '/usr/bin/apt-get', 'install', '-y', *required_packages)
54 if retcode:
55- raise OSError(bytes(error))
56+ raise OSError(error.encode('utf-8'))
57
58
59 def _installer_osx(distro_only, required_packages):
60@@ -102,7 +106,7 @@
61 retcode, _, error = utils.call(
62 '/usr/local/bin/brew', 'install', *required_packages)
63 if retcode:
64- raise OSError(bytes(error))
65+ raise OSError(error.encode('utf-8'))
66
67
68 INSTALLERS = {
69
70=== modified file 'quickstart/tests/models/test_bundles.py'
71--- quickstart/tests/models/test_bundles.py 2015-03-09 17:50:28 +0000
72+++ quickstart/tests/models/test_bundles.py 2015-03-10 13:22:58 +0000
73@@ -121,8 +121,10 @@
74 side_effect = [
75 # A first call urlread returns a not found error.
76 netutils.NotFoundError('boo!'),
77- # A second call succeeds.
78+ # A second call to retrieve the legacy data succeeds.
79 self.legacy_bundle_content,
80+ # The third call to retrieve the new data succeeds.
81+ self.bundle_content,
82 ]
83 with self.patch_urlread(error=side_effect) as mock_urlread:
84 bundle = bundles.from_source('bundle:mediawiki/bundle1')
85@@ -132,10 +134,11 @@
86 # XXX frankban 2015-03-09: remove this check once we get rid of the
87 # charmworld id concept.
88 self.assertEqual('mediawiki/bundle1', bundle.reference.charmworld_id)
89- # The urlread function has been called two times: the first time
90+ # The urlread function has been called three times: the first time
91 # including both the basket and the bundle name, the second time
92- # to retrieve the legacy bundle yaml, only including the basket.
93- self.assertEqual(mock_urlread.call_count, 2)
94+ # to retrieve the legacy bundle YAML, the third time to retrieve the
95+ # new bundle YAML (this time without including the basket name).
96+ self.assertEqual(len(side_effect), mock_urlread.call_count)
97 mock_urlread.assert_has_calls([
98 mock.call(
99 settings.CHARMSTORE_API +
100@@ -143,6 +146,9 @@
101 mock.call(
102 settings.CHARMSTORE_API +
103 'bundle/mediawiki/archive/bundles.yaml.orig'),
104+ mock.call(
105+ settings.CHARMSTORE_API +
106+ 'bundle/mediawiki/archive/bundle.yaml'),
107 ])
108
109 def test_charmworld_bundle_deprecation_warning(self):
110@@ -193,7 +199,7 @@
111 # The urlread function has been called two times: the first time
112 # including both the basket and the bundle name, the second time
113 # to retrieve the legacy bundle yaml, only including the basket.
114- self.assertEqual(mock_urlread.call_count, 2)
115+ self.assertEqual(2, mock_urlread.call_count)
116 mock_urlread.assert_has_calls([
117 mock.call(
118 settings.CHARMSTORE_API +
119@@ -220,7 +226,7 @@
120 # The urlread function has been called two times: the first time
121 # including both the basket and the bundle name, the second time
122 # to retrieve the legacy bundle yaml, only including the basket.
123- self.assertEqual(mock_urlread.call_count, 2)
124+ self.assertEqual(len(side_effect), mock_urlread.call_count)
125 mock_urlread.assert_has_calls([
126 mock.call(
127 settings.CHARMSTORE_API +
128@@ -230,6 +236,42 @@
129 'bundle/django/archive/bundles.yaml.orig'),
130 ])
131
132+ def test_charmworld_bundle_from_legacy_not_found_error(self):
133+ # An IOError is raised if the legacy bundle cannot be found.
134+ side_effect = [
135+ # A first call urlread returns a not found error.
136+ netutils.NotFoundError('boo!'),
137+ # A second call to retrieve the legacy bundle data succeeds.
138+ self.legacy_bundle_content,
139+ # The third call to get the bundle from the new API endpoint fails.
140+ # Note that this is unlikely to happen.
141+ netutils.NotFoundError('what!'),
142+ ]
143+ expected_error = (
144+ 'charm store resource not found at '
145+ '{}bundle/django/archive/bundle.yaml: '
146+ 'what!'.format(settings.CHARMSTORE_API))
147+ with self.patch_urlread(error=side_effect) as mock_urlread:
148+ with self.assertRaises(IOError) as ctx:
149+ bundles.from_source('bundle:django/bundle1')
150+ self.assertEqual(expected_error, bytes(ctx.exception))
151+ # The urlread function has been called three times: the first time
152+ # including both the basket and the bundle name, the second time
153+ # to retrieve the legacy bundle YAML, the third time to retrieve the
154+ # new bundle YAML (this time without including the basket name).
155+ self.assertEqual(len(side_effect), mock_urlread.call_count)
156+ mock_urlread.assert_has_calls([
157+ mock.call(
158+ settings.CHARMSTORE_API +
159+ 'bundle/django-bundle1/archive/bundle.yaml'),
160+ mock.call(
161+ settings.CHARMSTORE_API +
162+ 'bundle/django/archive/bundles.yaml.orig'),
163+ mock.call(
164+ settings.CHARMSTORE_API +
165+ 'bundle/django/archive/bundle.yaml'),
166+ ])
167+
168 def test_jujucharms_bundle(self):
169 # A bundle instance is properly returned from a jujucharms.com id.
170 with self.patch_urlread(contents=self.bundle_content) as mock_urlread:
171
172=== modified file 'quickstart/tests/test_app.py'
173--- quickstart/tests/test_app.py 2015-03-09 17:50:28 +0000
174+++ quickstart/tests/test_app.py 2015-03-10 13:22:58 +0000
175@@ -98,9 +98,10 @@
176 side_effects = (
177 (127, '', 'no juju'), # Retrieve the Juju version.
178 (0, 'saucy', ''), # Retrieve the Ubuntu release codename.
179+ (0, 'update 1', ''), # Update the repository with new sources.
180 (0, 'install add repo', ''), # Install add-apt-repository.
181 (0, 'add repo', ''), # Add the juju stable repository.
182- (0, 'update', ''), # Update the repository with new sources.
183+ (0, 'update 2', ''), # Update the repository with new sources.
184 (0, 'install', ''), # Install missing packages.
185 (0, '1.18.0', ''), # Retrieve the version again.
186 )
187@@ -109,6 +110,7 @@
188 mock_call.assert_has_calls([
189 mock.call(self.juju_command, 'version'),
190 mock.call('lsb_release', '-cs'),
191+ mock.call('sudo', self.apt_get, 'update'),
192 mock.call('sudo', self.apt_get, 'install', '-y',
193 'software-properties-common'),
194 mock.call('sudo', self.add_repository, '-y', 'ppa:juju/stable'),
195@@ -161,6 +163,7 @@
196 # All the missing packages are installed from the distro repository.
197 side_effects = (
198 (127, '', 'no juju'), # Retrieve the Juju version.
199+ (0, 'update', ''), # Update the repository with new sources.
200 (0, 'install', ''), # Install missing packages.
201 (0, '1.17.42', ''), # Retrieve the version again.
202 )
203@@ -169,6 +172,7 @@
204 self.assertEqual(len(side_effects), mock_call.call_count)
205 mock_call.assert_has_calls([
206 mock.call(self.juju_command, 'version'),
207+ mock.call('sudo', self.apt_get, 'update'),
208 mock.call('sudo', self.apt_get, 'install', '-y',
209 'juju-core', 'juju-local'),
210 mock.call(self.juju_command, 'version'),
211@@ -199,9 +203,10 @@
212 (0, '1.16.42', ''), # Check the juju command.
213 (127, '', 'no lxc'), # Check the lxc-ls command.
214 (0, 'saucy', ''), # Retrieve the Ubuntu release codename.
215+ (0, 'update 1', ''), # Update the repository with new sources.
216 (0, 'install add repo', ''), # Install add-apt-repository.
217 (0, 'add repo', ''), # Add the juju stable repository.
218- (0, 'update', ''), # Update the repository with new sources.
219+ (0, 'update 2', ''), # Update the repository with new sources.
220 (0, 'install', ''), # Install missing packages.
221 )
222 mock_call, juju_version = self.call_ensure_dependencies(side_effects)
223@@ -210,6 +215,7 @@
224 mock.call(self.juju_command, 'version'),
225 mock.call('/usr/bin/lxc-ls'),
226 mock.call('lsb_release', '-cs'),
227+ mock.call('sudo', self.apt_get, 'update'),
228 mock.call('sudo', self.apt_get, 'install', '-y',
229 'software-properties-common'),
230 mock.call('sudo', self.add_repository, '-y', 'ppa:juju/stable'),
231@@ -231,6 +237,7 @@
232 side_effects = (
233 (0, '1.16.42', ''), # Check the juju command.
234 (127, '', 'no lxc'), # Check the lxc-ls command.
235+ (0, 'update', ''), # Update the repository with new sources.
236 (0, 'install', ''), # Install missing packages.
237 )
238 mock_call, juju_version = self.call_ensure_dependencies(
239@@ -239,6 +246,7 @@
240 mock_call.assert_has_calls([
241 mock.call(self.juju_command, 'version'),
242 mock.call('/usr/bin/lxc-ls'),
243+ mock.call('sudo', self.apt_get, 'update'),
244 mock.call('sudo', self.apt_get, 'install', '-y', 'juju-local'),
245 ])
246 mock_print.assert_called_once_with(
247@@ -252,6 +260,7 @@
248 side_effects = (
249 (127, '', 'no juju'), # Check the juju command.
250 (0, 'saucy', ''), # Retrieve the Ubuntu release codename.
251+ (0, 'update', ''), # Update the repository with new sources.
252 (0, 'install add repo', ''), # Install add-apt-repository.
253 (1, '', 'add repo error'), # Add the juju stable repository.
254 )
255@@ -264,15 +273,27 @@
256 side_effects = (
257 (127, '', 'no juju'), # Check the juju command.
258 (0, 'saucy', ''), # Retrieve the Ubuntu release codename.
259+ (0, 'update 1', ''), # Update the repository with new sources.
260 (0, 'install add repo', ''), # Install add-apt-repository.
261 (0, 'add repo', ''), # Add the juju stable repository.
262- (0, 'update', ''), # Update the repository with new sources.
263+ (0, 'update 2', ''), # Update the repository with new sources.
264 (1, '', 'install error'), # Install missing packages.
265 )
266 with self.assert_program_exit('install error'):
267 mock_call = self.call_ensure_dependencies(side_effects)[0]
268 self.assertEqual(3, mock_call.call_count)
269
270+ def test_distro_only_update_failure_apt(self, mock_print):
271+ # A ProgramExit is raised if updating APT sources fails.
272+ side_effects = (
273+ (127, '', 'no juju'), # Retrieve the Juju version.
274+ (1, '', 'update error'), # Update the repository with new sources.
275+ )
276+ with self.assert_program_exit('update error'):
277+ mock_call = self.call_ensure_dependencies(
278+ side_effects, distro_only=True)[0]
279+ self.assertEqual(2, mock_call.call_count)
280+
281 def test_failed_install_osx(self, mock_print):
282 # A ProgramExit is raised if the packages installation fails.
283 side_effects = (
284@@ -292,9 +313,10 @@
285 side_effects = (
286 (127, '', 'no juju'), # Check the juju command.
287 (0, 'saucy', ''), # Retrieve the Ubuntu release codename.
288+ (0, 'update 1', ''), # Update the repository with new sources.
289 (0, 'install add repo', ''), # Install add-apt-repository.
290 (0, 'add repo', ''), # Add the juju stable repository.
291- (0, 'update', ''), # Update the repository with new sources.
292+ (0, 'update 2', ''), # Update the repository with new sources.
293 (0, 'install', ''), # Install missing packages.
294 (127, '', 'no juju (again)'), # Retrieve the Juju version.
295 )
296
297=== modified file 'quickstart/tests/test_utils.py'
298--- quickstart/tests/test_utils.py 2015-02-09 12:34:33 +0000
299+++ quickstart/tests/test_utils.py 2015-03-10 13:22:58 +0000
300@@ -40,9 +40,10 @@
301 apt_get = '/usr/bin/apt-get'
302 repository = 'ppa:good/stuff'
303 side_effects = (
304+ (0, 'first update', ''), # Update the global repository.
305 (0, 'apt-get install', ''), # Install add-apt-repository.
306 (0, 'add-apt-repository', ''), # Add the repository.
307- (0, 'update', ''), # Update the global repository
308+ (0, 'second update', ''), # Update the global repository.
309 )
310
311 def patch_codename(self, codename):
312@@ -59,6 +60,7 @@
313 mock_get_ubuntu_codename.assert_called_once_with()
314 self.assertEqual(len(self.side_effects), mock_call.call_count)
315 mock_call.assert_has_calls([
316+ mock.call('sudo', self.apt_get, 'update'),
317 mock.call('sudo', self.apt_get, 'install', '-y',
318 'python-software-properties'),
319 mock.call('sudo', self.apt_add_repository, '-y', self.repository),
320@@ -73,6 +75,7 @@
321 mock_get_ubuntu_codename.assert_called_once_with()
322 self.assertEqual(len(self.side_effects), mock_call.call_count)
323 mock_call.assert_has_calls([
324+ mock.call('sudo', self.apt_get, 'update'),
325 mock.call('sudo', self.apt_get, 'install', '-y',
326 'software-properties-common'),
327 mock.call('sudo', self.apt_add_repository, '-y', self.repository),
328@@ -92,15 +95,19 @@
329
330 def test_command_error(self, mock_print):
331 # An OSError is raised if a command error occurs.
332- side_effects = [(1, '', 'apt-get install error')]
333+ side_effects = [(0, 'update', ''), (1, '', 'apt-get install error')]
334 with self.patch_codename('quantal') as mock_get_ubuntu_codename:
335 with self.patch_multiple_calls(side_effects) as mock_call:
336 with self.assertRaises(OSError) as context_manager:
337 utils.add_apt_repository(self.repository)
338 mock_get_ubuntu_codename.assert_called_once_with()
339- mock_call.assert_called_once_with(
340- 'sudo', self.apt_get, 'install', '-y',
341- 'software-properties-common')
342+ self.assertEqual(len(side_effects), mock_call.call_count)
343+ mock_call.assert_has_calls([
344+ mock.call('sudo', self.apt_get, 'update'),
345+ mock.call(
346+ 'sudo', self.apt_get, 'install', '-y',
347+ 'software-properties-common'),
348+ ])
349 self.assertEqual(
350 'apt-get install error', bytes(context_manager.exception))
351
352
353=== modified file 'quickstart/utils.py'
354--- quickstart/utils.py 2015-03-10 10:18:58 +0000
355+++ quickstart/utils.py 2015-03-10 13:22:58 +0000
356@@ -48,6 +48,7 @@
357 if get_ubuntu_codename() == 'precise':
358 add_repository_package = 'python-software-properties'
359 commands = (
360+ ('/usr/bin/apt-get', 'update'),
361 ('/usr/bin/apt-get', 'install', '-y', add_repository_package),
362 ('/usr/bin/add-apt-repository', '-y', repository),
363 ('/usr/bin/apt-get', 'update'),

Subscribers

People subscribed via source and target branches