Merge lp:~frankban/juju-quickstart/sudo-fixes into lp:juju-quickstart
- sudo-fixes
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 22 |
Proposed branch: | lp:~frankban/juju-quickstart/sudo-fixes |
Merge into: | lp:juju-quickstart |
Diff against target: |
616 lines (+266/-105) 7 files modified
quickstart/__init__.py (+1/-1) quickstart/app.py (+21/-24) quickstart/tests/helpers.py (+4/-0) quickstart/tests/test_app.py (+111/-78) quickstart/tests/test_manage.py (+2/-2) quickstart/tests/test_utils.py (+90/-0) quickstart/utils.py (+37/-0) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/sudo-fixes |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
Install missing packages for add-apt-repository.
Also use absolute paths to commands executed
with sudo privileges.
Tests: `make check`
QA:
1) Create a saucy LXC sharing your home directory,
e.g. `sudo lxc-create -t ubuntu -n quickstart -f <MY-TEMPLATE> \
-- -r saucy -a amd64 -b $USER`
where "quickstart" is the name of the container,
"-r" is used to specify the release to use,
"-b" binds the home directory of the specified user,
and <MY-TEMPLATE> is a file with the following contents:
I assume you already have:
- a juju home containing the environments.yaml file
configured with an "ec2" ec2 environment;
- your ssh keys properly set up;
- run the tests with `make check` as described above.
So at this point the container does not have juju
installed, but the juju home and ssh keys are
available, and so the branch with a configured testing
virtualenv. We already have cards for environment
creation and ssh keys handling.
2) Start the LXC instance (`sudo lxc-start -n quickstart`).
3) Open a console inside the LXC with
`sudo lxc-console -n quickstart`, log in using your user
credentials, and cd into the directory where you checked
out this branch.
4) Run `.venv/bin/python juju-quickstart -e ec2 --no-browser`.
You should be asked the sudo password in order to add
the missing PPA and install juju-core and lxc.
Note that installing the packages can take some minutes.
The process will then proceed as usual.
5) Run `.venv/bin/python juju-quickstart -e ec2 --no-browser`
again: this time no packages installation should be required,
and quickstart just reuses the existing environment.
6) From the host, stop and destroy the LXC container:
`sudo lxc-stop -n quickstart` and `sudo lxc-destroy -n quickstart`.
7) Destroy your ec2 environment.
Thank you!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
- 26. By Francesco Banconi
-
Fixes as per review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
Please take a look.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Brad Crittenden (bac) wrote : | # |
Code LGTM. QA had one problem, which you've already fixed. QA on patch
was successful.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Richard Harding (rharding) wrote : | # |
LGTM with a question and suggestion.
https:/
File quickstart/app.py (right):
https:/
quickstart/
containers used by
shouldn't this dep be based on if your env is lxc vs just if the lxc-ls
command is available? Will this try to install lxc if I'm using
quickstart on non-local provider installs?
https:/
quickstart/
'--format', 'json')
maybe providing a COMMANDS or something list where this is hard coded
will be easier to maintain and less finding of paths through the code.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
On 2013/11/26 14:36:25, rharding wrote:
> LGTM with a question and suggestion.
> https:/
> File quickstart/app.py (right):
https:/
> quickstart/
containers
> used by
> shouldn't this dep be based on if your env is lxc vs just if the
lxc-ls command
> is available? Will this try to install lxc if I'm using quickstart on
non-local
> provider installs?
https:/
> quickstart/
env_name,
> '--format', 'json')
> maybe providing a COMMANDS or something list where this is hard coded
will be
> easier to maintain and less finding of paths through the code.
Both good points, thank you.
We agreed they are out of scope for this branch, so I'll create cards.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Install missing packages for add-apt-repository.
Also use absolute paths to commands executed
with sudo privileges.
Tests: `make check`
QA:
1) Create a saucy LXC sharing your home directory,
e.g. `sudo lxc-create -t ubuntu -n quickstart -f <MY-TEMPLATE> \
-- -r saucy -a amd64 -b $USER`
where "quickstart" is the name of the container,
"-r" is used to specify the release to use,
"-b" binds the home directory of the specified user,
and <MY-TEMPLATE> is a file with the following contents:
I assume you already have:
- a juju home containing the environments.yaml file
configured with an "ec2" ec2 environment;
- your ssh keys properly set up;
- run the tests with `make check` as described above.
So at this point the container does not have juju
installed, but the juju home and ssh keys are
available, and so the branch with a configured testing
virtualenv. We already have cards for environment
creation and ssh keys handling.
2) Start the LXC instance (`sudo lxc-start -n quickstart`).
3) Open a console inside the LXC with
`sudo lxc-console -n quickstart`, log in using your user
credentials, and cd into the directory where you checked
out this branch.
4) Run `.venv/bin/python juju-quickstart -e ec2 --no-browser`.
You should be asked the sudo password in order to add
the missing PPA and install juju-core and lxc.
Note that installing the packages can take some minutes.
The process will then proceed as usual.
5) Run `.venv/bin/python juju-quickstart -e ec2 --no-browser`
again: this time no packages installation should be required,
and quickstart just reuses the existing environment.
6) From the host, stop and destroy the LXC container:
`sudo lxc-stop -n quickstart` and `sudo lxc-destroy -n quickstart`.
7) Destroy your ec2 environment.
Thank you!
R=bac, rharding
CC=
https:/
Preview Diff
1 | === modified file 'quickstart/__init__.py' |
2 | --- quickstart/__init__.py 2013-11-22 08:30:42 +0000 |
3 | +++ quickstart/__init__.py 2013-11-26 14:39:39 +0000 |
4 | @@ -19,7 +19,7 @@ |
5 | that it can be managed using a Web interface (the Juju GUI). |
6 | """ |
7 | |
8 | -VERSION = (0, 4, 2) |
9 | +VERSION = (0, 4, 3) |
10 | |
11 | |
12 | def get_version(): |
13 | |
14 | === modified file 'quickstart/app.py' |
15 | --- quickstart/app.py 2013-11-22 08:32:44 +0000 |
16 | +++ quickstart/app.py 2013-11-26 14:39:39 +0000 |
17 | @@ -55,31 +55,28 @@ |
18 | Raise a ProgramExit if an error occurs installing. |
19 | """ |
20 | required_packages = [] |
21 | - retcode = utils.call('juju', 'version')[0] |
22 | + retcode = utils.call('/usr/bin/juju', 'version')[0] |
23 | if retcode > 0: |
24 | required_packages.append('juju-core') |
25 | - retcode = utils.call('lxc-ls')[0] |
26 | + retcode = utils.call('/usr/bin/lxc-ls')[0] |
27 | if retcode > 0: |
28 | - required_packages.append('lxc') |
29 | + # The lxc package is required to create the LXC containers used by |
30 | + # the local provider. When the local provider is enabled, the host |
31 | + # itself is used as bootstrap node. For this reason, the mongodb-server |
32 | + # package from the Juju stable PPA is also required. |
33 | + required_packages.extend(['lxc', 'mongodb-server']) |
34 | if required_packages: |
35 | - if len(required_packages) == 1: |
36 | - plural_parts = 'package needs' |
37 | - else: |
38 | - plural_parts = 'packages need' |
39 | - |
40 | - print('The following {} to be installed: {}'.format( |
41 | - plural_parts, ', '.join(required_packages))) |
42 | - print('sudo privileges are required for package installation.') |
43 | - |
44 | - cmds = ( |
45 | - ('apt-add-repository', '-y', 'ppa:juju/stable'), |
46 | - ('apt-get', 'update'), |
47 | - ['apt-get', 'install', '-y'] + required_packages, |
48 | - ) |
49 | - for cmd in cmds: |
50 | - retcode, _, error = utils.call('sudo', *cmd) |
51 | - if retcode: |
52 | - raise ProgramExit(error) |
53 | + try: |
54 | + utils.add_apt_repository('ppa:juju/stable') |
55 | + except OSError as err: |
56 | + raise ProgramExit(str(err)) |
57 | + print('installing the following packages: {} ' |
58 | + '(this can take a while)'.format(', '.join(required_packages))) |
59 | + print('sudo privileges will be used for package installation') |
60 | + retcode, _, error = utils.call( |
61 | + 'sudo', '/usr/bin/apt-get', 'install', '-y', *required_packages) |
62 | + if retcode: |
63 | + raise ProgramExit(error) |
64 | |
65 | |
66 | def bootstrap(env_name, is_local=False, debug=False): |
67 | @@ -100,7 +97,7 @@ |
68 | Raise a ProgramExit if any error occurs in the bootstrap process. |
69 | """ |
70 | already_bootstrapped = False |
71 | - cmd = ['juju', 'bootstrap', '-e', env_name] |
72 | + cmd = ['/usr/bin/juju', 'bootstrap', '-e', env_name] |
73 | # XXX frankban 2013-11-20: avoid using sudo once this is no longer required |
74 | # in juju-core. |
75 | if is_local: |
76 | @@ -140,7 +137,7 @@ |
77 | timeout = time.time() + (60*10) |
78 | while time.time() < timeout: |
79 | retcode, output, error = utils.call( |
80 | - 'juju', 'status', '-e', env_name, '--format', 'yaml') |
81 | + '/usr/bin/juju', 'status', '-e', env_name, '--format', 'yaml') |
82 | if retcode: |
83 | continue |
84 | # Ensure the state server is up and the agent is started. |
85 | @@ -166,7 +163,7 @@ |
86 | Raise a ProgramExit if any error occurs. |
87 | """ |
88 | retcode, output, error = utils.call( |
89 | - 'juju', 'api-endpoints', '-e', env_name, '--format', 'json') |
90 | + '/usr/bin/juju', 'api-endpoints', '-e', env_name, '--format', 'json') |
91 | if retcode: |
92 | raise ProgramExit(error) |
93 | # Assuming there is always at least one API address, grab the first one |
94 | |
95 | === modified file 'quickstart/tests/helpers.py' |
96 | --- quickstart/tests/helpers.py 2013-11-22 08:32:44 +0000 |
97 | +++ quickstart/tests/helpers.py 2013-11-26 14:39:39 +0000 |
98 | @@ -121,6 +121,10 @@ |
99 | return env_file.name |
100 | |
101 | |
102 | +# Mock the builtin print function. |
103 | +mock_print = mock.patch('__builtin__.print') |
104 | + |
105 | + |
106 | class UrlReadTestsMixin(object): |
107 | """Expose a method to mock the quickstart.utils.urlread helper function.""" |
108 | |
109 | |
110 | === modified file 'quickstart/tests/test_app.py' |
111 | --- quickstart/tests/test_app.py 2013-11-22 09:55:35 +0000 |
112 | +++ quickstart/tests/test_app.py 2013-11-26 14:39:39 +0000 |
113 | @@ -31,9 +31,6 @@ |
114 | from quickstart.tests import helpers |
115 | |
116 | |
117 | -mock_print = mock.patch('__builtin__.print') |
118 | - |
119 | - |
120 | class TestProgramExit(unittest.TestCase): |
121 | |
122 | def test_string_representation(self): |
123 | @@ -61,94 +58,128 @@ |
124 | return jujuclient.EnvError({'Error': message}) |
125 | |
126 | |
127 | -@mock_print |
128 | +@helpers.mock_print |
129 | class TestEnsureDependencies( |
130 | helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
131 | |
132 | + add_repository = '/usr/bin/add-apt-repository' |
133 | + apt_get = '/usr/bin/apt-get' |
134 | + |
135 | def call_ensure_dependencies(self, call_effects): |
136 | with self.patch_multiple_calls(call_effects) as mock_call: |
137 | app.ensure_dependencies() |
138 | return mock_call |
139 | |
140 | def test_success_install(self, mock_print): |
141 | - call = self.call_ensure_dependencies( |
142 | - ( |
143 | - (127, '', 'no juju'), |
144 | - (127, '', 'no lxc'), |
145 | - (0, 'add repo', ''), |
146 | - (0, 'update', ''), |
147 | - (0, 'install', ''), |
148 | - ) |
149 | + # All the missing packages are installed from the PPA. |
150 | + side_effects = ( |
151 | + (127, '', 'no juju'), # Check the juju command. |
152 | + (127, '', 'no lxc'), # Check the lxc-ls command. |
153 | + (0, 'saucy', ''), # Retrieve the Ubuntu release codename. |
154 | + (0, 'install add repo', ''), # Install add-apt-repository. |
155 | + (0, 'add repo', ''), # Add the juju stable repository. |
156 | + (0, 'update', ''), # Update the repository with new sources. |
157 | + (0, 'install', ''), # Install missing packages. |
158 | ) |
159 | - self.assertEqual(call.call_count, 5) |
160 | - call.assert_has_calls([ |
161 | - mock.call('juju', 'version'), |
162 | - mock.call('lxc-ls'), |
163 | - mock.call('sudo', 'apt-add-repository', '-y', 'ppa:juju/stable'), |
164 | - mock.call('sudo', 'apt-get', 'update'), |
165 | - mock.call('sudo', 'apt-get', 'install', '-y', 'juju-core', 'lxc'), |
166 | + mock_call = self.call_ensure_dependencies(side_effects) |
167 | + self.assertEqual(len(side_effects), mock_call.call_count) |
168 | + mock_call.assert_has_calls([ |
169 | + mock.call('/usr/bin/juju', 'version'), |
170 | + mock.call('/usr/bin/lxc-ls'), |
171 | + mock.call('lsb_release', '-cs'), |
172 | + mock.call('sudo', self.apt_get, 'install', '-y', |
173 | + 'software-properties-common'), |
174 | + mock.call('sudo', self.add_repository, '-y', 'ppa:juju/stable'), |
175 | + mock.call('sudo', self.apt_get, 'update'), |
176 | + mock.call('sudo', self.apt_get, 'install', '-y', |
177 | + 'juju-core', 'lxc', 'mongodb-server'), |
178 | ]) |
179 | mock_print.assert_has_calls([ |
180 | - mock.call( |
181 | - 'The following packages need to be installed: juju-core, lxc'), |
182 | - mock.call( |
183 | - 'sudo privileges are required for package installation.'), |
184 | + mock.call('sudo privileges are required for PPA installation'), |
185 | + mock.call('installing the following packages: juju-core, ' |
186 | + 'lxc, mongodb-server (this can take a while)'), |
187 | + mock.call('sudo privileges will be used for package installation'), |
188 | ]) |
189 | |
190 | def test_success_no_install(self, mock_print): |
191 | - call = self.call_ensure_dependencies( |
192 | - [ |
193 | - (0, '1.16', ''), |
194 | - (0, '', ''), |
195 | - (0, 'add repo', ''), |
196 | - (0, 'update', ''), |
197 | - (0, 'install', ''), |
198 | - ] |
199 | + # There is no need to install packages/PPAs if everything is already |
200 | + # set up. |
201 | + side_effects = ( |
202 | + (0, '1.16', ''), # Check the juju command. |
203 | + (0, '', ''), # Check the lxc-ls command. |
204 | + # The remaining call should be ignored. |
205 | + (1, '', 'not ignored'), |
206 | ) |
207 | - self.assertEqual(call.call_count, 2) |
208 | + mock_call = self.call_ensure_dependencies(side_effects) |
209 | + self.assertEqual(2, mock_call.call_count) |
210 | + self.assertFalse(mock_print.called) |
211 | |
212 | - def test_success_one_install(self, mock_print): |
213 | + def test_success_partial_install(self, mock_print): |
214 | # One missing installation is correctly handled. |
215 | - call = self.call_ensure_dependencies([ |
216 | - (0, '1.16', ''), |
217 | - (127, '', 'no lxc'), |
218 | - (0, 'add repo', ''), |
219 | - (0, 'update', ''), |
220 | - (0, 'install', ''), |
221 | - ]) |
222 | - self.assertEqual(call.call_count, 5) |
223 | - call.assert_has_calls([ |
224 | - mock.call('juju', 'version'), |
225 | - mock.call('lxc-ls'), |
226 | - mock.call('sudo', 'apt-add-repository', '-y', 'ppa:juju/stable'), |
227 | - mock.call('sudo', 'apt-get', 'update'), |
228 | - mock.call('sudo', 'apt-get', 'install', '-y', 'lxc'), |
229 | + side_effects = ( |
230 | + (0, '1.16', ''), # Check the juju command. |
231 | + (127, '', 'no lxc'), # Check the lxc-ls command. |
232 | + (0, 'saucy', ''), # Retrieve the Ubuntu release codename. |
233 | + (0, 'install add repo', ''), # Install add-apt-repository. |
234 | + (0, 'add repo', ''), # Add the juju stable repository. |
235 | + (0, 'update', ''), # Update the repository with new sources. |
236 | + (0, 'install', ''), # Install missing packages. |
237 | + ) |
238 | + mock_call = self.call_ensure_dependencies(side_effects) |
239 | + self.assertEqual(len(side_effects), mock_call.call_count) |
240 | + mock_call.assert_has_calls([ |
241 | + mock.call('/usr/bin/juju', 'version'), |
242 | + mock.call('/usr/bin/lxc-ls'), |
243 | + mock.call('lsb_release', '-cs'), |
244 | + mock.call('sudo', self.apt_get, 'install', '-y', |
245 | + 'software-properties-common'), |
246 | + mock.call('sudo', self.add_repository, '-y', 'ppa:juju/stable'), |
247 | + mock.call('sudo', self.apt_get, 'update'), |
248 | + mock.call('sudo', self.apt_get, 'install', '-y', |
249 | + 'lxc', 'mongodb-server'), |
250 | ]) |
251 | mock_print.assert_has_calls([ |
252 | - mock.call('The following package needs to be installed: lxc'), |
253 | - mock.call( |
254 | - 'sudo privileges are required for package installation.'), |
255 | + mock.call('sudo privileges are required for PPA installation'), |
256 | + mock.call('installing the following packages: ' |
257 | + 'lxc, mongodb-server (this can take a while)'), |
258 | + mock.call('sudo privileges will be used for package installation'), |
259 | ]) |
260 | |
261 | - def test_failure(self, mock_print): |
262 | - with self.assert_program_exit('install failure'): |
263 | - call = self.call_ensure_dependencies( |
264 | - [ |
265 | - (127, '', ''), |
266 | - (127, '', ''), |
267 | - (1, 'add repo', 'install failure'), |
268 | - (1, 'update', 'uh-oh'), |
269 | - (1, 'install', 'oh no'), |
270 | - ] |
271 | - ) |
272 | - self.assertEqual(call.call_count, 3) |
273 | - |
274 | - |
275 | -@mock_print |
276 | + def test_add_repository_failure(self, mock_print): |
277 | + # A ProgramExit is raised if the PPA is not successfully installed. |
278 | + side_effects = ( |
279 | + (127, '', 'no juju'), # Check the juju command. |
280 | + (127, '', 'no lxc'), # Check the lxc-ls command. |
281 | + (0, 'saucy', ''), # Retrieve the Ubuntu release codename. |
282 | + (0, 'install add repo', ''), # Install add-apt-repository. |
283 | + (1, '', 'add repo error'), # Add the juju stable repository. |
284 | + ) |
285 | + with self.assert_program_exit('add repo error'): |
286 | + mock_call = self.call_ensure_dependencies(side_effects) |
287 | + self.assertEqual(3, mock_call.call_count) |
288 | + |
289 | + def test_install_failure(self, mock_print): |
290 | + # A ProgramExit is raised if the packages installation fails. |
291 | + side_effects = ( |
292 | + (127, '', 'no juju'), # Check the juju command. |
293 | + (127, '', 'no lxc'), # Check the lxc-ls command. |
294 | + (0, 'saucy', ''), # Retrieve the Ubuntu release codename. |
295 | + (0, 'install add repo', ''), # Install add-apt-repository. |
296 | + (0, 'add repo', ''), # Add the juju stable repository. |
297 | + (0, 'update', ''), # Update the repository with new sources. |
298 | + (1, '', 'install error'), # Install missing packages. |
299 | + ) |
300 | + with self.assert_program_exit('install error'): |
301 | + mock_call = self.call_ensure_dependencies(side_effects) |
302 | + self.assertEqual(3, mock_call.call_count) |
303 | + |
304 | + |
305 | +@helpers.mock_print |
306 | class TestBootstrap( |
307 | helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase): |
308 | |
309 | env_name = 'my-juju-env' |
310 | + juju = '/usr/bin/juju' |
311 | status_message = 'retrieving the environment status' |
312 | |
313 | def make_status_output(self, agent_state): |
314 | @@ -160,7 +191,7 @@ |
315 | def make_status_calls(self, number): |
316 | """Return a list containing the given number of status calls.""" |
317 | call = mock.call( |
318 | - 'juju', 'status', '-e', self.env_name, '--format', 'yaml') |
319 | + self.juju, 'status', '-e', self.env_name, '--format', 'yaml') |
320 | return [call for _ in range(number)] |
321 | |
322 | def make_side_effects(self): |
323 | @@ -178,7 +209,7 @@ |
324 | with self.patch_multiple_calls(side_effects) as mock_call: |
325 | app.bootstrap(self.env_name) |
326 | mock_call.assert_has_calls([ |
327 | - mock.call('juju', 'bootstrap', '-e', self.env_name), |
328 | + mock.call(self.juju, 'bootstrap', '-e', self.env_name), |
329 | ] + self.make_status_calls(5)) |
330 | |
331 | def test_success(self, mock_print): |
332 | @@ -187,7 +218,7 @@ |
333 | already_bootstrapped = app.bootstrap(self.env_name) |
334 | self.assertFalse(already_bootstrapped) |
335 | mock_call.assert_has_calls([ |
336 | - mock.call('juju', 'bootstrap', '-e', self.env_name), |
337 | + mock.call(self.juju, 'bootstrap', '-e', self.env_name), |
338 | ] + self.make_status_calls(1)) |
339 | mock_print.assert_called_once_with(self.status_message) |
340 | |
341 | @@ -197,7 +228,7 @@ |
342 | already_bootstrapped = app.bootstrap(self.env_name, is_local=True) |
343 | self.assertFalse(already_bootstrapped) |
344 | mock_call.assert_has_calls([ |
345 | - mock.call('sudo', 'juju', 'bootstrap', '-e', self.env_name), |
346 | + mock.call('sudo', self.juju, 'bootstrap', '-e', self.env_name), |
347 | ] + self.make_status_calls(1)) |
348 | mock_print.assert_has_calls([ |
349 | mock.call('sudo privileges required to bootstrap the environment'), |
350 | @@ -210,7 +241,7 @@ |
351 | already_bootstrapped = app.bootstrap(self.env_name, debug=True) |
352 | self.assertFalse(already_bootstrapped) |
353 | mock_call.assert_has_calls([ |
354 | - mock.call('juju', 'bootstrap', '-e', self.env_name, '--debug'), |
355 | + mock.call(self.juju, 'bootstrap', '-e', self.env_name, '--debug'), |
356 | ] + self.make_status_calls(1)) |
357 | |
358 | def test_already_bootstrapped(self, mock_print): |
359 | @@ -224,7 +255,7 @@ |
360 | already_bootstrapped = app.bootstrap(self.env_name) |
361 | self.assertTrue(already_bootstrapped) |
362 | mock_call.assert_has_calls([ |
363 | - mock.call('juju', 'bootstrap', '-e', self.env_name), |
364 | + mock.call(self.juju, 'bootstrap', '-e', self.env_name), |
365 | ] + self.make_status_calls(1)) |
366 | existing_message = 'reusing the already bootstrapped {} environment' |
367 | mock_print.assert_has_calls([ |
368 | @@ -238,7 +269,7 @@ |
369 | with self.assert_program_exit('bad wolf'): |
370 | app.bootstrap(self.env_name) |
371 | mock_call.assert_called_once_with( |
372 | - 'juju', 'bootstrap', '-e', self.env_name) |
373 | + self.juju, 'bootstrap', '-e', self.env_name) |
374 | |
375 | def test_status_retry_error(self, mock_print): |
376 | # Before raising a ProgramExit, the functions tries to call |
377 | @@ -299,7 +330,7 @@ |
378 | with self.assert_program_exit(expected): |
379 | app.bootstrap(self.env_name) |
380 | mock_call.assert_has_calls([ |
381 | - mock.call('juju', 'bootstrap', '-e', self.env_name), |
382 | + mock.call(self.juju, 'bootstrap', '-e', self.env_name), |
383 | ] + self.make_status_calls(1)) |
384 | |
385 | def test_status_failure(self, mock_print): |
386 | @@ -325,7 +356,7 @@ |
387 | with self.assert_program_exit(expected): |
388 | app.bootstrap(self.env_name) |
389 | mock_call.assert_has_calls([ |
390 | - mock.call('juju', 'bootstrap', '-e', self.env_name), |
391 | + mock.call(self.juju, 'bootstrap', '-e', self.env_name), |
392 | ] + self.make_status_calls(2)) |
393 | |
394 | |
395 | @@ -341,7 +372,8 @@ |
396 | api_url = app.get_api_url(self.env_name) |
397 | self.assertEqual('wss://api.example.com:17070', api_url) |
398 | mock_call.assert_called_once_with( |
399 | - 'juju', 'api-endpoints', '-e', self.env_name, '--format', 'json') |
400 | + '/usr/bin/juju', 'api-endpoints', '-e', self.env_name, |
401 | + '--format', 'json') |
402 | |
403 | def test_failure(self): |
404 | # A ProgramExit is raised if an error occurs retrieving the API URL. |
405 | @@ -349,7 +381,8 @@ |
406 | with self.assert_program_exit('bad wolf'): |
407 | app.get_api_url(self.env_name) |
408 | mock_call.assert_called_once_with( |
409 | - 'juju', 'api-endpoints', '-e', self.env_name, '--format', 'json') |
410 | + '/usr/bin/juju', 'api-endpoints', '-e', self.env_name, |
411 | + '--format', 'json') |
412 | |
413 | |
414 | class TestConnect(ProgramExitTestsMixin, unittest.TestCase): |
415 | @@ -397,7 +430,7 @@ |
416 | self.assertIs(error, context_manager.exception) |
417 | |
418 | |
419 | -@mock_print |
420 | +@helpers.mock_print |
421 | class TestDeployGui( |
422 | ProgramExitTestsMixin, helpers.WatcherDataTestsMixin, |
423 | unittest.TestCase): |
424 | @@ -706,7 +739,7 @@ |
425 | self.assertIs(error, context_manager.exception) |
426 | |
427 | |
428 | -@mock_print |
429 | +@helpers.mock_print |
430 | class TestWatch(ProgramExitTestsMixin, unittest.TestCase): |
431 | |
432 | address = 'unit.example.com' |
433 | |
434 | === modified file 'quickstart/tests/test_manage.py' |
435 | --- quickstart/tests/test_manage.py 2013-11-22 15:07:02 +0000 |
436 | +++ quickstart/tests/test_manage.py 2013-11-26 14:39:39 +0000 |
437 | @@ -42,7 +42,7 @@ |
438 | '--test', action=manage._DescriptionAction, nargs=0) |
439 | |
440 | @mock.patch('sys.exit') |
441 | - @mock.patch('__builtin__.print') |
442 | + @helpers.mock_print |
443 | def test_action(self, mock_print, mock_exit): |
444 | # The action just prints the description and exits. |
445 | args = self.parser.parse_args(['--test']) |
446 | @@ -359,7 +359,7 @@ |
447 | |
448 | def test_description(self): |
449 | # The program description is properly printed out as required by juju. |
450 | - with mock.patch('__builtin__.print') as mock_print: |
451 | + with helpers.mock_print as mock_print: |
452 | self.call_setup(['--description']) |
453 | mock_print.assert_called_once_with(settings.DESCRIPTION) |
454 | |
455 | |
456 | === modified file 'quickstart/tests/test_utils.py' |
457 | --- quickstart/tests/test_utils.py 2013-11-22 09:55:35 +0000 |
458 | +++ quickstart/tests/test_utils.py 2013-11-26 14:39:39 +0000 |
459 | @@ -32,6 +32,78 @@ |
460 | from quickstart.tests import helpers |
461 | |
462 | |
463 | +@helpers.mock_print |
464 | +class TestAddAptRepository(helpers.CallTestsMixin, unittest.TestCase): |
465 | + |
466 | + apt_add_repository = '/usr/bin/add-apt-repository' |
467 | + apt_get = '/usr/bin/apt-get' |
468 | + repository = 'ppa:good/stuff' |
469 | + side_effects = ( |
470 | + (0, 'apt-get install', ''), # Install add-apt-repository. |
471 | + (0, 'add-apt-repository', ''), # Add the repository. |
472 | + (0, 'update', ''), # Update the global repository |
473 | + ) |
474 | + |
475 | + def patch_codename(self, codename): |
476 | + """Patch the Ubuntu codename returned by get_ubuntu_codename.""" |
477 | + return mock.patch( |
478 | + 'quickstart.utils.get_ubuntu_codename', |
479 | + mock.Mock(return_value=codename)) |
480 | + |
481 | + def test_precise(self, mock_print): |
482 | + # The repository is properly added in precise. |
483 | + with self.patch_codename('precise') as mock_get_ubuntu_codename: |
484 | + with self.patch_multiple_calls(self.side_effects) as mock_call: |
485 | + utils.add_apt_repository(self.repository) |
486 | + mock_get_ubuntu_codename.assert_called_once_with() |
487 | + self.assertEqual(len(self.side_effects), mock_call.call_count) |
488 | + mock_call.assert_has_calls([ |
489 | + mock.call('sudo', self.apt_get, 'install', '-y', |
490 | + 'python-software-properties'), |
491 | + mock.call('sudo', self.apt_add_repository, '-y', self.repository), |
492 | + mock.call('sudo', self.apt_get, 'update'), |
493 | + ]) |
494 | + |
495 | + def test_after_precise(self, mock_print): |
496 | + # The repository is correctly added in newer releases. |
497 | + with self.patch_codename('trusty') as mock_get_ubuntu_codename: |
498 | + with self.patch_multiple_calls(self.side_effects) as mock_call: |
499 | + utils.add_apt_repository(self.repository) |
500 | + mock_get_ubuntu_codename.assert_called_once_with() |
501 | + self.assertEqual(len(self.side_effects), mock_call.call_count) |
502 | + mock_call.assert_has_calls([ |
503 | + mock.call('sudo', self.apt_get, 'install', '-y', |
504 | + 'software-properties-common'), |
505 | + mock.call('sudo', self.apt_add_repository, '-y', self.repository), |
506 | + mock.call('sudo', self.apt_get, 'update'), |
507 | + ]) |
508 | + |
509 | + def test_output(self, mock_print): |
510 | + # The user is properly informed about the process. |
511 | + with self.patch_codename('raring'): |
512 | + with self.patch_multiple_calls(self.side_effects): |
513 | + utils.add_apt_repository(self.repository) |
514 | + self.assertEqual(2, mock_print.call_count) |
515 | + mock_print.assert_has_calls([ |
516 | + mock.call('adding the {} PPA repository'.format(self.repository)), |
517 | + mock.call('sudo privileges are required for PPA installation'), |
518 | + ]) |
519 | + |
520 | + def test_command_error(self, mock_print): |
521 | + # An OSError is raised if a command error occurs. |
522 | + side_effects = [(1, '', 'apt-get install error')] |
523 | + with self.patch_codename('quantal') as mock_get_ubuntu_codename: |
524 | + with self.patch_multiple_calls(side_effects) as mock_call: |
525 | + with self.assertRaises(OSError) as context_manager: |
526 | + utils.add_apt_repository(self.repository) |
527 | + mock_get_ubuntu_codename.assert_called_once_with() |
528 | + mock_call.assert_called_once_with( |
529 | + 'sudo', self.apt_get, 'install', '-y', |
530 | + 'software-properties-common') |
531 | + self.assertEqual( |
532 | + 'apt-get install error', str(context_manager.exception)) |
533 | + |
534 | + |
535 | class TestCall(unittest.TestCase): |
536 | |
537 | def test_success(self): |
538 | @@ -285,6 +357,24 @@ |
539 | self.assertEqual(expected, utils.get_service_info([], 'my-gui')) |
540 | |
541 | |
542 | +class TestGetUbuntuCodename(helpers.CallTestsMixin, unittest.TestCase): |
543 | + |
544 | + def test_codename(self): |
545 | + # The distribution codename is correctly returned. |
546 | + with self.patch_call(0, output='trusty\n') as mock_call: |
547 | + codename = utils.get_ubuntu_codename() |
548 | + self.assertEqual('trusty', codename) |
549 | + mock_call.assert_called_once_with('lsb_release', '-cs') |
550 | + |
551 | + def test_error_retrieving_codename(self): |
552 | + # An OSError is returned if the codename cannot be retrieved. |
553 | + with self.patch_call(1, error='bad wolf') as mock_call: |
554 | + with self.assertRaises(OSError) as context_manager: |
555 | + utils.get_ubuntu_codename() |
556 | + self.assertEqual('bad wolf', str(context_manager.exception)) |
557 | + mock_call.assert_called_once_with('lsb_release', '-cs') |
558 | + |
559 | + |
560 | class TestParseBundle( |
561 | helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin, |
562 | unittest.TestCase): |
563 | |
564 | === modified file 'quickstart/utils.py' |
565 | --- quickstart/utils.py 2013-11-22 08:32:44 +0000 |
566 | +++ quickstart/utils.py 2013-11-26 14:39:39 +0000 |
567 | @@ -39,6 +39,32 @@ |
568 | _juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n') |
569 | |
570 | |
571 | +def add_apt_repository(repository): |
572 | + """Add the given APT repository to the current list of APT sources. |
573 | + |
574 | + Also take care of installing the add-apt-repository script and of updating |
575 | + the list of APT packages after the repository installation. |
576 | + |
577 | + Raise an OSError if any error occur in the process. |
578 | + """ |
579 | + print('adding the {} PPA repository'.format(repository)) |
580 | + print('sudo privileges are required for PPA installation') |
581 | + # The package including add-apt-repository is python-software-properties |
582 | + # in precise and software-properties-common after precise. |
583 | + add_repository_package = 'software-properties-common' |
584 | + if get_ubuntu_codename() == 'precise': |
585 | + add_repository_package = 'python-software-properties' |
586 | + commands = ( |
587 | + ('/usr/bin/apt-get', 'install', '-y', add_repository_package), |
588 | + ('/usr/bin/add-apt-repository', '-y', repository), |
589 | + ('/usr/bin/apt-get', 'update'), |
590 | + ) |
591 | + for command in commands: |
592 | + retcode, _, error = call('sudo', *command) |
593 | + if retcode: |
594 | + raise OSError(error) |
595 | + |
596 | + |
597 | def call(command, *args): |
598 | """Call a subprocess passing the given arguments. |
599 | |
600 | @@ -162,6 +188,17 @@ |
601 | return services[0], units[0] if units else None |
602 | |
603 | |
604 | +def get_ubuntu_codename(): |
605 | + """Return the codename of the current Ubuntu release (e.g. "trusty"). |
606 | + |
607 | + Raise an OSError if an error occurs retrieving the codename. |
608 | + """ |
609 | + retcode, output, error = call('lsb_release', '-cs') |
610 | + if retcode: |
611 | + raise OSError(error) |
612 | + return output.strip() |
613 | + |
614 | + |
615 | def parse_bundle(bundle_yaml, bundle_name=None): |
616 | """Parse the provided bundle YAML encoded contents. |
617 |
Reviewers: mp+196692_ code.launchpad. net,
Message:
Please take a look.
Description:
Install missing packages for add-apt-repository.
Also use absolute paths to commands executed
with sudo privileges.
Tests: `make check`
QA:
1) Create a saucy LXC sharing your home directory,
lxc.network. type=veth
lxc.network. link=lxcbr0
lxc.network. flags=up
e.g. `sudo lxc-create -t ubuntu -n quickstart -f <MY-TEMPLATE> \
-- -r saucy -a amd64 -b $USER`
where "quickstart" is the name of the container,
"-r" is used to specify the release to use,
"-b" binds the home directory of the specified user,
and <MY-TEMPLATE> is a file with the following contents:
I assume you already have:
- a juju home containing the environments.yaml file
configured with an "ec2" ec2 environment;
- your ssh keys properly set up;
- run the tests with `make check` as described above.
So at this point the container does not have juju
installed, but the juju home and ssh keys are
available, and so the branch with a configured testing
virtualenv. We already have cards for environment
creation and ssh keys handling.
2) Start the LXC instance (`sudo lxc-start -n quickstart`).
3) Open a console inside the LXC with
`sudo lxc-console -n quickstart`, log in using your user
credentials, and cd into the directory where you checked
out this branch.
4) Run `.venv/bin/python juju-quickstart -e ec2 --no-browser`.
You should be asked the sudo password in order to add
the missing PPA and install juju-core and lxc.
Note that installing the packages can take some minutes.
The process will then proceed as usual.
5) Run `.venv/bin/python juju-quickstart -e ec2 --no-browser`
again: this time no packages installation should be required,
and quickstart just reuses the existing environment.
6) From the host, stop and destroy the LXC container:
`sudo lxc-stop -n quickstart` and `sudo lxc-destroy -n quickstart`.
7) Destroy your ec2 environment.
Thank you!
https:/ /code.launchpad .net/~frankban/ juju-quickstart /sudo-fixes/ +merge/ 196692
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/32760043/
Affected files (+255, -94 lines): __init_ _.py tests/helpers. py tests/test_ app.py tests/test_ manage. py tests/test_ utils.py
A [revision details]
M quickstart/
M quickstart/app.py
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/utils.py