Merge lp:~frankban/juju-quickstart/support-json into lp:juju-quickstart
- support-json
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 10 | ||||
Proposed branch: | lp:~frankban/juju-quickstart/support-json | ||||
Merge into: | lp:juju-quickstart | ||||
Diff against target: |
564 lines (+191/-50) 12 files modified
.bzrignore (+1/-0) HACKING.rst (+1/-1) README.rst (+1/-1) quickstart/__init__.py (+1/-1) quickstart/app.py (+20/-10) quickstart/manage.py (+36/-10) quickstart/tests/helpers.py (+26/-5) quickstart/tests/test_app.py (+43/-12) quickstart/tests/test_manage.py (+38/-2) quickstart/tests/test_utils.py (+19/-6) quickstart/utils.py (+4/-1) setup.py (+1/-1) |
||||
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/support-json | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
JSON support + minor fixes.
This branch includes official JSON
support for bundle deployments.
This branch also contains a number of
minor fixes/improvements:
- Added the possibility to provide a
customized Juju GUI charm URL.
- Added support for providing a bundle
directory (a dir containing a
bundles.yaml file): fix bug 1247181.
- Some documentation clean up.
- Added an option to decline opening the
browser.
- The Juju env is bootstrapped passing
--debug when quickstart is run in debug
mode.
- Bumped version up.
Tests: `make check`.
QA:
I uploaded a bundle JSON here:
http://
So it is possible to check the new features
by running the following (expect a scary output):
.venv/bin/python juju-quickstart --debug --no-browser \
--gui-charm-url cs:~juju-
-e ec2 http://
At the end of the process, locate the GUI url
printed before the last debug messages, and check
the GUI is up, running and the bundle is deploying.
Remember to destroy your ec2 env.
Thank you!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Madison Scott-Clary (makyo) wrote : | # |
LGTM, QA Okay. Thanks!
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
LGTM with trivial. Thank you!
https:/
File quickstart/app.py (right):
https:/
quickstart/
URL is not provided, the
Maybe give an example of the expected format, e.g.
cs:~juju-
valuable/important for the commandline help.
https:/
File quickstart/
https:/
quickstart/
deployed')
This would be the ideal place to include an example IMO, like mentioning
that you can get the dev charm by using cs:~juju-
- 16. By Francesco Banconi
-
Changes as per review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
JSON support + minor fixes.
This branch includes official JSON
support for bundle deployments.
This branch also contains a number of
minor fixes/improvements:
- Added the possibility to provide a
customized Juju GUI charm URL.
- Added support for providing a bundle
directory (a dir containing a
bundles.yaml file): fix bug 1247181.
- Some documentation clean up.
- Added an option to decline opening the
browser.
- The Juju env is bootstrapped passing
--debug when quickstart is run in debug
mode.
- Bumped version up.
Tests: `make check`.
QA:
I uploaded a bundle JSON here:
http://
So it is possible to check the new features
by running the following (expect a scary output):
.venv/bin/python juju-quickstart --debug --no-browser \
--gui-charm-url cs:~juju-
-e ec2 http://
At the end of the process, locate the GUI url
printed before the last debug messages, and check
the GUI is up, running and the bundle is deploying.
Remember to destroy your ec2 env.
Thank you!
R=
CC=
https:/
https:/
File quickstart/app.py (right):
https:/
quickstart/
URL is not provided, the
On 2013/11/06 17:40:43, gary.poster wrote:
> Maybe give an example of the expected format, e.g.
> cs:~juju-
valuable/important for
> the commandline help.
Done.
https:/
File quickstart/
https:/
quickstart/
deployed')
On 2013/11/06 17:40:43, gary.poster wrote:
> This would be the ideal place to include an example IMO, like
mentioning that
> you can get the dev charm by using cs:~juju-
Done.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
Thank you both!
Preview Diff
1 | === modified file '.bzrignore' |
2 | --- .bzrignore 2013-10-14 15:54:00 +0000 |
3 | +++ .bzrignore 2013-11-06 17:54:24 +0000 |
4 | @@ -1,4 +1,5 @@ |
5 | .coverage |
6 | +.DS_Store |
7 | .emacs* |
8 | .venv |
9 | build |
10 | |
11 | === modified file 'HACKING.rst' |
12 | --- HACKING.rst 2013-10-14 15:24:29 +0000 |
13 | +++ HACKING.rst 2013-11-06 17:54:24 +0000 |
14 | @@ -5,7 +5,7 @@ |
15 | environment in very few steps. The environment is bootstrapped and set up so |
16 | that it can be managed using a Web interface (the Juju GUI). |
17 | |
18 | -Bundle support is a planned feature, and will allow for setting up a complete |
19 | +Bundle deployments are also supported, and allow for setting up a complete |
20 | topology of services in one simple command. |
21 | |
22 | Creating a development environment |
23 | |
24 | === modified file 'README.rst' |
25 | --- README.rst 2013-10-14 15:24:29 +0000 |
26 | +++ README.rst 2013-11-06 17:54:24 +0000 |
27 | @@ -5,5 +5,5 @@ |
28 | environment in very few steps. The environment is bootstrapped and set up so |
29 | that it can be managed using a Web interface (the Juju GUI). |
30 | |
31 | -Bundle support is a planned feature, and will allow for setting up a complete |
32 | +Bundle deployments are also supported, and allow for setting up a complete |
33 | topology of services in one simple command. |
34 | |
35 | === modified file 'quickstart/__init__.py' |
36 | --- quickstart/__init__.py 2013-10-30 10:16:36 +0000 |
37 | +++ quickstart/__init__.py 2013-11-06 17:54:24 +0000 |
38 | @@ -19,7 +19,7 @@ |
39 | that it can be managed using a Web interface (the Juju GUI). |
40 | """ |
41 | |
42 | -VERSION = (0, 2, 0) |
43 | +VERSION = (0, 3, 0) |
44 | |
45 | |
46 | def get_version(): |
47 | |
48 | === modified file 'quickstart/app.py' |
49 | --- quickstart/app.py 2013-10-30 17:37:46 +0000 |
50 | +++ quickstart/app.py 2013-11-06 17:54:24 +0000 |
51 | @@ -48,13 +48,18 @@ |
52 | return 'juju-quickstart: error: {}'.format(self.message) |
53 | |
54 | |
55 | -def bootstrap(env_name): |
56 | +def bootstrap(env_name, debug=False): |
57 | """Bootstrap the Juju environment with the given name. |
58 | |
59 | + If debug is True, bootstrap the environment passing the --debug flag. |
60 | + |
61 | Return when the bootstrap node is ready. |
62 | Raise a ProgramExit if any error occurs in the bootstrap process. |
63 | """ |
64 | - retcode, _, error = utils.call('juju', 'bootstrap', '-e', env_name) |
65 | + cmd = ['juju', 'bootstrap', '-e', env_name] |
66 | + if debug: |
67 | + cmd.append('--debug') |
68 | + retcode, _, error = utils.call(*cmd) |
69 | if retcode: |
70 | raise ProgramExit(error) |
71 | # Call "juju status" multiple times until the bootstrap node is ready. |
72 | @@ -111,20 +116,25 @@ |
73 | return env |
74 | |
75 | |
76 | -def deploy_gui(env, service_name): |
77 | +def deploy_gui(env, service_name, charm_url=None): |
78 | """Deploy and expose the given service, reusing the bootstrap node. |
79 | |
80 | Receive an authenticated Juju Environment instance, the name of the service |
81 | - and the corresponding charm URL. |
82 | + and the optional Juju GUI charm URL, e.g. cs:~juju-gui/precise/juju-gui-42. |
83 | + If the charm URL is not provided, the function tries to retrieve it from |
84 | + charmworld. In this case a default charm URL is used if charmworld is not |
85 | + available. |
86 | |
87 | Raise a ProgramExit if the API server returns an error response. |
88 | """ |
89 | - try: |
90 | - charm_url = utils.get_charm_url() |
91 | - except (IOError, ValueError) as err: |
92 | - msg = 'unable to retrieve the Juju GUI charm URL from the API: {}' |
93 | - logging.warn(msg.format(err)) |
94 | - charm_url = DEFAULT_CHARM_URL |
95 | + if charm_url is None: |
96 | + try: |
97 | + charm_url = utils.get_charm_url() |
98 | + except (IOError, ValueError) as err: |
99 | + msg = 'unable to retrieve the Juju GUI charm URL from the API: {}' |
100 | + logging.warn(msg.format(err)) |
101 | + charm_url = DEFAULT_CHARM_URL |
102 | + print('charm URL: {}'.format(charm_url)) |
103 | try: |
104 | env.deploy(service_name, charm_url, to=0) |
105 | env.expose(service_name) |
106 | |
107 | === modified file 'quickstart/manage.py' |
108 | --- quickstart/manage.py 2013-10-30 17:37:46 +0000 |
109 | +++ quickstart/manage.py 2013-11-06 17:54:24 +0000 |
110 | @@ -62,6 +62,8 @@ |
111 | else: |
112 | # Load the bundle file. |
113 | bundle_file = os.path.abspath(os.path.expanduser(bundle)) |
114 | + if os.path.isdir(bundle_file): |
115 | + bundle_file = os.path.join(bundle_file, 'bundles.yaml') |
116 | try: |
117 | bundle_yaml = open(bundle_file).read() |
118 | except IOError as err: |
119 | @@ -131,9 +133,19 @@ |
120 | |
121 | Return the options as a namespace containing the following attributes: |
122 | - admin_secret: the password to use to access the Juju API; |
123 | + - bundle: the optional bundle (path or URL) to be deployed; |
124 | + - charm_url: the Juju GUI charm URL or None if not specified; |
125 | + - debug: whether debug mode is activated; |
126 | - env_file: the absolute path of the Juju environments.yaml file; |
127 | - env_name: the name of the Juju environment to use; |
128 | - - env_type: the provider type of the selected Juju environment. |
129 | + - env_type: the provider type of the selected Juju environment; |
130 | + - open_browser: whether the GUI browser must be opened. |
131 | + |
132 | + The following attributes will also be included in the namespace if a bundle |
133 | + deployment is requested: |
134 | + - bundle_name: the name of the bundle to be deployed; |
135 | + - bundle_services: a list of service names included in the bundle; |
136 | + - bundle_yaml: the YAML encoded contents of the bundle. |
137 | |
138 | Exit with an error if the provided arguments are not valid. |
139 | """ |
140 | @@ -146,24 +158,39 @@ |
141 | parser = argparse.ArgumentParser(description=quickstart.__doc__) |
142 | parser.add_argument( |
143 | 'bundle', default=None, nargs='?', |
144 | - help='The bundle URL or the path to the bundle file to deploy') |
145 | + help='The optional bundle to be deployed. The bundle can be ' |
146 | + '1) a path to a YAML/JSON file or ' |
147 | + '2) a path to a directory containing a bundles.yaml file or ' |
148 | + '3) a URL (starting with http:// or https://) to a YAML/JSON') |
149 | parser.add_argument( |
150 | '-e', '--environment', default=default_env_name, dest='env_name', |
151 | help=env_help) |
152 | parser.add_argument( |
153 | '-n', '--bundle-name', default=None, dest='bundle_name', |
154 | help='The name of the bundle to use. This must be included in the ' |
155 | - 'provided bundle file. Specifying the bundle name is not ' |
156 | - 'required if the bundle file only contains one bundle. This ' |
157 | + 'provided bundle YAML/JSON. Specifying the bundle name is not ' |
158 | + 'required if the bundle YAML/JSON only contains one bundle. This ' |
159 | 'option is ignored if the bundle file is not specified') |
160 | parser.add_argument( |
161 | '--environments-file', |
162 | default=os.path.join(juju_home, 'environments.yaml'), dest='env_file', |
163 | help='The path to the Juju environments YAML file (%(default)s)') |
164 | parser.add_argument( |
165 | + '--gui-charm-url', dest='charm_url', |
166 | + help='The Juju GUI charm URL to deploy in the environment. If not ' |
167 | + 'provided, the last release of the GUI will be deployed. The ' |
168 | + 'charm URL must include the charm version, e.g. ' |
169 | + 'cs:~juju-gui/precise/juju-gui-116') |
170 | + parser.add_argument( |
171 | + '--no-browser', action='store_false', dest='open_browser', |
172 | + help='Avoid opening the browser to the GUI at the end of the process') |
173 | + parser.add_argument( |
174 | '--version', action='version', version='%(prog)s {}'.format(version)) |
175 | parser.add_argument( |
176 | - '--debug', action='store_true', help='Turn debug mode on') |
177 | + '--debug', action='store_true', |
178 | + help='Turn debug mode on. When enabled, all the subcommands and API ' |
179 | + 'calls are logged to stdout, and the Juju environment is ' |
180 | + 'bootstrapped passing --debug') |
181 | # This is required by juju-core: see "juju help plugins". |
182 | parser.add_argument( |
183 | '--description', action=_DescriptionAction, default=argparse.SUPPRESS, |
184 | @@ -184,13 +211,13 @@ |
185 | print('juju quickstart v{}'.format(version)) |
186 | print('bootstrapping the {} environment (type: {})'.format( |
187 | options.env_name, options.env_type)) |
188 | - app.bootstrap(options.env_name) |
189 | + app.bootstrap(options.env_name, debug=options.debug) |
190 | print('retrieving the Juju API address') |
191 | api_url = app.get_api_url(options.env_name) |
192 | print('connecting to {}'.format(api_url)) |
193 | env = app.connect(api_url, options.admin_secret) |
194 | print('requesting Juju GUI deployment') |
195 | - app.deploy_gui(env, 'juju-gui') |
196 | + app.deploy_gui(env, 'juju-gui', charm_url=options.charm_url) |
197 | print('Juju GUI deployment request accepted') |
198 | address = app.watch(env, 'juju-gui') |
199 | url = 'https://{}'.format(address) |
200 | @@ -208,7 +235,6 @@ |
201 | gui_api_url, options.admin_secret, |
202 | options.bundle_yaml, options.bundle_name) |
203 | |
204 | - # XXX 2013-10-18 frankban: |
205 | - # Add a command line option to decline opening the browser. |
206 | - webbrowser.open(url) |
207 | + if options.open_browser: |
208 | + webbrowser.open(url) |
209 | print('done!') |
210 | |
211 | === modified file 'quickstart/tests/helpers.py' |
212 | --- quickstart/tests/helpers.py 2013-10-30 17:37:46 +0000 |
213 | +++ quickstart/tests/helpers.py 2013-11-06 17:54:24 +0000 |
214 | @@ -18,6 +18,7 @@ |
215 | |
216 | from contextlib import contextmanager |
217 | import os |
218 | +import shutil |
219 | import tempfile |
220 | |
221 | import mock |
222 | @@ -45,6 +46,14 @@ |
223 | 'bundle2': {'services': {'django': {}, 'nodejs': {}}}, |
224 | }) |
225 | |
226 | + def _write_bundle_file(self, bundle_file, contents): |
227 | + """Parse and write contents into the given bundle file object.""" |
228 | + if contents is None: |
229 | + contents = self.valid_bundle |
230 | + elif isinstance(contents, dict): |
231 | + contents = yaml.safe_dump(contents) |
232 | + bundle_file.write(contents) |
233 | + |
234 | def make_bundle_file(self, contents=None): |
235 | """Create a Juju bundle file containing the given contents. |
236 | |
237 | @@ -52,16 +61,28 @@ |
238 | self.valid_bundle. |
239 | Return the bundle file path. |
240 | """ |
241 | - if contents is None: |
242 | - contents = self.valid_bundle |
243 | - elif isinstance(contents, dict): |
244 | - contents = yaml.safe_dump(contents) |
245 | bundle_file = tempfile.NamedTemporaryFile(delete=False) |
246 | self.addCleanup(os.remove, bundle_file.name) |
247 | - bundle_file.write(contents) |
248 | + self._write_bundle_file(bundle_file, contents) |
249 | bundle_file.close() |
250 | return bundle_file.name |
251 | |
252 | + def make_bundle_dir(self, contents=None): |
253 | + """Create a Juju bundle directory including a bundles.yaml file. |
254 | + |
255 | + The file will contain the given contents. |
256 | + |
257 | + If contents is None, use the valid bundle contents defined in |
258 | + self.valid_bundle. |
259 | + Return the bundle directory path. |
260 | + """ |
261 | + bundle_dir = tempfile.mkdtemp() |
262 | + self.addCleanup(shutil.rmtree, bundle_dir) |
263 | + bundle_path = os.path.join(bundle_dir, 'bundles.yaml') |
264 | + with open(bundle_path, 'w') as bundle_file: |
265 | + self._write_bundle_file(bundle_file, contents) |
266 | + return bundle_dir |
267 | + |
268 | |
269 | class CallTestsMixin(object): |
270 | """Easily use the quickstart.utils.call function.""" |
271 | |
272 | === modified file 'quickstart/tests/test_app.py' |
273 | --- quickstart/tests/test_app.py 2013-10-30 17:37:46 +0000 |
274 | +++ quickstart/tests/test_app.py 2013-11-06 17:54:24 +0000 |
275 | @@ -28,6 +28,9 @@ |
276 | from quickstart.tests import helpers |
277 | |
278 | |
279 | +mock_print = mock.patch('__builtin__.print') |
280 | + |
281 | + |
282 | class TestProgramExit(unittest.TestCase): |
283 | |
284 | def test_string_representation(self): |
285 | @@ -72,6 +75,13 @@ |
286 | 'juju', 'status', '-e', self.env_name, '--format', 'yaml') |
287 | return [call for _ in range(number)] |
288 | |
289 | + def make_side_effects(self): |
290 | + """Return the minimum number of side effects for a successful call.""" |
291 | + return [ |
292 | + (0, '', ''), # Add a bootstrap call. |
293 | + (0, self.make_status_output('started'), ''), # Add a status call. |
294 | + ] |
295 | + |
296 | def assert_status_retried(self, side_effects): |
297 | """Ensure the "juju status" command is retried several times. |
298 | |
299 | @@ -85,16 +95,20 @@ |
300 | |
301 | def test_success(self): |
302 | # The environment is successfully bootstrapped. |
303 | - side_effects = [ |
304 | - (0, '', ''), # Add a bootstrap call. |
305 | - (0, self.make_status_output('started'), ''), # Add a status call. |
306 | - ] |
307 | - with self.patch_multiple_calls(side_effects) as mock_call: |
308 | + with self.patch_multiple_calls(self.make_side_effects()) as mock_call: |
309 | app.bootstrap(self.env_name) |
310 | mock_call.assert_has_calls([ |
311 | mock.call('juju', 'bootstrap', '-e', self.env_name), |
312 | ] + self.make_status_calls(1)) |
313 | |
314 | + def test_success_debug(self): |
315 | + # The environment is successfully bootstrapped in debug mode. |
316 | + with self.patch_multiple_calls(self.make_side_effects()) as mock_call: |
317 | + app.bootstrap(self.env_name, debug=True) |
318 | + mock_call.assert_has_calls([ |
319 | + mock.call('juju', 'bootstrap', '-e', self.env_name, '--debug'), |
320 | + ] + self.make_status_calls(1)) |
321 | + |
322 | def test_bootstrap_failure(self): |
323 | # A ProgramExit is raised if an error occurs while bootstrapping. |
324 | with self.patch_call(1, error='bad wolf') as mock_call: |
325 | @@ -233,6 +247,7 @@ |
326 | self.assertIs(error, context_manager.exception) |
327 | |
328 | |
329 | +@mock_print |
330 | class TestDeployGui(ProgramExitTestsMixin, unittest.TestCase): |
331 | |
332 | charm_url = 'cs:precise/juju-gui-42' |
333 | @@ -244,18 +259,20 @@ |
334 | mock_get_charm_url = mock.Mock(side_effect=side_effect) |
335 | return mock.patch('quickstart.utils.get_charm_url', mock_get_charm_url) |
336 | |
337 | - def test_deployment(self): |
338 | + def test_deployment(self, mock_print): |
339 | # The function correctly deploys and exposes the service, retrieving |
340 | # the charm URL from the charmworld API. |
341 | env = mock.Mock() |
342 | with self.patch_get_charm_url(): |
343 | app.deploy_gui(env, 'my-gui') |
344 | env.assert_has_calls([ |
345 | - mock.call.deploy('my-gui', 'cs:precise/juju-gui-42', to=0), |
346 | + mock.call.deploy('my-gui', self.charm_url, to=0), |
347 | mock.call.expose('my-gui') |
348 | ]) |
349 | + mock_print.assert_called_once_with( |
350 | + 'charm URL: {}'.format(self.charm_url)) |
351 | |
352 | - def test_deployment_default_charm_url(self): |
353 | + def test_deployment_default_charm_url(self, mock_print): |
354 | # The function correctly deploys and exposes the service, even if it is |
355 | # not able to retrieve the charm URL from the charmworld API. |
356 | env = mock.Mock() |
357 | @@ -268,8 +285,22 @@ |
358 | mock.call.deploy('my-gui', app.DEFAULT_CHARM_URL, to=0), |
359 | mock.call.expose('my-gui') |
360 | ]) |
361 | - |
362 | - def test_api_error(self): |
363 | + mock_print.assert_called_once_with( |
364 | + 'charm URL: {}'.format(app.DEFAULT_CHARM_URL)) |
365 | + |
366 | + def test_deployment_provided_charm_url(self, mock_print): |
367 | + # The function correctly deploys and exposes the service using a user |
368 | + # provided Juju GUI charm URL. |
369 | + env = mock.Mock() |
370 | + charm_url = 'cs:~juju-gui/precise/juju-gui-116' |
371 | + app.deploy_gui(env, 'my-gui', charm_url=charm_url) |
372 | + env.assert_has_calls([ |
373 | + mock.call.deploy('my-gui', charm_url, to=0), |
374 | + mock.call.expose('my-gui') |
375 | + ]) |
376 | + mock_print.assert_called_once_with('charm URL: {}'.format(charm_url)) |
377 | + |
378 | + def test_api_error(self, mock_print): |
379 | # A ProgramExit is raised if an error occurs in one of the API calls. |
380 | env = mock.Mock() |
381 | env.deploy.side_effect = self.make_env_error('service already exists') |
382 | @@ -280,7 +311,7 @@ |
383 | env.deploy.assert_called_once_with( |
384 | 'another-gui', 'cs:precise/juju-gui-42', to=0) |
385 | |
386 | - def test_other_errors(self): |
387 | + def test_other_errors(self, mock_print): |
388 | # Any other errors occurred during the process are not trapped. |
389 | error = ValueError('explode!') |
390 | env = mock.Mock() |
391 | @@ -294,7 +325,7 @@ |
392 | self.assertIs(error, context_manager.exception) |
393 | |
394 | |
395 | -@mock.patch('__builtin__.print') |
396 | +@mock_print |
397 | class TestWatch(ProgramExitTestsMixin, unittest.TestCase): |
398 | |
399 | address = 'unit.example.com' |
400 | |
401 | === modified file 'quickstart/tests/test_manage.py' |
402 | --- quickstart/tests/test_manage.py 2013-10-30 17:37:46 +0000 |
403 | +++ quickstart/tests/test_manage.py 2013-11-06 17:54:24 +0000 |
404 | @@ -19,6 +19,8 @@ |
405 | import argparse |
406 | import logging |
407 | import os |
408 | +import shutil |
409 | +import tempfile |
410 | import unittest |
411 | |
412 | import mock |
413 | @@ -80,6 +82,17 @@ |
414 | ['mysql', 'wordpress'], sorted(options.bundle_services)) |
415 | self.assertEqual(open(bundle_file).read(), options.bundle_yaml) |
416 | |
417 | + def test_resulting_options_from_dir(self): |
418 | + # The options object is correctly set up when a bundle dir is passed. |
419 | + bundle_dir = self.make_bundle_dir() |
420 | + options = self.make_options(bundle_dir, bundle_name='bundle1') |
421 | + manage._validate_bundle(options, self.parser) |
422 | + self.assertEqual('bundle1', options.bundle_name) |
423 | + self.assertEqual( |
424 | + ['mysql', 'wordpress'], sorted(options.bundle_services)) |
425 | + expected = open(os.path.join(bundle_dir, 'bundles.yaml')).read() |
426 | + self.assertEqual(expected, options.bundle_yaml) |
427 | + |
428 | def test_expand_user(self): |
429 | # The ~ construct is correctly expanded in the validation process. |
430 | bundle_file = self.make_bundle_file() |
431 | @@ -104,6 +117,19 @@ |
432 | ) |
433 | self.parser.error.assert_called_once_with(expected) |
434 | |
435 | + def test_bundle_dir_not_valid(self): |
436 | + # A parser error is invoked if the bundle dir does not contain the |
437 | + # bundles.yaml file. |
438 | + bundle_dir = tempfile.mkdtemp() |
439 | + self.addCleanup(shutil.rmtree, bundle_dir) |
440 | + options = self.make_options(bundle_dir) |
441 | + manage._validate_bundle(options, self.parser) |
442 | + expected = ( |
443 | + 'unable to open bundle file: ' |
444 | + "[Errno 2] No such file or directory: '{}/bundles.yaml'" |
445 | + ).format(bundle_dir) |
446 | + self.parser.error.assert_called_once_with(expected) |
447 | + |
448 | def test_url_error(self): |
449 | # A parser error is invoked if the bundle cannot be fetched from the |
450 | # provided URL. |
451 | @@ -283,9 +309,12 @@ |
452 | """Set up the options to be passed to the run function.""" |
453 | options = { |
454 | 'admin_secret': 'Secret!', |
455 | + 'debug': False, |
456 | + 'charm_url': None, |
457 | 'bundle': None, |
458 | 'env_name': 'aws', |
459 | 'env_type': 'ec2', |
460 | + 'open_browser': True, |
461 | } |
462 | options.update(kwargs) |
463 | return mock.Mock(**options) |
464 | @@ -294,12 +323,13 @@ |
465 | # The application runs correctly if no bundle is provided. |
466 | options = self.make_options() |
467 | manage.run(options) |
468 | - mock_app.bootstrap.assert_called_once_with(options.env_name) |
469 | + mock_app.bootstrap.assert_called_once_with( |
470 | + options.env_name, debug=options.debug) |
471 | mock_app.get_api_url.assert_called_once_with(options.env_name) |
472 | mock_app.connect.assert_called_once_with( |
473 | mock_app.get_api_url(), options.admin_secret) |
474 | mock_app.deploy_gui.assert_called_once_with( |
475 | - mock_app.connect(), 'juju-gui') |
476 | + mock_app.connect(), 'juju-gui', charm_url=options.charm_url) |
477 | mock_app.watch.assert_called_once_with(mock_app.connect(), 'juju-gui') |
478 | mock_open.assert_called_once_with( |
479 | 'https://{}'.format(mock_app.watch())) |
480 | @@ -315,3 +345,9 @@ |
481 | mock_app.deploy_bundle.assert_called_once_with( |
482 | 'wss://gui.example.com:443/ws', options.admin_secret, |
483 | 'mybundle: contents', 'mybundle') |
484 | + |
485 | + def test_no_browser(self, mock_app, mock_open): |
486 | + # It is possible to avoid opening the GUI in the browser. |
487 | + options = self.make_options(open_browser=False) |
488 | + manage.run(options) |
489 | + self.assertFalse(mock_open.called) |
490 | |
491 | === modified file 'quickstart/tests/test_utils.py' |
492 | --- quickstart/tests/test_utils.py 2013-10-31 08:33:49 +0000 |
493 | +++ quickstart/tests/test_utils.py 2013-11-06 17:54:24 +0000 |
494 | @@ -157,6 +157,14 @@ |
495 | helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin, |
496 | unittest.TestCase): |
497 | |
498 | + def assert_bundle( |
499 | + self, expected_name, expected_services, contents, |
500 | + bundle_name=None): |
501 | + """Ensure parsing the given contents returns the expected values.""" |
502 | + name, services = utils.parse_bundle(contents, bundle_name=bundle_name) |
503 | + self.assertEqual(expected_name, name) |
504 | + self.assertEqual(set(expected_services), set(services)) |
505 | + |
506 | def test_invalid_yaml(self): |
507 | # A ValueError is raised if the bundle contents are not a valid YAML. |
508 | with self.assertRaises(ValueError) as context_manager: |
509 | @@ -230,15 +238,20 @@ |
510 | contents = yaml.safe_dump({ |
511 | 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}}, |
512 | }) |
513 | - name, services = utils.parse_bundle(contents) |
514 | - self.assertEqual('mybundle', name) |
515 | - self.assertEqual(['mysql', 'wordpress'], sorted(services)) |
516 | + self.assert_bundle('mybundle', ['mysql', 'wordpress'], contents) |
517 | |
518 | def test_success_multiple_bundles(self): |
519 | # The function succeeds with multiple bundles. |
520 | - name, services = utils.parse_bundle(self.valid_bundle, 'bundle2') |
521 | - self.assertEqual('bundle2', name) |
522 | - self.assertEqual(['django', 'nodejs'], sorted(services)) |
523 | + self.assert_bundle( |
524 | + 'bundle2', ['django', 'nodejs'], self.valid_bundle, 'bundle2') |
525 | + |
526 | + def test_success_json(self): |
527 | + # Since JSON is a subset of YAML, the function also support JSON |
528 | + # encoded bundles. |
529 | + contents = json.dumps({ |
530 | + 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}}, |
531 | + }) |
532 | + self.assert_bundle('mybundle', ['mysql', 'wordpress'], contents) |
533 | |
534 | |
535 | class TestParseEnvFile( |
536 | |
537 | === modified file 'quickstart/utils.py' |
538 | --- quickstart/utils.py 2013-10-31 08:33:49 +0000 |
539 | +++ quickstart/utils.py 2013-11-06 17:54:24 +0000 |
540 | @@ -101,7 +101,10 @@ |
541 | |
542 | |
543 | def parse_bundle(bundle_yaml, bundle_name=None): |
544 | - """Parse the provided bundle YAML decoded contents. |
545 | + """Parse the provided bundle YAML encoded contents. |
546 | + |
547 | + Since a valid JSON is a subset of YAML this function can be used also to |
548 | + parse JSON encoded contents. |
549 | |
550 | Return a tuple containing the bundle name and the list of services included |
551 | in the bundle. |
552 | |
553 | === modified file 'setup.py' |
554 | --- setup.py 2013-10-30 10:16:36 +0000 |
555 | +++ setup.py 2013-11-06 17:54:24 +0000 |
556 | @@ -57,7 +57,7 @@ |
557 | long_description=open(description_path).read(), |
558 | author='The Juju GUI team', |
559 | author_email='juju-gui@lists.ubuntu.com', |
560 | - url='https://launchpad.net/juju-gui/juju-quickstart', |
561 | + url='https://launchpad.net/juju-quickstart', |
562 | keywords='juju quickstart plugin', |
563 | packages=find_packages(), |
564 | package_data={PROJECT_NAME: data_files}, |
Reviewers: mp+194172_ code.launchpad. net,
Message:
Please take a look.
Description:
JSON support + minor fixes.
This branch includes official JSON
support for bundle deployments.
This branch also contains a number of
minor fixes/improvements:
- Added the possibility to provide a
customized Juju GUI charm URL.
- Added support for providing a bundle
directory (a dir containing a
bundles.yaml file): fix bug 1247181.
- Some documentation clean up.
- Added an option to decline opening the
browser.
- The Juju env is bootstrapped passing
--debug when quickstart is run in debug
mode.
- Bumped version up.
Tests: `make check`.
QA: pastebin. com/raw. php?i=4mDCcagp gui/precise/ juju-gui- 116 \ pastebin. com/raw. php?i=4mDCcagp
I uploaded a bundle JSON here:
http://
So it is possible to check the new features
by running the following (expect a scary output):
.venv/bin/python juju-quickstart --debug --no-browser \
--gui-charm-url cs:~juju-
-e ec2 http://
At the end of the process, locate the GUI url
printed before the last debug messages, and check
the GUI is up, running and the bundle is deploying.
Remember to destroy your ec2 env.
Thank you!
https:/ /code.launchpad .net/~frankban/ juju-quickstart /support- json/+merge/ 194172
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/22300044/
Affected files (+190, -50 lines): __init_ _.py manage. py tests/helpers. py tests/test_ app.py tests/test_ manage. py tests/test_ utils.py
M .bzrignore
M HACKING.rst
M README.rst
A [revision details]
M quickstart/
M quickstart/app.py
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/utils.py
M setup.py