Merge lp:~frankban/juju-quickstart/local-provider into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 18
Proposed branch: lp:~frankban/juju-quickstart/local-provider
Merge into: lp:juju-quickstart
Diff against target: 369 lines (+131/-39)
6 files modified
HACKING.rst (+19/-3)
quickstart/app.py (+24/-2)
quickstart/manage.py (+3/-7)
quickstart/tests/test_app.py (+68/-20)
quickstart/tests/test_manage.py (+12/-3)
quickstart/utils.py (+5/-4)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/local-provider
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+195952@code.launchpad.net

Description of the change

Add support for local providers.

Bootstrap the environment with "sudo" if
the environment is configured to use the
local provider.

Also improved debugging documentation.

Tests: make check

To QA this, just run `.venv/bin/python juju-quickstart`
as you are used to, but with the local provider:
try to deploy a bundle, try to re-run quickstart again
with the environment already bootstrapped.
In general the application should ask for sudo password
and then proceed as usual.

https://codereview.appspot.com/29540044/

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

Reviewers: mp+195952_code.launchpad.net,

Message:
Please take a look.

Description:
Add support for local providers.

Bootstrap the environment with "sudo" if
the environment is configured to use the
local provider.

Also improved debugging documentation.

Tests: make check

To QA this, just run `.venv/bin/python juju-quickstart`
as you are used to, but with the local provider:
try to deploy a bundle, try to re-run quickstart again
with the environment already bootstrapped.
In general the application should ask for sudo password
and then proceed as usual.

https://code.launchpad.net/~frankban/juju-quickstart/local-provider/+merge/195952

(do not edit description out of merge proposal)

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

Affected files (+133, -39 lines):
   M HACKING.rst
   A [revision details]
   M quickstart/app.py
   M quickstart/manage.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_manage.py
   M quickstart/utils.py

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

Code looks good, doing qa.

https://codereview.appspot.com/29540044/

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

*** Submitted:

Add support for local providers.

Bootstrap the environment with "sudo" if
the environment is configured to use the
local provider.

Also improved debugging documentation.

Tests: make check

To QA this, just run `.venv/bin/python juju-quickstart`
as you are used to, but with the local provider:
try to deploy a bundle, try to re-run quickstart again
with the environment already bootstrapped.
In general the application should ask for sudo password
and then proceed as usual.

R=rharding
CC=
https://codereview.appspot.com/29540044

https://codereview.appspot.com/29540044/

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'HACKING.rst'
--- HACKING.rst 2013-11-18 18:01:22 +0000
+++ HACKING.rst 2013-11-20 12:26:05 +0000
@@ -134,11 +134,27 @@
134the deployment process using the GUI.134the deployment process using the GUI.
135135
136Under the hood, a bundle deployment is executed by the GUI builtin server,136Under the hood, a bundle deployment is executed by the GUI builtin server,
137which in turn leverages the juju-deployer library. Sometimes, when an error137which in turn leverages the juju-deployer library. Since juju-deployer is not
138occurs, it is not obvious where to retrieve information about what is going on.138asynchronous, the actual deployment is executed in a separate process.
139The GUI builtin server exposes some bundle information in two places:139
140Sometimes, when an error occurs, it is not obvious where to retrieve
141information about what is going on. The GUI builtin server exposes some bundle
142information in two places:
140143
141- https://<juju-gui-url>/gui-server-info displays in JSON format the current144- https://<juju-gui-url>/gui-server-info displays in JSON format the current
142 status of all scheduled/started/completed bundle deployments;145 status of all scheduled/started/completed bundle deployments;
143- /var/log/upstart/guiserver.log is the builtin server log file, which includes146- /var/log/upstart/guiserver.log is the builtin server log file, which includes
144 logs output from the juju-deployer library.147 logs output from the juju-deployer library.
148
149Moreover, setting `builtin-server-logging=debug` gives more debugging
150information, e.g. it prints to the log the contents of the WebSocket messages
151sent by the client (usually the Juju GUI) and by the Juju API server.
152As mentioned, juju-deployer works on its own sandbox and uses its own API
153connections, and for this reason the WebSocket traffic it generates is not
154logged.
155
156Sometimes, while debugging, it is convenient to restart the builtin server
157(which also empties the bundle deployments queue). To do that, run the
158following in the Juju GUI machine:
159
160 service guiserver restart
145161
=== modified file 'quickstart/app.py'
--- quickstart/app.py 2013-11-18 20:34:09 +0000
+++ quickstart/app.py 2013-11-20 12:26:05 +0000
@@ -20,6 +20,7 @@
20import functools20import functools
21import json21import json
22import logging22import logging
23import time
2324
24import jujuclient25import jujuclient
2526
@@ -44,7 +45,7 @@
44 return 'juju-quickstart: error: {}'.format(self.message)45 return 'juju-quickstart: error: {}'.format(self.message)
4546
4647
47def bootstrap(env_name, debug=False):48def bootstrap(env_name, is_local=False, debug=False):
48 """Bootstrap the Juju environment with the given name.49 """Bootstrap the Juju environment with the given name.
4950
50 Do not bootstrap the environment if already bootstrapped.51 Do not bootstrap the environment if already bootstrapped.
@@ -52,6 +53,10 @@
52 Return True without errors if the environment is already bootstrapped.53 Return True without errors if the environment is already bootstrapped.
53 Return False otherwise. Only return when the bootstrap node is ready.54 Return False otherwise. Only return when the bootstrap node is ready.
5455
56 The is_local argument indicates whether the environment is configured to
57 use the local provider. If so, sudo privileges are requested in order to
58 bootstrap the environment.
59
55 If debug is True and the environment not bootstrapped, execute the60 If debug is True and the environment not bootstrapped, execute the
56 bootstrap command passing the --debug flag.61 bootstrap command passing the --debug flag.
5762
@@ -59,6 +64,11 @@
59 """64 """
60 already_bootstrapped = False65 already_bootstrapped = False
61 cmd = ['juju', 'bootstrap', '-e', env_name]66 cmd = ['juju', 'bootstrap', '-e', env_name]
67 # XXX frankban 2013-11-20: avoid using sudo once this is no longer required
68 # in juju-core.
69 if is_local:
70 print('sudo privileges required to bootstrap the environment')
71 cmd.insert(0, 'sudo')
62 if debug:72 if debug:
63 cmd.append('--debug')73 cmd.append('--debug')
64 retcode, _, error = utils.call(*cmd)74 retcode, _, error = utils.call(*cmd)
@@ -82,8 +92,16 @@
82 # Juju is bootstrapped, but we don't know if it is ready yet. Fall92 # Juju is bootstrapped, but we don't know if it is ready yet. Fall
83 # through to the next block for that check.93 # through to the next block for that check.
84 already_bootstrapped = True94 already_bootstrapped = True
95 print('reusing the already bootstrapped {} environment'.format(
96 env_name))
85 # Call "juju status" multiple times until the bootstrap node is ready.97 # Call "juju status" multiple times until the bootstrap node is ready.
86 for _ in range(5):98 # Exit with an error if the agent is not ready after ten minutes.
99 # Note: when using the local provider, calling "juju status" is very fast,
100 # but e.g. on ec2 the first call (right after "bootstrap") can take
101 # several minutes, and subsequent calls are relatively fast (seconds).
102 print('retrieving the environment status')
103 timeout = time.time() + (60*10)
104 while time.time() < timeout:
87 retcode, output, error = utils.call(105 retcode, output, error = utils.call(
88 'juju', 'status', '-e', env_name, '--format', 'yaml')106 'juju', 'status', '-e', env_name, '--format', 'yaml')
89 if retcode:107 if retcode:
@@ -95,6 +113,10 @@
95 continue113 continue
96 if agent_state == 'started':114 if agent_state == 'started':
97 return already_bootstrapped115 return already_bootstrapped
116 # If the agent is in an error state, there is nothing we can do, and
117 # it's not useful to keep trying.
118 if agent_state == 'error':
119 raise ProgramExit('state server failure:\n{}'.format(output))
98 details = ''.join(filter(None, [output, error])).strip()120 details = ''.join(filter(None, [output, error])).strip()
99 raise ProgramExit('the state server is not ready:\n{}'.format(details))121 raise ProgramExit('the state server is not ready:\n{}'.format(details))
100122
101123
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2013-11-19 12:22:32 +0000
+++ quickstart/manage.py 2013-11-20 12:26:05 +0000
@@ -110,9 +110,6 @@
110 env_type, admin_secret = utils.parse_env_file(env_file, env_name)110 env_type, admin_secret = utils.parse_env_file(env_file, env_name)
111 except ValueError as err:111 except ValueError as err:
112 return parser.error(str(err))112 return parser.error(str(err))
113 # XXX 2013-10-15 frankban: add support for local providers.
114 if env_type == 'local':
115 return parser.error('the local provider is not currently supported')
116 # Update the options namespace with the new values.113 # Update the options namespace with the new values.
117 options.admin_secret = admin_secret114 options.admin_secret = admin_secret
118 options.env_file = env_file115 options.env_file = env_file
@@ -223,10 +220,9 @@
223 print('juju quickstart v{}'.format(version))220 print('juju quickstart v{}'.format(version))
224 print('bootstrapping the {} environment (type: {})'.format(221 print('bootstrapping the {} environment (type: {})'.format(
225 options.env_name, options.env_type))222 options.env_name, options.env_type))
226 already_bootstrapped = app.bootstrap(options.env_name, debug=options.debug)223 is_local = options.env_type == 'local'
227 if already_bootstrapped:224 already_bootstrapped = app.bootstrap(
228 print('reusing the already bootstrapped {} environment'.format(225 options.env_name, is_local=is_local, debug=options.debug)
229 options.env_name))
230226
231 print('retrieving the Juju API address')227 print('retrieving the Juju API address')
232 api_url = app.get_api_url(options.env_name)228 api_url = app.get_api_url(options.env_name)
233229
=== modified file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 2013-11-18 20:34:09 +0000
+++ quickstart/tests/test_app.py 2013-11-20 12:26:05 +0000
@@ -61,10 +61,12 @@
61 return jujuclient.EnvError({'Error': message})61 return jujuclient.EnvError({'Error': message})
6262
6363
64@mock_print
64class TestBootstrap(65class TestBootstrap(
65 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):66 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
6667
67 env_name = 'ec2'68 env_name = 'my-juju-env'
69 status_message = 'retrieving the environment status'
6870
69 def make_status_output(self, agent_state):71 def make_status_output(self, agent_state):
70 """Create and return a YAML status output."""72 """Create and return a YAML status output."""
@@ -96,7 +98,7 @@
96 mock.call('juju', 'bootstrap', '-e', self.env_name),98 mock.call('juju', 'bootstrap', '-e', self.env_name),
97 ] + self.make_status_calls(5))99 ] + self.make_status_calls(5))
98100
99 def test_success(self):101 def test_success(self, mock_print):
100 # The environment is successfully bootstrapped.102 # The environment is successfully bootstrapped.
101 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:103 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:
102 already_bootstrapped = app.bootstrap(self.env_name)104 already_bootstrapped = app.bootstrap(self.env_name)
@@ -104,8 +106,22 @@
104 mock_call.assert_has_calls([106 mock_call.assert_has_calls([
105 mock.call('juju', 'bootstrap', '-e', self.env_name),107 mock.call('juju', 'bootstrap', '-e', self.env_name),
106 ] + self.make_status_calls(1))108 ] + self.make_status_calls(1))
107109 mock_print.assert_called_once_with(self.status_message)
108 def test_success_debug(self):110
111 def test_success_local_provider(self, mock_print):
112 # The environment is bootstrapped with sudo using the local provider.
113 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:
114 already_bootstrapped = app.bootstrap(self.env_name, is_local=True)
115 self.assertFalse(already_bootstrapped)
116 mock_call.assert_has_calls([
117 mock.call('sudo', 'juju', 'bootstrap', '-e', self.env_name),
118 ] + self.make_status_calls(1))
119 mock_print.assert_has_calls([
120 mock.call('sudo privileges required to bootstrap the environment'),
121 mock.call(self.status_message),
122 ])
123
124 def test_success_debug(self, mock_print):
109 # The environment is successfully bootstrapped in debug mode.125 # The environment is successfully bootstrapped in debug mode.
110 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:126 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:
111 already_bootstrapped = app.bootstrap(self.env_name, debug=True)127 already_bootstrapped = app.bootstrap(self.env_name, debug=True)
@@ -114,7 +130,7 @@
114 mock.call('juju', 'bootstrap', '-e', self.env_name, '--debug'),130 mock.call('juju', 'bootstrap', '-e', self.env_name, '--debug'),
115 ] + self.make_status_calls(1))131 ] + self.make_status_calls(1))
116132
117 def test_already_bootstrapped(self):133 def test_already_bootstrapped(self, mock_print):
118 # The function succeeds and returns True if the environment is already134 # The function succeeds and returns True if the environment is already
119 # bootstrapped.135 # bootstrapped.
120 side_effects = [136 side_effects = [
@@ -127,8 +143,13 @@
127 mock_call.assert_has_calls([143 mock_call.assert_has_calls([
128 mock.call('juju', 'bootstrap', '-e', self.env_name),144 mock.call('juju', 'bootstrap', '-e', self.env_name),
129 ] + self.make_status_calls(1))145 ] + self.make_status_calls(1))
146 existing_message = 'reusing the already bootstrapped {} environment'
147 mock_print.assert_has_calls([
148 mock.call(existing_message.format(self.env_name)),
149 mock.call(self.status_message),
150 ])
130151
131 def test_bootstrap_failure(self):152 def test_bootstrap_failure(self, mock_print):
132 # A ProgramExit is raised if an error occurs while bootstrapping.153 # A ProgramExit is raised if an error occurs while bootstrapping.
133 with self.patch_call(1, error='bad wolf') as mock_call:154 with self.patch_call(1, error='bad wolf') as mock_call:
134 with self.assert_program_exit('bad wolf'):155 with self.assert_program_exit('bad wolf'):
@@ -136,7 +157,7 @@
136 mock_call.assert_called_once_with(157 mock_call.assert_called_once_with(
137 'juju', 'bootstrap', '-e', self.env_name)158 'juju', 'bootstrap', '-e', self.env_name)
138159
139 def test_status_retry_error(self):160 def test_status_retry_error(self, mock_print):
140 # Before raising a ProgramExit, the functions tries to call161 # Before raising a ProgramExit, the functions tries to call
141 # "juju status" multiple times if it exits with an error.162 # "juju status" multiple times if it exits with an error.
142 side_effects = [163 side_effects = [
@@ -151,7 +172,7 @@
151 ]172 ]
152 self.assert_status_retried(side_effects)173 self.assert_status_retried(side_effects)
153174
154 def test_status_retry_invalid_output(self):175 def test_status_retry_invalid_output(self, mock_print):
155 # Before raising a ProgramExit, the functions tries to call176 # Before raising a ProgramExit, the functions tries to call
156 # "juju status" multiple times if its output is not well formed or if177 # "juju status" multiple times if its output is not well formed or if
157 # the agent is not started.178 # the agent is not started.
@@ -167,7 +188,7 @@
167 ]188 ]
168 self.assert_status_retried(side_effects)189 self.assert_status_retried(side_effects)
169190
170 def test_status_retry_both(self):191 def test_status_retry_both(self, mock_print):
171 # Before raising a ProgramExit, the functions tries to call192 # Before raising a ProgramExit, the functions tries to call
172 # "juju status" multiple times in any case.193 # "juju status" multiple times in any case.
173 side_effects = [194 side_effects = [
@@ -182,20 +203,47 @@
182 ]203 ]
183 self.assert_status_retried(side_effects)204 self.assert_status_retried(side_effects)
184205
185 def test_status_failure(self):206 def test_agent_error(self, mock_print):
207 # A ProgramExit is raised immediately if the Juju agent in the
208 # bootstrap node is in an error state.
209 status_output = self.make_status_output('error')
210 side_effects = [
211 (0, '', ''), # Add the bootstrap call.
212 (0, status_output, ''), # Add the status call: agent error.
213 ]
214 expected = 'state server failure:\n{}'.format(status_output)
215 with self.patch_multiple_calls(side_effects) as mock_call:
216 with self.assert_program_exit(expected):
217 app.bootstrap(self.env_name)
218 mock_call.assert_has_calls([
219 mock.call('juju', 'bootstrap', '-e', self.env_name),
220 ] + self.make_status_calls(1))
221
222 def test_status_failure(self, mock_print):
186 # A ProgramExit is raised if "juju status" keeps failing.223 # A ProgramExit is raised if "juju status" keeps failing.
187 status_side_effects = [224 call_side_effects = [
188 (i, 'output #{}\n'.format(i), 'error #{}\n'.format(i))225 (0, '', ''), # Add the bootstrap call.
189 for i in range(5)226 (1, 'output1', 'error1'), # Add the first status call: retried.
190 ]227 (1, 'output2', 'error2'), # Add the second status call: error.
191 side_effects = [(0, '', '')] + status_side_effects228 ]
192 expected = 'the state server is not ready:\noutput #4\nerror #4'229 time_side_effects = [
193 with self.patch_multiple_calls(side_effects) as mock_call:230 0, # Start at time zero (expiration at time 600).
194 with self.assert_program_exit(expected):231 10, # First call before the timeout expiration.
195 app.bootstrap(self.env_name)232 100, # Second call before the timeout expiration.
233 1000, # Third call after the timeout expiration.
234 ]
235 mock_time = mock.Mock(side_effect=time_side_effects)
236 expected = 'the state server is not ready:\noutput2error2'
237 with self.patch_multiple_calls(call_side_effects) as mock_call:
238 # Simulate the timeout expired: the first time call is used to
239 # calculate the timeout, the second one for the first status check,
240 # the third for the second status check, the fourth should fail.
241 with mock.patch('time.time', mock_time):
242 with self.assert_program_exit(expected):
243 app.bootstrap(self.env_name)
196 mock_call.assert_has_calls([244 mock_call.assert_has_calls([
197 mock.call('juju', 'bootstrap', '-e', self.env_name),245 mock.call('juju', 'bootstrap', '-e', self.env_name),
198 ] + self.make_status_calls(5))246 ] + self.make_status_calls(2))
199247
200248
201class TestGetApiUrl(249class TestGetApiUrl(
202250
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2013-11-19 12:22:32 +0000
+++ quickstart/tests/test_manage.py 2013-11-20 12:26:05 +0000
@@ -238,8 +238,10 @@
238 env_file = self.make_env_file(contents)238 env_file = self.make_env_file(contents)
239 options = self.make_options(env_file, env_name='lxc')239 options = self.make_options(env_file, env_name='lxc')
240 manage._validate_env(options, self.parser)240 manage._validate_env(options, self.parser)
241 expected = 'the local provider is not currently supported'241 self.assertEqual('Secret!', options.admin_secret)
242 self.parser.error.assert_called_once_with(expected)242 self.assertEqual(env_file, options.env_file)
243 self.assertEqual('lxc', options.env_name)
244 self.assertEqual('local', options.env_type)
243245
244246
245@mock.patch('quickstart.manage._validate_env', mock.Mock())247@mock.patch('quickstart.manage._validate_env', mock.Mock())
@@ -352,7 +354,7 @@
352 options = self.make_options()354 options = self.make_options()
353 manage.run(options)355 manage.run(options)
354 mock_app.bootstrap.assert_called_once_with(356 mock_app.bootstrap.assert_called_once_with(
355 options.env_name, debug=options.debug)357 options.env_name, is_local=False, debug=options.debug)
356 mock_app.get_api_url.assert_called_once_with(options.env_name)358 mock_app.get_api_url.assert_called_once_with(options.env_name)
357 mock_app.connect.assert_called_once_with(359 mock_app.connect.assert_called_once_with(
358 mock_app.get_api_url(), options.admin_secret)360 mock_app.get_api_url(), options.admin_secret)
@@ -377,6 +379,13 @@
377 'wss://gui.example.com:443/ws', options.admin_secret,379 'wss://gui.example.com:443/ws', options.admin_secret,
378 'mybundle: contents', 'mybundle', None)380 'mybundle: contents', 'mybundle', None)
379381
382 def test_local_provider(self, mock_app, mock_open):
383 # The application correctly handles working with local providers.
384 options = self.make_options(env_type='local')
385 manage.run(options)
386 mock_app.bootstrap.assert_called_once_with(
387 options.env_name, is_local=True, debug=options.debug)
388
380 def test_no_browser(self, mock_app, mock_open):389 def test_no_browser(self, mock_app, mock_open):
381 # It is possible to avoid opening the GUI in the browser.390 # It is possible to avoid opening the GUI in the browser.
382 options = self.make_options(open_browser=False)391 options = self.make_options(open_browser=False)
383392
=== modified file 'quickstart/utils.py'
--- quickstart/utils.py 2013-11-18 20:34:09 +0000
+++ quickstart/utils.py 2013-11-20 12:26:05 +0000
@@ -35,7 +35,7 @@
35_juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n')35_juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n')
3636
3737
38def call(*args):38def call(command, *args):
39 """Call a subprocess passing the given arguments.39 """Call a subprocess passing the given arguments.
4040
41 Take the subcommand and its parameters as args.41 Take the subcommand and its parameters as args.
@@ -43,14 +43,15 @@
43 Return a tuple containing the subprocess return code, output and error.43 Return a tuple containing the subprocess return code, output and error.
44 """44 """
45 pipe = subprocess.PIPE45 pipe = subprocess.PIPE
46 cmdline = ' '.join(map(pipes.quote, args))46 cmd = (command,) + args
47 cmdline = ' '.join(map(pipes.quote, cmd))
47 logging.debug('running the following: {}'.format(cmdline))48 logging.debug('running the following: {}'.format(cmdline))
48 try:49 try:
49 process = subprocess.Popen(args, stdout=pipe, stderr=pipe)50 process = subprocess.Popen(cmd, stdout=pipe, stderr=pipe)
50 except OSError as err:51 except OSError as err:
51 # A return code 127 is returned by the shell when the command is not52 # A return code 127 is returned by the shell when the command is not
52 # found in the PATH.53 # found in the PATH.
53 return 127, '', '{}: {}'.format(args[0], err)54 return 127, '', '{}: {}'.format(command, err)
54 output, error = process.communicate()55 output, error = process.communicate()
55 retcode = process.poll()56 retcode = process.poll()
56 logging.debug('retcode: {} | output: {!r} | error: {!r}'.format(57 logging.debug('retcode: {} | output: {!r} | error: {!r}'.format(

Subscribers

People subscribed via source and target branches