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
1=== modified file 'HACKING.rst'
2--- HACKING.rst 2013-11-18 18:01:22 +0000
3+++ HACKING.rst 2013-11-20 12:26:05 +0000
4@@ -134,11 +134,27 @@
5 the deployment process using the GUI.
6
7 Under the hood, a bundle deployment is executed by the GUI builtin server,
8-which in turn leverages the juju-deployer library. Sometimes, when an error
9-occurs, it is not obvious where to retrieve information about what is going on.
10-The GUI builtin server exposes some bundle information in two places:
11+which in turn leverages the juju-deployer library. Since juju-deployer is not
12+asynchronous, the actual deployment is executed in a separate process.
13+
14+Sometimes, when an error occurs, it is not obvious where to retrieve
15+information about what is going on. The GUI builtin server exposes some bundle
16+information in two places:
17
18 - https://<juju-gui-url>/gui-server-info displays in JSON format the current
19 status of all scheduled/started/completed bundle deployments;
20 - /var/log/upstart/guiserver.log is the builtin server log file, which includes
21 logs output from the juju-deployer library.
22+
23+Moreover, setting `builtin-server-logging=debug` gives more debugging
24+information, e.g. it prints to the log the contents of the WebSocket messages
25+sent by the client (usually the Juju GUI) and by the Juju API server.
26+As mentioned, juju-deployer works on its own sandbox and uses its own API
27+connections, and for this reason the WebSocket traffic it generates is not
28+logged.
29+
30+Sometimes, while debugging, it is convenient to restart the builtin server
31+(which also empties the bundle deployments queue). To do that, run the
32+following in the Juju GUI machine:
33+
34+ service guiserver restart
35
36=== modified file 'quickstart/app.py'
37--- quickstart/app.py 2013-11-18 20:34:09 +0000
38+++ quickstart/app.py 2013-11-20 12:26:05 +0000
39@@ -20,6 +20,7 @@
40 import functools
41 import json
42 import logging
43+import time
44
45 import jujuclient
46
47@@ -44,7 +45,7 @@
48 return 'juju-quickstart: error: {}'.format(self.message)
49
50
51-def bootstrap(env_name, debug=False):
52+def bootstrap(env_name, is_local=False, debug=False):
53 """Bootstrap the Juju environment with the given name.
54
55 Do not bootstrap the environment if already bootstrapped.
56@@ -52,6 +53,10 @@
57 Return True without errors if the environment is already bootstrapped.
58 Return False otherwise. Only return when the bootstrap node is ready.
59
60+ The is_local argument indicates whether the environment is configured to
61+ use the local provider. If so, sudo privileges are requested in order to
62+ bootstrap the environment.
63+
64 If debug is True and the environment not bootstrapped, execute the
65 bootstrap command passing the --debug flag.
66
67@@ -59,6 +64,11 @@
68 """
69 already_bootstrapped = False
70 cmd = ['juju', 'bootstrap', '-e', env_name]
71+ # XXX frankban 2013-11-20: avoid using sudo once this is no longer required
72+ # in juju-core.
73+ if is_local:
74+ print('sudo privileges required to bootstrap the environment')
75+ cmd.insert(0, 'sudo')
76 if debug:
77 cmd.append('--debug')
78 retcode, _, error = utils.call(*cmd)
79@@ -82,8 +92,16 @@
80 # Juju is bootstrapped, but we don't know if it is ready yet. Fall
81 # through to the next block for that check.
82 already_bootstrapped = True
83+ print('reusing the already bootstrapped {} environment'.format(
84+ env_name))
85 # Call "juju status" multiple times until the bootstrap node is ready.
86- for _ in range(5):
87+ # Exit with an error if the agent is not ready after ten minutes.
88+ # Note: when using the local provider, calling "juju status" is very fast,
89+ # but e.g. on ec2 the first call (right after "bootstrap") can take
90+ # several minutes, and subsequent calls are relatively fast (seconds).
91+ print('retrieving the environment status')
92+ timeout = time.time() + (60*10)
93+ while time.time() < timeout:
94 retcode, output, error = utils.call(
95 'juju', 'status', '-e', env_name, '--format', 'yaml')
96 if retcode:
97@@ -95,6 +113,10 @@
98 continue
99 if agent_state == 'started':
100 return already_bootstrapped
101+ # If the agent is in an error state, there is nothing we can do, and
102+ # it's not useful to keep trying.
103+ if agent_state == 'error':
104+ raise ProgramExit('state server failure:\n{}'.format(output))
105 details = ''.join(filter(None, [output, error])).strip()
106 raise ProgramExit('the state server is not ready:\n{}'.format(details))
107
108
109=== modified file 'quickstart/manage.py'
110--- quickstart/manage.py 2013-11-19 12:22:32 +0000
111+++ quickstart/manage.py 2013-11-20 12:26:05 +0000
112@@ -110,9 +110,6 @@
113 env_type, admin_secret = utils.parse_env_file(env_file, env_name)
114 except ValueError as err:
115 return parser.error(str(err))
116- # XXX 2013-10-15 frankban: add support for local providers.
117- if env_type == 'local':
118- return parser.error('the local provider is not currently supported')
119 # Update the options namespace with the new values.
120 options.admin_secret = admin_secret
121 options.env_file = env_file
122@@ -223,10 +220,9 @@
123 print('juju quickstart v{}'.format(version))
124 print('bootstrapping the {} environment (type: {})'.format(
125 options.env_name, options.env_type))
126- already_bootstrapped = app.bootstrap(options.env_name, debug=options.debug)
127- if already_bootstrapped:
128- print('reusing the already bootstrapped {} environment'.format(
129- options.env_name))
130+ is_local = options.env_type == 'local'
131+ already_bootstrapped = app.bootstrap(
132+ options.env_name, is_local=is_local, debug=options.debug)
133
134 print('retrieving the Juju API address')
135 api_url = app.get_api_url(options.env_name)
136
137=== modified file 'quickstart/tests/test_app.py'
138--- quickstart/tests/test_app.py 2013-11-18 20:34:09 +0000
139+++ quickstart/tests/test_app.py 2013-11-20 12:26:05 +0000
140@@ -61,10 +61,12 @@
141 return jujuclient.EnvError({'Error': message})
142
143
144+@mock_print
145 class TestBootstrap(
146 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
147
148- env_name = 'ec2'
149+ env_name = 'my-juju-env'
150+ status_message = 'retrieving the environment status'
151
152 def make_status_output(self, agent_state):
153 """Create and return a YAML status output."""
154@@ -96,7 +98,7 @@
155 mock.call('juju', 'bootstrap', '-e', self.env_name),
156 ] + self.make_status_calls(5))
157
158- def test_success(self):
159+ def test_success(self, mock_print):
160 # The environment is successfully bootstrapped.
161 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:
162 already_bootstrapped = app.bootstrap(self.env_name)
163@@ -104,8 +106,22 @@
164 mock_call.assert_has_calls([
165 mock.call('juju', 'bootstrap', '-e', self.env_name),
166 ] + self.make_status_calls(1))
167-
168- def test_success_debug(self):
169+ mock_print.assert_called_once_with(self.status_message)
170+
171+ def test_success_local_provider(self, mock_print):
172+ # The environment is bootstrapped with sudo using the local provider.
173+ with self.patch_multiple_calls(self.make_side_effects()) as mock_call:
174+ already_bootstrapped = app.bootstrap(self.env_name, is_local=True)
175+ self.assertFalse(already_bootstrapped)
176+ mock_call.assert_has_calls([
177+ mock.call('sudo', 'juju', 'bootstrap', '-e', self.env_name),
178+ ] + self.make_status_calls(1))
179+ mock_print.assert_has_calls([
180+ mock.call('sudo privileges required to bootstrap the environment'),
181+ mock.call(self.status_message),
182+ ])
183+
184+ def test_success_debug(self, mock_print):
185 # The environment is successfully bootstrapped in debug mode.
186 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:
187 already_bootstrapped = app.bootstrap(self.env_name, debug=True)
188@@ -114,7 +130,7 @@
189 mock.call('juju', 'bootstrap', '-e', self.env_name, '--debug'),
190 ] + self.make_status_calls(1))
191
192- def test_already_bootstrapped(self):
193+ def test_already_bootstrapped(self, mock_print):
194 # The function succeeds and returns True if the environment is already
195 # bootstrapped.
196 side_effects = [
197@@ -127,8 +143,13 @@
198 mock_call.assert_has_calls([
199 mock.call('juju', 'bootstrap', '-e', self.env_name),
200 ] + self.make_status_calls(1))
201+ existing_message = 'reusing the already bootstrapped {} environment'
202+ mock_print.assert_has_calls([
203+ mock.call(existing_message.format(self.env_name)),
204+ mock.call(self.status_message),
205+ ])
206
207- def test_bootstrap_failure(self):
208+ def test_bootstrap_failure(self, mock_print):
209 # A ProgramExit is raised if an error occurs while bootstrapping.
210 with self.patch_call(1, error='bad wolf') as mock_call:
211 with self.assert_program_exit('bad wolf'):
212@@ -136,7 +157,7 @@
213 mock_call.assert_called_once_with(
214 'juju', 'bootstrap', '-e', self.env_name)
215
216- def test_status_retry_error(self):
217+ def test_status_retry_error(self, mock_print):
218 # Before raising a ProgramExit, the functions tries to call
219 # "juju status" multiple times if it exits with an error.
220 side_effects = [
221@@ -151,7 +172,7 @@
222 ]
223 self.assert_status_retried(side_effects)
224
225- def test_status_retry_invalid_output(self):
226+ def test_status_retry_invalid_output(self, mock_print):
227 # Before raising a ProgramExit, the functions tries to call
228 # "juju status" multiple times if its output is not well formed or if
229 # the agent is not started.
230@@ -167,7 +188,7 @@
231 ]
232 self.assert_status_retried(side_effects)
233
234- def test_status_retry_both(self):
235+ def test_status_retry_both(self, mock_print):
236 # Before raising a ProgramExit, the functions tries to call
237 # "juju status" multiple times in any case.
238 side_effects = [
239@@ -182,20 +203,47 @@
240 ]
241 self.assert_status_retried(side_effects)
242
243- def test_status_failure(self):
244+ def test_agent_error(self, mock_print):
245+ # A ProgramExit is raised immediately if the Juju agent in the
246+ # bootstrap node is in an error state.
247+ status_output = self.make_status_output('error')
248+ side_effects = [
249+ (0, '', ''), # Add the bootstrap call.
250+ (0, status_output, ''), # Add the status call: agent error.
251+ ]
252+ expected = 'state server failure:\n{}'.format(status_output)
253+ with self.patch_multiple_calls(side_effects) as mock_call:
254+ with self.assert_program_exit(expected):
255+ app.bootstrap(self.env_name)
256+ mock_call.assert_has_calls([
257+ mock.call('juju', 'bootstrap', '-e', self.env_name),
258+ ] + self.make_status_calls(1))
259+
260+ def test_status_failure(self, mock_print):
261 # A ProgramExit is raised if "juju status" keeps failing.
262- status_side_effects = [
263- (i, 'output #{}\n'.format(i), 'error #{}\n'.format(i))
264- for i in range(5)
265- ]
266- side_effects = [(0, '', '')] + status_side_effects
267- expected = 'the state server is not ready:\noutput #4\nerror #4'
268- with self.patch_multiple_calls(side_effects) as mock_call:
269- with self.assert_program_exit(expected):
270- app.bootstrap(self.env_name)
271+ call_side_effects = [
272+ (0, '', ''), # Add the bootstrap call.
273+ (1, 'output1', 'error1'), # Add the first status call: retried.
274+ (1, 'output2', 'error2'), # Add the second status call: error.
275+ ]
276+ time_side_effects = [
277+ 0, # Start at time zero (expiration at time 600).
278+ 10, # First call before the timeout expiration.
279+ 100, # Second call before the timeout expiration.
280+ 1000, # Third call after the timeout expiration.
281+ ]
282+ mock_time = mock.Mock(side_effect=time_side_effects)
283+ expected = 'the state server is not ready:\noutput2error2'
284+ with self.patch_multiple_calls(call_side_effects) as mock_call:
285+ # Simulate the timeout expired: the first time call is used to
286+ # calculate the timeout, the second one for the first status check,
287+ # the third for the second status check, the fourth should fail.
288+ with mock.patch('time.time', mock_time):
289+ with self.assert_program_exit(expected):
290+ app.bootstrap(self.env_name)
291 mock_call.assert_has_calls([
292 mock.call('juju', 'bootstrap', '-e', self.env_name),
293- ] + self.make_status_calls(5))
294+ ] + self.make_status_calls(2))
295
296
297 class TestGetApiUrl(
298
299=== modified file 'quickstart/tests/test_manage.py'
300--- quickstart/tests/test_manage.py 2013-11-19 12:22:32 +0000
301+++ quickstart/tests/test_manage.py 2013-11-20 12:26:05 +0000
302@@ -238,8 +238,10 @@
303 env_file = self.make_env_file(contents)
304 options = self.make_options(env_file, env_name='lxc')
305 manage._validate_env(options, self.parser)
306- expected = 'the local provider is not currently supported'
307- self.parser.error.assert_called_once_with(expected)
308+ self.assertEqual('Secret!', options.admin_secret)
309+ self.assertEqual(env_file, options.env_file)
310+ self.assertEqual('lxc', options.env_name)
311+ self.assertEqual('local', options.env_type)
312
313
314 @mock.patch('quickstart.manage._validate_env', mock.Mock())
315@@ -352,7 +354,7 @@
316 options = self.make_options()
317 manage.run(options)
318 mock_app.bootstrap.assert_called_once_with(
319- options.env_name, debug=options.debug)
320+ options.env_name, is_local=False, debug=options.debug)
321 mock_app.get_api_url.assert_called_once_with(options.env_name)
322 mock_app.connect.assert_called_once_with(
323 mock_app.get_api_url(), options.admin_secret)
324@@ -377,6 +379,13 @@
325 'wss://gui.example.com:443/ws', options.admin_secret,
326 'mybundle: contents', 'mybundle', None)
327
328+ def test_local_provider(self, mock_app, mock_open):
329+ # The application correctly handles working with local providers.
330+ options = self.make_options(env_type='local')
331+ manage.run(options)
332+ mock_app.bootstrap.assert_called_once_with(
333+ options.env_name, is_local=True, debug=options.debug)
334+
335 def test_no_browser(self, mock_app, mock_open):
336 # It is possible to avoid opening the GUI in the browser.
337 options = self.make_options(open_browser=False)
338
339=== modified file 'quickstart/utils.py'
340--- quickstart/utils.py 2013-11-18 20:34:09 +0000
341+++ quickstart/utils.py 2013-11-20 12:26:05 +0000
342@@ -35,7 +35,7 @@
343 _juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n')
344
345
346-def call(*args):
347+def call(command, *args):
348 """Call a subprocess passing the given arguments.
349
350 Take the subcommand and its parameters as args.
351@@ -43,14 +43,15 @@
352 Return a tuple containing the subprocess return code, output and error.
353 """
354 pipe = subprocess.PIPE
355- cmdline = ' '.join(map(pipes.quote, args))
356+ cmd = (command,) + args
357+ cmdline = ' '.join(map(pipes.quote, cmd))
358 logging.debug('running the following: {}'.format(cmdline))
359 try:
360- process = subprocess.Popen(args, stdout=pipe, stderr=pipe)
361+ process = subprocess.Popen(cmd, stdout=pipe, stderr=pipe)
362 except OSError as err:
363 # A return code 127 is returned by the shell when the command is not
364 # found in the PATH.
365- return 127, '', '{}: {}'.format(args[0], err)
366+ return 127, '', '{}: {}'.format(command, err)
367 output, error = process.communicate()
368 retcode = process.poll()
369 logging.debug('retcode: {} | output: {!r} | error: {!r}'.format(

Subscribers

People subscribed via source and target branches