Merge lp:~frankban/juju-quickstart/local-provider into lp:juju-quickstart
- local-provider
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+195952@code.launchpad.net |
Commit message
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.
Francesco Banconi (frankban) wrote : | # |
Richard Harding (rharding) wrote : | # |
Code looks good, doing qa.
Richard Harding (rharding) wrote : | # |
LGTM + qa ok in local
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:/
Francesco Banconi (frankban) wrote : | # |
Thank you!
Preview Diff
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( |
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): manage. py tests/test_ app.py tests/test_ manage. py
M HACKING.rst
A [revision details]
M quickstart/app.py
M quickstart/
M quickstart/
M quickstart/
M quickstart/utils.py