Merge lp:~frankban/juju-quickstart/sudo-fixes into lp:juju-quickstart

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

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:
        lxc.network.type=veth
        lxc.network.link=lxcbr0
        lxc.network.flags=up
    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://codereview.appspot.com/32760043/

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

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,
     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:
         lxc.network.type=veth
         lxc.network.link=lxcbr0
         lxc.network.flags=up
     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):
   A [revision details]
   M quickstart/__init__.py
   M quickstart/app.py
   M quickstart/tests/helpers.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_manage.py
   M quickstart/tests/test_utils.py
   M quickstart/utils.py

26. By Francesco Banconi

Fixes as per review.

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

Code LGTM. QA had one problem, which you've already fixed. QA on patch
was successful.

https://codereview.appspot.com/32760043/

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

LGTM with a question and suggestion.

https://codereview.appspot.com/32760043/diff/20001/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/32760043/diff/20001/quickstart/app.py#newcode63
quickstart/app.py:63: # The lxc package is required to create the LXC
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://codereview.appspot.com/32760043/diff/20001/quickstart/app.py#newcode166
quickstart/app.py:166: '/usr/bin/juju', 'api-endpoints', '-e', 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.

https://codereview.appspot.com/32760043/

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

On 2013/11/26 14:36:25, rharding wrote:
> LGTM with a question and suggestion.

> https://codereview.appspot.com/32760043/diff/20001/quickstart/app.py
> File quickstart/app.py (right):

https://codereview.appspot.com/32760043/diff/20001/quickstart/app.py#newcode63
> quickstart/app.py:63: # The lxc package is required to create the LXC
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://codereview.appspot.com/32760043/diff/20001/quickstart/app.py#newcode166
> quickstart/app.py:166: '/usr/bin/juju', 'api-endpoints', '-e',
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.

https://codereview.appspot.com/32760043/

Revision history for this message
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:
         lxc.network.type=veth
         lxc.network.link=lxcbr0
         lxc.network.flags=up
     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://codereview.appspot.com/32760043

https://codereview.appspot.com/32760043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches