Merge lp:~frankban/juju-quickstart/quickstart-bundle-file into lp:juju-quickstart
- quickstart-bundle-file
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 7 |
Proposed branch: | lp:~frankban/juju-quickstart/quickstart-bundle-file |
Merge into: | lp:juju-quickstart |
Diff against target: |
602 lines (+427/-7) 10 files modified
quickstart/__init__.py (+1/-1) quickstart/app.py (+17/-0) quickstart/juju.py (+12/-0) quickstart/manage.py (+62/-4) quickstart/tests/helpers.py (+26/-0) quickstart/tests/test_app.py (+37/-0) quickstart/tests/test_juju.py (+22/-0) quickstart/tests/test_manage.py (+108/-2) quickstart/tests/test_utils.py (+88/-0) quickstart/utils.py (+54/-0) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/quickstart-bundle-file |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+192194@code.launchpad.net |
Commit message
Description of the change
Deploy a bundle from a file path.
Tests: run `make check`.
QA (assuming you have an ec2 environment set up named 'ec2'):
- run `.venv/bin/python juju-quickstart /no/such/file`:
you should see the following error:
"unable to open bundle file: [Errno 2]
No such file or directory: '/no/such/file'";
- run `echo "env: no-services" > ~/invalid-
- run `.venv/bin/python juju-quickstart ~/invalid-
you should see the following error:
"invalid YAML contents: {'env': 'no-services'}";
- remove the file you created in your home:
`rm ~/invalid-
- download the bundle file from
http://
for later as '../mybundle.yaml';
- run `.venv/bin/python juju-quickstart ../mybundle.yaml`:
you should see the following error:
"multiple bundles found (exp1, exp2) but no bundle name specified";
- run `.venv/bin/python juju-quickstart ../mybundle.yaml -n exp1`:
this time the quickstart application should start normally.
- wait fr quickstart to complete: after some minutes the browser
should open on the GUI page, the bundle deployment should be
started, and the app exits without erros.
Remember to destroy your ec2 environment.
Francesco Banconi (frankban) wrote : | # |
Gary Poster (gary) wrote : | # |
LGTM! Thank you. :-)
I will qa later if no one else has been able to.
Gary
https:/
File quickstart/
https:/
quickstart/
specified')
I'd be tempted to change all the help to full sentences, with capital
letters and such, and then replace a lot of the semicolons with periods.
https:/
File quickstart/utils.py (right):
https:/
quickstart/
service'
any services
- 29. By Francesco Banconi
-
Changes as per review.
- 30. By Francesco Banconi
-
Fix tests.
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Deploy a bundle from a file path.
Tests: run `make check`.
QA (assuming you have an ec2 environment set up named 'ec2'):
- run `.venv/bin/python juju-quickstart /no/such/file`:
you should see the following error:
"unable to open bundle file: [Errno 2]
No such file or directory: '/no/such/file'";
- run `echo "env: no-services" > ~/invalid-
- run `.venv/bin/python juju-quickstart ~/invalid-
you should see the following error:
"invalid YAML contents: {'env': 'no-services'}";
- remove the file you created in your home:
`rm ~/invalid-
- download the bundle file from
http://
for later as '../mybundle.yaml';
- run `.venv/bin/python juju-quickstart ../mybundle.yaml`:
you should see the following error:
"multiple bundles found (exp1, exp2) but no bundle name specified";
- run `.venv/bin/python juju-quickstart ../mybundle.yaml -n exp1`:
this time the quickstart application should start normally.
- wait fr quickstart to complete: after some minutes the browser
should open on the GUI page, the bundle deployment should be
started, and the app exits without erros.
Remember to destroy your ec2 environment.
R=gary.poster
CC=
https:/
https:/
File quickstart/
https:/
quickstart/
specified')
On 2013/10/22 21:00:51, gary.poster wrote:
> I'd be tempted to change all the help to full sentences, with capital
letters
> and such, and then replace a lot of the semicolons with periods.
Done.
https:/
File quickstart/utils.py (right):
https:/
quickstart/
service'
On 2013/10/22 21:00:51, gary.poster wrote:
> any services
Done.
Francesco Banconi (frankban) wrote : | # |
Thanks Gary and Matthew!
Preview Diff
1 | === modified file 'quickstart/__init__.py' |
2 | --- quickstart/__init__.py 2013-10-18 08:05:43 +0000 |
3 | +++ quickstart/__init__.py 2013-10-22 22:03:55 +0000 |
4 | @@ -19,7 +19,7 @@ |
5 | that it can be managed using a Web interface (the Juju GUI). |
6 | """ |
7 | |
8 | -VERSION = (0, 1, 0) |
9 | +VERSION = (0, 2, 0) |
10 | |
11 | |
12 | def get_version(): |
13 | |
14 | === modified file 'quickstart/app.py' |
15 | --- quickstart/app.py 2013-10-18 14:06:25 +0000 |
16 | +++ quickstart/app.py 2013-10-22 22:03:55 +0000 |
17 | @@ -174,3 +174,20 @@ |
18 | print('{} is ready'.format(unit_name)) |
19 | return address |
20 | current_status = status |
21 | + |
22 | + |
23 | +def deploy_bundle(api_url, admin_secret, bundle_yaml, bundle_name): |
24 | + """Deploy a bundle. |
25 | + |
26 | + Receive an API URL to a WebSocket server supporting bundle deployments, the |
27 | + admin_secret to use in the authentication process, the bundle YAML encoded |
28 | + contents and the bundle name to be imported. |
29 | + |
30 | + Raise a ProgramExit if the API server returns an error response. |
31 | + """ |
32 | + env = connect(api_url, admin_secret) |
33 | + try: |
34 | + env.deploy_bundle(bundle_yaml, name=bundle_name) |
35 | + env.close() |
36 | + except jujuclient.EnvError as err: |
37 | + raise ProgramExit('bad API server response: {}'.format(err.message)) |
38 | |
39 | === modified file 'quickstart/juju.py' |
40 | --- quickstart/juju.py 2013-10-17 19:20:55 +0000 |
41 | +++ quickstart/juju.py 2013-10-22 22:03:55 +0000 |
42 | @@ -70,6 +70,18 @@ |
43 | } |
44 | return self._rpc(request) |
45 | |
46 | + def deploy_bundle(self, yaml, name=None): |
47 | + """Deploy a bundle.""" |
48 | + params = {'YAML': yaml} |
49 | + if name is not None: |
50 | + params['Name'] = name |
51 | + request = { |
52 | + 'Type': 'Deployer', |
53 | + 'Request': 'Import', |
54 | + 'Params': params, |
55 | + } |
56 | + return self._rpc(request) |
57 | + |
58 | def watch_changes(self, processor): |
59 | """Start watching the changes occurring in the Juju environment. |
60 | |
61 | |
62 | === modified file 'quickstart/manage.py' |
63 | --- quickstart/manage.py 2013-10-18 14:15:49 +0000 |
64 | +++ quickstart/manage.py 2013-10-22 22:03:55 +0000 |
65 | @@ -42,6 +42,38 @@ |
66 | parser.exit() |
67 | |
68 | |
69 | +def _validate_bundle(options, parser): |
70 | + """Validate and process the bundle options. |
71 | + |
72 | + Populate the options namespace with the following names: |
73 | + - bundle_file: the full path to the bundle file; |
74 | + - bundle_name: the name of the bundle; |
75 | + - bundle_services: a list of service names included in the bundle; |
76 | + - bundle_yaml: the YAML encoded contents of the bundle. |
77 | + |
78 | + Exit with an error if the bundle options are not valid. |
79 | + """ |
80 | + # XXX 2013-10-18 frankban: |
81 | + # This function is supposed to also support bundle URLs. |
82 | + bundle_file = os.path.abspath(os.path.expanduser(options.bundle)) |
83 | + # Load the bundle file. |
84 | + try: |
85 | + bundle_yaml = open(bundle_file).read() |
86 | + except IOError as err: |
87 | + return parser.error('unable to open bundle file: {}'.format(err)) |
88 | + # Validate the bundle. |
89 | + try: |
90 | + bundle_name, bundle_services = utils.parse_bundle( |
91 | + bundle_yaml, options.bundle_name) |
92 | + except ValueError as err: |
93 | + return parser.error(str(err)) |
94 | + # Update the options namespace with the new values. |
95 | + options.bundle_file = bundle_file |
96 | + options.bundle_name = bundle_name |
97 | + options.bundle_services = bundle_services |
98 | + options.bundle_yaml = bundle_yaml |
99 | + |
100 | + |
101 | def _validate_env(options, parser): |
102 | """Validate and process the provided environment related options. |
103 | |
104 | @@ -103,30 +135,43 @@ |
105 | """ |
106 | default_env_name = utils.get_default_env_name() |
107 | # Define the help message for the --environment option. |
108 | - env_help = 'the name of the Juju environment to use' |
109 | + env_help = 'The name of the Juju environment to use' |
110 | if default_env_name is not None: |
111 | env_help = '{} (%(default)s)'.format(env_help) |
112 | # Create and set up the arguments parser. |
113 | parser = argparse.ArgumentParser(description=quickstart.__doc__) |
114 | + # XXX 2013-10-18 frankban: |
115 | + # Make it possible to pass a URL as bundle argument. |
116 | + parser.add_argument( |
117 | + 'bundle', default=None, nargs='?', |
118 | + help='The path to the bundle file to deploy') |
119 | parser.add_argument( |
120 | '-e', '--environment', default=default_env_name, dest='env_name', |
121 | help=env_help) |
122 | parser.add_argument( |
123 | + '-n', '--bundle-name', default=None, dest='bundle_name', |
124 | + help='The name of the bundle to use. This must be included in the ' |
125 | + 'provided bundle file. Specifying the bundle name is not ' |
126 | + 'required if the bundle file only contains one bundle. This ' |
127 | + 'option is ignored if the bundle file is not specified') |
128 | + parser.add_argument( |
129 | '--environments-file', |
130 | default=os.path.join(juju_home, 'environments.yaml'), dest='env_file', |
131 | - help='the path to the Juju environments YAML file (%(default)s)') |
132 | + help='The path to the Juju environments YAML file (%(default)s)') |
133 | parser.add_argument( |
134 | '--version', action='version', version='%(prog)s {}'.format(version)) |
135 | parser.add_argument( |
136 | - '--debug', action='store_true', help='turn debug mode on') |
137 | + '--debug', action='store_true', help='Turn debug mode on') |
138 | # This is required by juju-core: see "juju help plugins". |
139 | parser.add_argument( |
140 | '--description', action=_DescriptionAction, default=argparse.SUPPRESS, |
141 | - nargs=0, help="show program's description and exit") |
142 | + nargs=0, help="Show program's description and exit") |
143 | # Parse the provided arguments. |
144 | options = parser.parse_args() |
145 | # Validate and process the provided arguments. |
146 | _validate_env(options, parser) |
147 | + if options.bundle is not None: |
148 | + _validate_bundle(options, parser) |
149 | # Set up logging. |
150 | _configure_logging(logging.DEBUG if options.debug else logging.INFO) |
151 | return options |
152 | @@ -148,6 +193,19 @@ |
153 | address = app.watch(env, 'juju-gui') |
154 | url = 'https://{}'.format(address) |
155 | print('url: {}\npassword: {}'.format(url, options.admin_secret)) |
156 | + |
157 | + # Handle bundle deployment. |
158 | + if options.bundle is not None: |
159 | + print('deploying the bundle {} with the following services: {}'.format( |
160 | + options.bundle_name, ', '.join(options.bundle_services))) |
161 | + # We need to connect to an API WebSocket server supporting bundle |
162 | + # deployments. The GUI builtin server, listening on the Juju GUI |
163 | + # address, exposes an API suitable for deploying bundles. |
164 | + gui_api_url = 'wss://{}:443/ws'.format(address) |
165 | + app.deploy_bundle( |
166 | + gui_api_url, options.admin_secret, |
167 | + options.bundle_yaml, options.bundle_name) |
168 | + |
169 | # XXX 2013-10-18 frankban: |
170 | # Add a command line option to decline opening the browser. |
171 | webbrowser.open(url) |
172 | |
173 | === modified file 'quickstart/tests/helpers.py' |
174 | --- quickstart/tests/helpers.py 2013-10-17 16:51:55 +0000 |
175 | +++ quickstart/tests/helpers.py 2013-10-22 22:03:55 +0000 |
176 | @@ -51,6 +51,32 @@ |
177 | return mock.patch('quickstart.utils.call', mock_call) |
178 | |
179 | |
180 | +class BundleFileTestsMixin(object): |
181 | + """Shared methods for testing Juju bundle files.""" |
182 | + |
183 | + valid_bundle = yaml.safe_dump({ |
184 | + 'bundle1': {'services': {'wordpress': {}, 'mysql': {}}}, |
185 | + 'bundle2': {'services': {'django': {}, 'nodejs': {}}}, |
186 | + }) |
187 | + |
188 | + def make_bundle_file(self, contents=None): |
189 | + """Create a Juju bundle file containing the given contents. |
190 | + |
191 | + If contents is None, use the valid bundle contents defined in |
192 | + self.valid_bundle. |
193 | + Return the bundle file path. |
194 | + """ |
195 | + if contents is None: |
196 | + contents = self.valid_bundle |
197 | + elif isinstance(contents, dict): |
198 | + contents = yaml.safe_dump(contents) |
199 | + bundle_file = tempfile.NamedTemporaryFile(delete=False) |
200 | + self.addCleanup(os.remove, bundle_file.name) |
201 | + bundle_file.write(contents) |
202 | + bundle_file.close() |
203 | + return bundle_file.name |
204 | + |
205 | + |
206 | class EnvFileTestsMixin(object): |
207 | """Shared methods for testing a Juju environments file.""" |
208 | |
209 | |
210 | === modified file 'quickstart/tests/test_app.py' |
211 | --- quickstart/tests/test_app.py 2013-10-18 14:07:28 +0000 |
212 | +++ quickstart/tests/test_app.py 2013-10-22 22:03:55 +0000 |
213 | @@ -431,3 +431,40 @@ |
214 | with self.assert_program_exit(expected): |
215 | app.watch(env, 'django') |
216 | mock_print.assert_has_calls([self.pending_call]) |
217 | + |
218 | + |
219 | +@mock.patch('quickstart.app.connect') |
220 | +class TestDeployBundle(ProgramExitTestsMixin, unittest.TestCase): |
221 | + |
222 | + admin_secret = 'Secret!' |
223 | + api_url = 'wss://juju-gui.example.com:443/ws' |
224 | + name = 'mybundle' |
225 | + yaml = 'mybundle: contents' |
226 | + |
227 | + def test_bundle_deployment(self, mock_connect): |
228 | + # A bundle is successfully deployed. |
229 | + app.deploy_bundle( |
230 | + self.api_url, self.admin_secret, self.yaml, self.name) |
231 | + mock_connect.assert_called_once_with(self.api_url, self.admin_secret) |
232 | + mock_connect().assert_has_calls([ |
233 | + mock.call.deploy_bundle(self.yaml, name=self.name), |
234 | + mock.call.close(), |
235 | + ]) |
236 | + |
237 | + def test_api_error(self, mock_connect): |
238 | + # A ProgramExit is raised if an error occurs in one of the API calls. |
239 | + mock_connect().deploy_bundle.side_effect = self.make_env_error( |
240 | + 'bundle deployment failure') |
241 | + expected = 'bad API server response: bundle deployment failure' |
242 | + with self.assert_program_exit(expected): |
243 | + app.deploy_bundle( |
244 | + self.api_url, self.admin_secret, self.yaml, self.name) |
245 | + |
246 | + def test_other_errors(self, mock_connect): |
247 | + # Any other errors occurred during the process are not trapped. |
248 | + error = ValueError('explode!') |
249 | + mock_connect().deploy_bundle.side_effect = error |
250 | + with self.assertRaises(ValueError) as context_manager: |
251 | + app.deploy_bundle( |
252 | + self.api_url, self.admin_secret, self.yaml, self.name) |
253 | + self.assertIs(error, context_manager.exception) |
254 | |
255 | === modified file 'quickstart/tests/test_juju.py' |
256 | --- quickstart/tests/test_juju.py 2013-10-17 15:05:11 +0000 |
257 | +++ quickstart/tests/test_juju.py 2013-10-22 22:03:55 +0000 |
258 | @@ -115,6 +115,28 @@ |
259 | expected = self.make_deploy_request(ToMachineSpec='0') |
260 | mock_rpc.assert_called_once_with(expected) |
261 | |
262 | + @mock.patch('quickstart.juju.Environment._rpc') |
263 | + def test_deploy_bundle(self, mock_rpc): |
264 | + # The deploy bundle call is properly generated. |
265 | + self.env.deploy_bundle('name: contents') |
266 | + expected = { |
267 | + 'Type': 'Deployer', |
268 | + 'Request': 'Import', |
269 | + 'Params': {'YAML': 'name: contents'}, |
270 | + } |
271 | + mock_rpc.assert_called_once_with(expected) |
272 | + |
273 | + @mock.patch('quickstart.juju.Environment._rpc') |
274 | + def test_deploy_bundle_with_name(self, mock_rpc): |
275 | + # The deploy bundle call is properly generated when passing a name. |
276 | + self.env.deploy_bundle('name: contents', name='name') |
277 | + expected = { |
278 | + 'Type': 'Deployer', |
279 | + 'Request': 'Import', |
280 | + 'Params': {'Name': 'name', 'YAML': 'name: contents'}, |
281 | + } |
282 | + mock_rpc.assert_called_once_with(expected) |
283 | + |
284 | @mock.patch('quickstart.juju.jujuclient.Watcher._rpc') |
285 | def test_watch_changes(self, mock_rpc): |
286 | # It is possible to watch for changes using a processor callable. |
287 | |
288 | === modified file 'quickstart/tests/test_manage.py' |
289 | --- quickstart/tests/test_manage.py 2013-10-16 15:23:08 +0000 |
290 | +++ quickstart/tests/test_manage.py 2013-10-22 22:03:55 +0000 |
291 | @@ -46,6 +46,60 @@ |
292 | mock_exit.assert_called_once_with(0) |
293 | |
294 | |
295 | +class TestValidateBundle(helpers.BundleFileTestsMixin, unittest.TestCase): |
296 | + |
297 | + def setUp(self): |
298 | + self.parser = mock.Mock() |
299 | + |
300 | + def make_options(self, bundle, bundle_name=None): |
301 | + """Return a mock options object which includes the passed arguments.""" |
302 | + return mock.Mock(bundle=bundle, bundle_name=bundle_name) |
303 | + |
304 | + def test_resulting_options(self): |
305 | + # The options object is correctly set up. |
306 | + bundle_file = self.make_bundle_file() |
307 | + options = self.make_options(bundle_file, bundle_name='bundle1') |
308 | + manage._validate_bundle(options, self.parser) |
309 | + self.assertEqual(bundle_file, options.bundle_file) |
310 | + self.assertEqual('bundle1', options.bundle_name) |
311 | + self.assertEqual( |
312 | + ['mysql', 'wordpress'], sorted(options.bundle_services)) |
313 | + self.assertEqual(open(bundle_file).read(), options.bundle_yaml) |
314 | + |
315 | + def test_expand_user(self): |
316 | + # The ~ construct is correctly expanded in the validation process. |
317 | + bundle_file = self.make_bundle_file() |
318 | + # Split the full path of the bundle file, e.g. from a full |
319 | + # "/tmp/bundle.file" path retrieve the base path "/tmp" and the file |
320 | + # name "bundle.file". By doing that we can simulate that the user's |
321 | + # home is "/tmp" and that the bundle file is "~/bundle.file". |
322 | + base_path, filename = os.path.split(bundle_file) |
323 | + path = '~/{}'.format(filename) |
324 | + options = self.make_options(bundle=path, bundle_name='bundle2') |
325 | + with mock.patch('os.environ', {'HOME': base_path}): |
326 | + manage._validate_bundle(options, self.parser) |
327 | + self.assertEqual(bundle_file, options.bundle_file) |
328 | + |
329 | + def test_bundle_file_not_found(self): |
330 | + # A parser error is invoked if the bundle file is not found. |
331 | + options = self.make_options('/no/such/file.yaml') |
332 | + manage._validate_bundle(options, self.parser) |
333 | + expected = ( |
334 | + 'unable to open bundle file: ' |
335 | + "[Errno 2] No such file or directory: '/no/such/file.yaml'" |
336 | + ) |
337 | + self.parser.error.assert_called_once_with(expected) |
338 | + |
339 | + def test_error_parsing_bundle_file(self): |
340 | + # A parser error is invoked if an error occurs parsing the bundle file. |
341 | + bundle_file = self.make_bundle_file() |
342 | + options = self.make_options(bundle_file, bundle_name='no-such') |
343 | + manage._validate_bundle(options, self.parser) |
344 | + expected = ('bundle no-such not found in the provided list of bundles ' |
345 | + '(bundle1, bundle2)') |
346 | + self.parser.error.assert_called_once_with(expected) |
347 | + |
348 | + |
349 | class TestValidateEnv(helpers.EnvFileTestsMixin, unittest.TestCase): |
350 | |
351 | def setUp(self): |
352 | @@ -147,7 +201,7 @@ |
353 | self.assertIn(quickstart.__doc__, output) |
354 | self.assertIn('--environment', output) |
355 | # Without a default environment, the -e option has no default. |
356 | - self.assertIn('the name of the Juju environment to use\n', output) |
357 | + self.assertIn('The name of the Juju environment to use\n', output) |
358 | |
359 | def test_help_with_default_environment(self): |
360 | # The program help message is properly formatted when a default Juju |
361 | @@ -158,7 +212,7 @@ |
362 | self.assertTrue(stdout_write.called) |
363 | # Retrieve the output from the mock call. |
364 | output = stdout_write.call_args[0][0] |
365 | - self.assertIn('the name of the Juju environment to use (hp)\n', output) |
366 | + self.assertIn('The name of the Juju environment to use (hp)\n', output) |
367 | |
368 | def test_description(self): |
369 | # The program description is properly printed out as required by juju. |
370 | @@ -173,6 +227,15 @@ |
371 | expected = 'juju-quickstart {}\n'.format(quickstart.get_version()) |
372 | mock_stderr.write.assert_called_once_with(expected) |
373 | |
374 | + @mock.patch('quickstart.manage._validate_bundle') |
375 | + def test_bundle(self, mock_validate_bundle): |
376 | + # The bundle validation process is started if a bundle is provided. |
377 | + self.call_setup(['/path/to/bundle.file'], exit_called=False) |
378 | + self.assertTrue(mock_validate_bundle.called) |
379 | + options, parser = mock_validate_bundle.call_args_list[0][0] |
380 | + self.assertIsInstance(options, argparse.Namespace) |
381 | + self.assertIsInstance(parser, argparse.ArgumentParser) |
382 | + |
383 | def test_configure_logging(self): |
384 | # Logging is properly set up at the info level. |
385 | logger = logging.getLogger() |
386 | @@ -184,3 +247,46 @@ |
387 | logger = logging.getLogger() |
388 | self.call_setup(['--debug'], 'ec2', exit_called=False) |
389 | self.assertEqual(logging.DEBUG, logger.level) |
390 | + |
391 | + |
392 | +@mock.patch('webbrowser.open') |
393 | +@mock.patch('quickstart.manage.app') |
394 | +@mock.patch('__builtin__.print', mock.Mock()) |
395 | +class TestRun(unittest.TestCase): |
396 | + |
397 | + def make_options(self, **kwargs): |
398 | + """Set up the options to be passed to the run function.""" |
399 | + options = { |
400 | + 'admin_secret': 'Secret!', |
401 | + 'bundle': None, |
402 | + 'env_name': 'aws', |
403 | + 'env_type': 'ec2', |
404 | + } |
405 | + options.update(kwargs) |
406 | + return mock.Mock(**options) |
407 | + |
408 | + def test_no_bundle(self, mock_app, mock_open): |
409 | + # The application runs correctly if no bundle is provided. |
410 | + options = self.make_options() |
411 | + manage.run(options) |
412 | + mock_app.bootstrap.assert_called_once_with(options.env_name) |
413 | + mock_app.get_api_url.assert_called_once_with(options.env_name) |
414 | + mock_app.connect.assert_called_once_with( |
415 | + mock_app.get_api_url(), options.admin_secret) |
416 | + mock_app.deploy_gui.assert_called_once_with( |
417 | + mock_app.connect(), 'juju-gui') |
418 | + mock_app.watch.assert_called_once_with(mock_app.connect(), 'juju-gui') |
419 | + mock_open.assert_called_once_with( |
420 | + 'https://{}'.format(mock_app.watch())) |
421 | + self.assertFalse(mock_app.deploy_bundle.called) |
422 | + |
423 | + def test_bundle(self, mock_app, mock_open): |
424 | + # A bundle is correctly deployed by the application. |
425 | + options = self.make_options( |
426 | + bundle='/my/bunlde/file.yaml', bundle_yaml='mybundle: contents', |
427 | + bundle_name='mybundle', bundle_services=['service1', 'service2']) |
428 | + mock_app.watch.return_value = 'gui.example.com' |
429 | + manage.run(options) |
430 | + mock_app.deploy_bundle.assert_called_once_with( |
431 | + 'wss://gui.example.com:443/ws', options.admin_secret, |
432 | + 'mybundle: contents', 'mybundle') |
433 | |
434 | === modified file 'quickstart/tests/test_utils.py' |
435 | --- quickstart/tests/test_utils.py 2013-10-17 19:56:17 +0000 |
436 | +++ quickstart/tests/test_utils.py 2013-10-22 22:03:55 +0000 |
437 | @@ -120,6 +120,94 @@ |
438 | mock_call.assert_called_once_with('juju', 'switch') |
439 | |
440 | |
441 | +class TestParseBundle( |
442 | + helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin, |
443 | + unittest.TestCase): |
444 | + |
445 | + def test_invalid_yaml(self): |
446 | + # A ValueError is raised if the bundle contents are not a valid YAML. |
447 | + with self.assertRaises(ValueError) as context_manager: |
448 | + utils.parse_bundle(':') |
449 | + expected = 'unable to parse the bundle' |
450 | + self.assertIn(expected, str(context_manager.exception)) |
451 | + |
452 | + def test_yaml_invalid_type(self): |
453 | + # A ValueError is raised if the bundle contents are not well formed. |
454 | + with self.assert_value_error("invalid YAML contents: 'a-string'"): |
455 | + utils.parse_bundle('a-string') |
456 | + |
457 | + def test_yaml_invalid_bundle_data(self): |
458 | + # A ValueError is raised if bundles are not well formed. |
459 | + contents = yaml.safe_dump({'mybundle': 'not valid'}) |
460 | + expected = "invalid YAML contents: {'mybundle': 'not valid'}" |
461 | + with self.assert_value_error(expected): |
462 | + utils.parse_bundle(contents) |
463 | + |
464 | + def test_yaml_no_service(self): |
465 | + # A ValueError is raised if bundles do not include services. |
466 | + contents = yaml.safe_dump({'mybundle': {}}) |
467 | + expected = "invalid YAML contents: {'mybundle': {}}" |
468 | + with self.assert_value_error(expected): |
469 | + utils.parse_bundle(contents) |
470 | + |
471 | + def test_yaml_none_bundle_services(self): |
472 | + # A ValueError is raised if services are None. |
473 | + contents = yaml.safe_dump({'mybundle': {'services': None}}) |
474 | + expected = "invalid YAML contents: {'mybundle': {'services': None}}" |
475 | + with self.assert_value_error(expected): |
476 | + utils.parse_bundle(contents) |
477 | + |
478 | + def test_yaml_invalid_bundle_services_type(self): |
479 | + # A ValueError is raised if services have an invalid type. |
480 | + contents = yaml.safe_dump({'mybundle': {'services': 42}}) |
481 | + expected = "invalid YAML contents: {'mybundle': {'services': 42}}" |
482 | + with self.assert_value_error(expected): |
483 | + utils.parse_bundle(contents) |
484 | + |
485 | + def test_yaml_no_bundles(self): |
486 | + # A ValueError is raised if the bundle contents are empty. |
487 | + with self.assert_value_error('no bundles found'): |
488 | + utils.parse_bundle(yaml.safe_dump({})) |
489 | + |
490 | + def test_bundle_name_not_specified(self): |
491 | + # A ValueError is raised if the bundle name is not specified and the |
492 | + # contents contain more than one bundle. |
493 | + expected = ('multiple bundles found (bundle1, bundle2) ' |
494 | + 'but no bundle name specified') |
495 | + with self.assert_value_error(expected): |
496 | + utils.parse_bundle(self.valid_bundle) |
497 | + |
498 | + def test_bundle_name_not_found(self): |
499 | + # A ValueError is raised if the given bundle is not found in the file. |
500 | + expected = ('bundle no-such not found in the provided list of bundles ' |
501 | + '(bundle1, bundle2)') |
502 | + with self.assert_value_error(expected): |
503 | + utils.parse_bundle(self.valid_bundle, 'no-such') |
504 | + |
505 | + def test_no_services(self): |
506 | + # A ValueError is raised if the specified bundle does not contain |
507 | + # services. |
508 | + contents = yaml.safe_dump({'mybundle': {'services': {}}}) |
509 | + expected = 'bundle mybundle does not include any services' |
510 | + with self.assert_value_error(expected): |
511 | + utils.parse_bundle(contents) |
512 | + |
513 | + def test_success_no_name(self): |
514 | + # The function succeeds when an implicit bundle name is used. |
515 | + contents = yaml.safe_dump({ |
516 | + 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}}, |
517 | + }) |
518 | + name, services = utils.parse_bundle(contents) |
519 | + self.assertEqual('mybundle', name) |
520 | + self.assertEqual(['mysql', 'wordpress'], sorted(services)) |
521 | + |
522 | + def test_success_multiple_bundles(self): |
523 | + # The function succeeds with multiple bundles. |
524 | + name, services = utils.parse_bundle(self.valid_bundle, 'bundle2') |
525 | + self.assertEqual('bundle2', name) |
526 | + self.assertEqual(['django', 'nodejs'], sorted(services)) |
527 | + |
528 | + |
529 | class TestParseEnvFile( |
530 | helpers.EnvFileTestsMixin, helpers.ValueErrorTestsMixin, |
531 | unittest.TestCase): |
532 | |
533 | === modified file 'quickstart/utils.py' |
534 | --- quickstart/utils.py 2013-10-18 10:10:30 +0000 |
535 | +++ quickstart/utils.py 2013-10-22 22:03:55 +0000 |
536 | @@ -16,6 +16,7 @@ |
537 | |
538 | """Juju Quickstart utility functions and classes.""" |
539 | |
540 | +import collections |
541 | import re |
542 | import os |
543 | import logging |
544 | @@ -80,6 +81,59 @@ |
545 | return match.groups()[0] |
546 | |
547 | |
548 | +def parse_bundle(bundle_yaml, bundle_name=None): |
549 | + """Parse the provided bundle YAML decoded contents. |
550 | + |
551 | + Return a tuple containing the bundle name and the list of services included |
552 | + in the bundle. |
553 | + |
554 | + Raise a ValueError if: |
555 | + - the bundle YAML contents are not parsable by YAML; |
556 | + - the YAML contents are not properly structured; |
557 | + - the bundle name is specified but not included in the bundle file; |
558 | + - the bundle name is not specified and the bundle file includes more than |
559 | + one bundle; |
560 | + - the bundle does not include services. |
561 | + """ |
562 | + # Parse the bundle file. |
563 | + try: |
564 | + bundles = yaml.safe_load(bundle_yaml) |
565 | + except Exception as err: |
566 | + msg = 'unable to parse the bundle: {}'.format(err) |
567 | + raise ValueError(msg) |
568 | + # Ensure the bundle file is well formed and contains at least one bundle. |
569 | + if not isinstance(bundles, collections.Mapping): |
570 | + raise ValueError('invalid YAML contents: {!r}'.format(bundles)) |
571 | + try: |
572 | + name_services_map = dict( |
573 | + (key, value['services'].keys()) |
574 | + for key, value in bundles.items() |
575 | + ) |
576 | + except (AttributeError, KeyError, TypeError): |
577 | + raise ValueError('invalid YAML contents: {!r}'.format(bundles)) |
578 | + if not name_services_map: |
579 | + raise ValueError('no bundles found') |
580 | + # Retrieve the bundle name and services. |
581 | + if bundle_name is None: |
582 | + if len(name_services_map) > 1: |
583 | + msg = 'multiple bundles found ({}) but no bundle name specified' |
584 | + bundle_names = ', '.join(sorted(name_services_map.keys())) |
585 | + raise ValueError(msg.format(bundle_names)) |
586 | + bundle_name, bundle_services = name_services_map.items()[0] |
587 | + else: |
588 | + bundle_services = name_services_map.get(bundle_name) |
589 | + if bundle_services is None: |
590 | + msg = 'bundle {} not found in the provided list of bundles ({})' |
591 | + bundle_names = ', '.join(sorted(name_services_map.keys())) |
592 | + raise ValueError(msg.format(bundle_name, bundle_names)) |
593 | + if not bundle_services: |
594 | + msg = 'bundle {} does not include any services'.format(bundle_name) |
595 | + raise ValueError(msg) |
596 | + # XXX 2013-10-21 frankban: |
597 | + # Handle the case when the GUI is included in the bundle. |
598 | + return bundle_name, bundle_services |
599 | + |
600 | + |
601 | def parse_env_file(env_file, env_name): |
602 | """Parse the provided Juju environments.yaml file. |
603 |
Reviewers: mp+192194_ code.launchpad. net,
Message:
Please take a look.
Description:
Deploy a bundle from a file path.
Tests: run `make check`. bundle. yaml`; bundle. yaml`: bundle. yaml`; pastebin. ubuntu. com/6283832/ and save it
QA (assuming you have an ec2 environment set up named 'ec2'):
- run `.venv/bin/python juju-quickstart /no/such/file`:
you should see the following error:
"unable to open bundle file: [Errno 2]
No such file or directory: '/no/such/file'";
- run `echo "env: no-services" > ~/invalid-
- run `.venv/bin/python juju-quickstart ~/invalid-
you should see the following error:
"invalid YAML contents: {'env': 'no-services'}";
- remove the file you created in your home:
`rm ~/invalid-
- download the bundle file from
http://
for later as '../mybundle.yaml';
- run `.venv/bin/python juju-quickstart ../mybundle.yaml`:
you should see the following error:
"multiple bundles found (exp1, exp2) but no bundle name specified";
- run `.venv/bin/python juju-quickstart ../mybundle.yaml -n exp1`:
this time the quickstart application should start normally.
- wait fr quickstart to complete: after some minutes the browser
should open on the GUI page, the bundle deployment should be
started, and the app exits without erros.
Remember to destroy your ec2 environment.
https:/ /code.launchpad .net/~frankban/ juju-quickstart /quickstart- bundle- file/+merge/ 192194
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/15880043/
Affected files (+423, -1 lines): __init_ _.py manage. py tests/helpers. py tests/test_ app.py tests/test_ juju.py tests/test_ manage. py tests/test_ utils.py
A [revision details]
M quickstart/
M quickstart/app.py
M quickstart/juju.py
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/utils.py