Merge lp:~frankban/juju-quickstart/open-urls into lp:juju-quickstart
- open-urls
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 9 |
Proposed branch: | lp:~frankban/juju-quickstart/open-urls |
Merge into: | lp:juju-quickstart |
Diff against target: |
445 lines (+220/-35) 7 files modified
quickstart/app.py (+12/-4) quickstart/manage.py (+15/-13) quickstart/tests/helpers.py (+17/-0) quickstart/tests/test_app.py (+38/-11) quickstart/tests/test_manage.py (+32/-7) quickstart/tests/test_utils.py (+73/-0) quickstart/utils.py (+33/-0) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/open-urls |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email:
|
Commit message
Description of the change
Deploy a bundle from a HTTP(S) url.
The bundle argument can be a file path or
a http/https URL pointing to a bundle YAML.
Also get the URL of the last Juju GUI charm
revision from charmworld (API 2 for now).
Tests: `make check`.
QA:
I copied a bundle YAML over here:
http://
You should be able to deploy that bundle
by running the following (after juju switching to ec2 or similar):
`.venv/bin/python juju-quickstart http://
Remember to destroy your ec2 environment...
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
Code LGTM with trivial update. Trying QA now.
https:/
File quickstart/app.py (right):
https:/
quickstart/
nice.
https:/
File quickstart/
https:/
quickstart/
self.patch_
You scared me.
https:/
File quickstart/utils.py (right):
https:/
quickstart/
is fixed.
We had a fix and a deployment, and
http://
(yay!)
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Gary Poster (gary) wrote : | # |
Go you and your bad 100% test coverage self.
qa good (though EC2 left a bunch of services in pending state and the
haproxy and daisy charms were DOA :-/ )
Thank you!
- 11. By Francesco Banconi
-
Changes as per review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Deploy a bundle from a HTTP(S) url.
The bundle argument can be a file path or
a http/https URL pointing to a bundle YAML.
Also get the URL of the last Juju GUI charm
revision from charmworld (API 2 for now).
Tests: `make check`.
QA:
I copied a bundle YAML over here:
http://
You should be able to deploy that bundle
by running the following (after juju switching to ec2 or similar):
`.venv/bin/python juju-quickstart http://
Remember to destroy your ec2 environment...
R=gary.poster
CC=
https:/
https:/
File quickstart/utils.py (right):
https:/
quickstart/
is fixed.
On 2013/10/30 20:58:55, gary.poster wrote:
> We had a fix and a deployment, and
> http://
(yay!)
Cool! Done.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Francesco Banconi (frankban) wrote : | # |
Thanks for the review Gary!
Preview Diff
1 | === modified file 'quickstart/app.py' | |||
2 | --- quickstart/app.py 2013-10-30 10:16:36 +0000 | |||
3 | +++ quickstart/app.py 2013-10-31 08:49:45 +0000 | |||
4 | @@ -19,6 +19,7 @@ | |||
5 | 19 | from __future__ import print_function | 19 | from __future__ import print_function |
6 | 20 | import functools | 20 | import functools |
7 | 21 | import json | 21 | import json |
8 | 22 | import logging | ||
9 | 22 | 23 | ||
10 | 23 | import jujuclient | 24 | import jujuclient |
11 | 24 | 25 | ||
12 | @@ -28,6 +29,11 @@ | |||
13 | 28 | ) | 29 | ) |
14 | 29 | 30 | ||
15 | 30 | 31 | ||
16 | 32 | # The default Juju GUI charm URL to use when it is not possible to retrieve it | ||
17 | 33 | # from the charmworld API, e.g. due to temporary connection/charmworld errors. | ||
18 | 34 | DEFAULT_CHARM_URL = 'cs:precise/juju-gui-78' | ||
19 | 35 | |||
20 | 36 | |||
21 | 31 | class ProgramExit(Exception): | 37 | class ProgramExit(Exception): |
22 | 32 | """An error occurred while setting up the Juju environment. | 38 | """An error occurred while setting up the Juju environment. |
23 | 33 | 39 | ||
24 | @@ -113,10 +119,12 @@ | |||
25 | 113 | 119 | ||
26 | 114 | Raise a ProgramExit if the API server returns an error response. | 120 | Raise a ProgramExit if the API server returns an error response. |
27 | 115 | """ | 121 | """ |
32 | 116 | # XXX 2013-10-17 frankban: | 122 | try: |
33 | 117 | # Retrieve the URL of the last charm revision from | 123 | charm_url = utils.get_charm_url() |
34 | 118 | # manage.jujucharms.com. | 124 | except (IOError, ValueError) as err: |
35 | 119 | charm_url = 'cs:precise/juju-gui-78' | 125 | msg = 'unable to retrieve the Juju GUI charm URL from the API: {}' |
36 | 126 | logging.warn(msg.format(err)) | ||
37 | 127 | charm_url = DEFAULT_CHARM_URL | ||
38 | 120 | try: | 128 | try: |
39 | 121 | env.deploy(service_name, charm_url, to=0) | 129 | env.deploy(service_name, charm_url, to=0) |
40 | 122 | env.expose(service_name) | 130 | env.expose(service_name) |
41 | 123 | 131 | ||
42 | === modified file 'quickstart/manage.py' | |||
43 | --- quickstart/manage.py 2013-10-30 10:16:36 +0000 | |||
44 | +++ quickstart/manage.py 2013-10-31 08:49:45 +0000 | |||
45 | @@ -46,21 +46,26 @@ | |||
46 | 46 | """Validate and process the bundle options. | 46 | """Validate and process the bundle options. |
47 | 47 | 47 | ||
48 | 48 | Populate the options namespace with the following names: | 48 | Populate the options namespace with the following names: |
49 | 49 | - bundle_file: the full path to the bundle file; | ||
50 | 50 | - bundle_name: the name of the bundle; | 49 | - bundle_name: the name of the bundle; |
51 | 51 | - bundle_services: a list of service names included in the bundle; | 50 | - bundle_services: a list of service names included in the bundle; |
52 | 52 | - bundle_yaml: the YAML encoded contents of the bundle. | 51 | - bundle_yaml: the YAML encoded contents of the bundle. |
53 | 53 | 52 | ||
54 | 54 | Exit with an error if the bundle options are not valid. | 53 | Exit with an error if the bundle options are not valid. |
55 | 55 | """ | 54 | """ |
64 | 56 | # XXX 2013-10-18 frankban: | 55 | bundle = options.bundle |
65 | 57 | # This function is supposed to also support bundle URLs. | 56 | if bundle.startswith('http://') or bundle.startswith('https://'): |
66 | 58 | bundle_file = os.path.abspath(os.path.expanduser(options.bundle)) | 57 | # Load the bundle URL. |
67 | 59 | # Load the bundle file. | 58 | try: |
68 | 60 | try: | 59 | bundle_yaml = utils.urlread(bundle) |
69 | 61 | bundle_yaml = open(bundle_file).read() | 60 | except IOError as err: |
70 | 62 | except IOError as err: | 61 | return parser.error('unable to open bundle URL: {}'.format(err)) |
71 | 63 | return parser.error('unable to open bundle file: {}'.format(err)) | 62 | else: |
72 | 63 | # Load the bundle file. | ||
73 | 64 | bundle_file = os.path.abspath(os.path.expanduser(bundle)) | ||
74 | 65 | try: | ||
75 | 66 | bundle_yaml = open(bundle_file).read() | ||
76 | 67 | except IOError as err: | ||
77 | 68 | return parser.error('unable to open bundle file: {}'.format(err)) | ||
78 | 64 | # Validate the bundle. | 69 | # Validate the bundle. |
79 | 65 | try: | 70 | try: |
80 | 66 | bundle_name, bundle_services = utils.parse_bundle( | 71 | bundle_name, bundle_services = utils.parse_bundle( |
81 | @@ -68,7 +73,6 @@ | |||
82 | 68 | except ValueError as err: | 73 | except ValueError as err: |
83 | 69 | return parser.error(str(err)) | 74 | return parser.error(str(err)) |
84 | 70 | # Update the options namespace with the new values. | 75 | # Update the options namespace with the new values. |
85 | 71 | options.bundle_file = bundle_file | ||
86 | 72 | options.bundle_name = bundle_name | 76 | options.bundle_name = bundle_name |
87 | 73 | options.bundle_services = bundle_services | 77 | options.bundle_services = bundle_services |
88 | 74 | options.bundle_yaml = bundle_yaml | 78 | options.bundle_yaml = bundle_yaml |
89 | @@ -140,11 +144,9 @@ | |||
90 | 140 | env_help = '{} (%(default)s)'.format(env_help) | 144 | env_help = '{} (%(default)s)'.format(env_help) |
91 | 141 | # Create and set up the arguments parser. | 145 | # Create and set up the arguments parser. |
92 | 142 | parser = argparse.ArgumentParser(description=quickstart.__doc__) | 146 | parser = argparse.ArgumentParser(description=quickstart.__doc__) |
93 | 143 | # XXX 2013-10-18 frankban: | ||
94 | 144 | # Make it possible to pass a URL as bundle argument. | ||
95 | 145 | parser.add_argument( | 147 | parser.add_argument( |
96 | 146 | 'bundle', default=None, nargs='?', | 148 | 'bundle', default=None, nargs='?', |
98 | 147 | help='The path to the bundle file to deploy') | 149 | help='The bundle URL or the path to the bundle file to deploy') |
99 | 148 | parser.add_argument( | 150 | parser.add_argument( |
100 | 149 | '-e', '--environment', default=default_env_name, dest='env_name', | 151 | '-e', '--environment', default=default_env_name, dest='env_name', |
101 | 150 | help=env_help) | 152 | help=env_help) |
102 | 151 | 153 | ||
103 | === modified file 'quickstart/tests/helpers.py' | |||
104 | --- quickstart/tests/helpers.py 2013-10-30 10:16:36 +0000 | |||
105 | +++ quickstart/tests/helpers.py 2013-10-31 08:49:45 +0000 | |||
106 | @@ -100,6 +100,23 @@ | |||
107 | 100 | return env_file.name | 100 | return env_file.name |
108 | 101 | 101 | ||
109 | 102 | 102 | ||
110 | 103 | class UrlReadTestsMixin(object): | ||
111 | 104 | """Expose a method to mock the quickstart.utils.urlread helper function.""" | ||
112 | 105 | |||
113 | 106 | def patch_urlread(self, contents=None, error=False): | ||
114 | 107 | """Patch the quickstart.utils.urlread helper function. | ||
115 | 108 | |||
116 | 109 | If contents is not None, urlread() will return the provided contents. | ||
117 | 110 | If error is set to True, an IOError will be simulated. | ||
118 | 111 | """ | ||
119 | 112 | mock_urlread = mock.Mock() | ||
120 | 113 | if contents is not None: | ||
121 | 114 | mock_urlread.return_value = contents | ||
122 | 115 | if error: | ||
123 | 116 | mock_urlread.side_effect = IOError('bad wolf') | ||
124 | 117 | return mock.patch('quickstart.utils.urlread', mock_urlread) | ||
125 | 118 | |||
126 | 119 | |||
127 | 103 | class ValueErrorTestsMixin(object): | 120 | class ValueErrorTestsMixin(object): |
128 | 104 | """Set up some base methods for testing functions raising ValueErrors.""" | 121 | """Set up some base methods for testing functions raising ValueErrors.""" |
129 | 105 | 122 | ||
130 | 106 | 123 | ||
131 | === modified file 'quickstart/tests/test_app.py' | |||
132 | --- quickstart/tests/test_app.py 2013-10-30 10:16:36 +0000 | |||
133 | +++ quickstart/tests/test_app.py 2013-10-31 08:49:45 +0000 | |||
134 | @@ -235,12 +235,37 @@ | |||
135 | 235 | 235 | ||
136 | 236 | class TestDeployGui(ProgramExitTestsMixin, unittest.TestCase): | 236 | class TestDeployGui(ProgramExitTestsMixin, unittest.TestCase): |
137 | 237 | 237 | ||
138 | 238 | charm_url = 'cs:precise/juju-gui-42' | ||
139 | 239 | |||
140 | 240 | def patch_get_charm_url(self, side_effect=None): | ||
141 | 241 | """Patch the get_charm_url helper function.""" | ||
142 | 242 | if side_effect is None: | ||
143 | 243 | side_effect = [self.charm_url] | ||
144 | 244 | mock_get_charm_url = mock.Mock(side_effect=side_effect) | ||
145 | 245 | return mock.patch('quickstart.utils.get_charm_url', mock_get_charm_url) | ||
146 | 246 | |||
147 | 238 | def test_deployment(self): | 247 | def test_deployment(self): |
153 | 239 | # The function correctly deploys and exposes the service. | 248 | # The function correctly deploys and exposes the service, retrieving |
154 | 240 | env = mock.Mock() | 249 | # the charm URL from the charmworld API. |
155 | 241 | app.deploy_gui(env, 'my-gui') | 250 | env = mock.Mock() |
156 | 242 | env.assert_has_calls([ | 251 | with self.patch_get_charm_url(): |
157 | 243 | mock.call.deploy('my-gui', 'cs:precise/juju-gui-78', to=0), | 252 | app.deploy_gui(env, 'my-gui') |
158 | 253 | env.assert_has_calls([ | ||
159 | 254 | mock.call.deploy('my-gui', 'cs:precise/juju-gui-42', to=0), | ||
160 | 255 | mock.call.expose('my-gui') | ||
161 | 256 | ]) | ||
162 | 257 | |||
163 | 258 | def test_deployment_default_charm_url(self): | ||
164 | 259 | # The function correctly deploys and exposes the service, even if it is | ||
165 | 260 | # not able to retrieve the charm URL from the charmworld API. | ||
166 | 261 | env = mock.Mock() | ||
167 | 262 | log = 'unable to retrieve the Juju GUI charm URL from the API: boo!' | ||
168 | 263 | with self.patch_get_charm_url(side_effect=IOError('boo!')): | ||
169 | 264 | # A warning is logged which notifies we are using the default URL. | ||
170 | 265 | with helpers.assert_logs([log], level='warn'): | ||
171 | 266 | app.deploy_gui(env, 'my-gui') | ||
172 | 267 | env.assert_has_calls([ | ||
173 | 268 | mock.call.deploy('my-gui', app.DEFAULT_CHARM_URL, to=0), | ||
174 | 244 | mock.call.expose('my-gui') | 269 | mock.call.expose('my-gui') |
175 | 245 | ]) | 270 | ]) |
176 | 246 | 271 | ||
177 | @@ -249,20 +274,22 @@ | |||
178 | 249 | env = mock.Mock() | 274 | env = mock.Mock() |
179 | 250 | env.deploy.side_effect = self.make_env_error('service already exists') | 275 | env.deploy.side_effect = self.make_env_error('service already exists') |
180 | 251 | expected = 'bad API server response: service already exists' | 276 | expected = 'bad API server response: service already exists' |
183 | 252 | with self.assert_program_exit(expected): | 277 | with self.patch_get_charm_url(): |
184 | 253 | app.deploy_gui(env, 'another-gui') | 278 | with self.assert_program_exit(expected): |
185 | 279 | app.deploy_gui(env, 'another-gui') | ||
186 | 254 | env.deploy.assert_called_once_with( | 280 | env.deploy.assert_called_once_with( |
188 | 255 | 'another-gui', 'cs:precise/juju-gui-78', to=0) | 281 | 'another-gui', 'cs:precise/juju-gui-42', to=0) |
189 | 256 | 282 | ||
190 | 257 | def test_other_errors(self): | 283 | def test_other_errors(self): |
191 | 258 | # Any other errors occurred during the process are not trapped. | 284 | # Any other errors occurred during the process are not trapped. |
192 | 259 | error = ValueError('explode!') | 285 | error = ValueError('explode!') |
193 | 260 | env = mock.Mock() | 286 | env = mock.Mock() |
194 | 261 | env.expose.side_effect = error | 287 | env.expose.side_effect = error |
197 | 262 | with self.assertRaises(ValueError) as context_manager: | 288 | with self.patch_get_charm_url(): |
198 | 263 | app.deploy_gui(env, 'juju-gui') | 289 | with self.assertRaises(ValueError) as context_manager: |
199 | 290 | app.deploy_gui(env, 'juju-gui') | ||
200 | 264 | env.deploy.assert_called_once_with( | 291 | env.deploy.assert_called_once_with( |
202 | 265 | 'juju-gui', 'cs:precise/juju-gui-78', to=0) | 292 | 'juju-gui', 'cs:precise/juju-gui-42', to=0) |
203 | 266 | env.expose.assert_called_once_with('juju-gui') | 293 | env.expose.assert_called_once_with('juju-gui') |
204 | 267 | self.assertIs(error, context_manager.exception) | 294 | self.assertIs(error, context_manager.exception) |
205 | 268 | 295 | ||
206 | 269 | 296 | ||
207 | === modified file 'quickstart/tests/test_manage.py' | |||
208 | --- quickstart/tests/test_manage.py 2013-10-30 10:16:36 +0000 | |||
209 | +++ quickstart/tests/test_manage.py 2013-10-31 08:49:45 +0000 | |||
210 | @@ -46,7 +46,9 @@ | |||
211 | 46 | mock_exit.assert_called_once_with(0) | 46 | mock_exit.assert_called_once_with(0) |
212 | 47 | 47 | ||
213 | 48 | 48 | ||
215 | 49 | class TestValidateBundle(helpers.BundleFileTestsMixin, unittest.TestCase): | 49 | class TestValidateBundle( |
216 | 50 | helpers.BundleFileTestsMixin, helpers.UrlReadTestsMixin, | ||
217 | 51 | unittest.TestCase): | ||
218 | 50 | 52 | ||
219 | 51 | def setUp(self): | 53 | def setUp(self): |
220 | 52 | self.parser = mock.Mock() | 54 | self.parser = mock.Mock() |
221 | @@ -55,12 +57,24 @@ | |||
222 | 55 | """Return a mock options object which includes the passed arguments.""" | 57 | """Return a mock options object which includes the passed arguments.""" |
223 | 56 | return mock.Mock(bundle=bundle, bundle_name=bundle_name) | 58 | return mock.Mock(bundle=bundle, bundle_name=bundle_name) |
224 | 57 | 59 | ||
227 | 58 | def test_resulting_options(self): | 60 | def test_resulting_options_from_file(self): |
228 | 59 | # The options object is correctly set up. | 61 | # The options object is correctly set up when a bundle file is passed. |
229 | 60 | bundle_file = self.make_bundle_file() | 62 | bundle_file = self.make_bundle_file() |
230 | 61 | options = self.make_options(bundle_file, bundle_name='bundle1') | 63 | options = self.make_options(bundle_file, bundle_name='bundle1') |
231 | 62 | manage._validate_bundle(options, self.parser) | 64 | manage._validate_bundle(options, self.parser) |
233 | 63 | self.assertEqual(bundle_file, options.bundle_file) | 65 | self.assertEqual('bundle1', options.bundle_name) |
234 | 66 | self.assertEqual( | ||
235 | 67 | ['mysql', 'wordpress'], sorted(options.bundle_services)) | ||
236 | 68 | self.assertEqual(open(bundle_file).read(), options.bundle_yaml) | ||
237 | 69 | |||
238 | 70 | def test_resulting_options_from_url(self): | ||
239 | 71 | # The options object is correctly set up when a bundle URL is passed. | ||
240 | 72 | bundle_file = self.make_bundle_file() | ||
241 | 73 | url = 'http://example.com/bundle.yaml' | ||
242 | 74 | options = self.make_options(url, bundle_name='bundle1') | ||
243 | 75 | with self.patch_urlread(contents=self.valid_bundle) as mock_urlread: | ||
244 | 76 | manage._validate_bundle(options, self.parser) | ||
245 | 77 | mock_urlread.assert_called_once_with(url) | ||
246 | 64 | self.assertEqual('bundle1', options.bundle_name) | 78 | self.assertEqual('bundle1', options.bundle_name) |
247 | 65 | self.assertEqual( | 79 | self.assertEqual( |
248 | 66 | ['mysql', 'wordpress'], sorted(options.bundle_services)) | 80 | ['mysql', 'wordpress'], sorted(options.bundle_services)) |
249 | @@ -78,7 +92,7 @@ | |||
250 | 78 | options = self.make_options(bundle=path, bundle_name='bundle2') | 92 | options = self.make_options(bundle=path, bundle_name='bundle2') |
251 | 79 | with mock.patch('os.environ', {'HOME': base_path}): | 93 | with mock.patch('os.environ', {'HOME': base_path}): |
252 | 80 | manage._validate_bundle(options, self.parser) | 94 | manage._validate_bundle(options, self.parser) |
254 | 81 | self.assertEqual(bundle_file, options.bundle_file) | 95 | self.assertEqual(self.valid_bundle, options.bundle_yaml) |
255 | 82 | 96 | ||
256 | 83 | def test_bundle_file_not_found(self): | 97 | def test_bundle_file_not_found(self): |
257 | 84 | # A parser error is invoked if the bundle file is not found. | 98 | # A parser error is invoked if the bundle file is not found. |
258 | @@ -90,8 +104,19 @@ | |||
259 | 90 | ) | 104 | ) |
260 | 91 | self.parser.error.assert_called_once_with(expected) | 105 | self.parser.error.assert_called_once_with(expected) |
261 | 92 | 106 | ||
264 | 93 | def test_error_parsing_bundle_file(self): | 107 | def test_url_error(self): |
265 | 94 | # A parser error is invoked if an error occurs parsing the bundle file. | 108 | # A parser error is invoked if the bundle cannot be fetched from the |
266 | 109 | # provided URL. | ||
267 | 110 | url = 'http://example.com/bundle.yaml' | ||
268 | 111 | options = self.make_options(url) | ||
269 | 112 | with self.patch_urlread(error=True) as mock_urlread: | ||
270 | 113 | manage._validate_bundle(options, self.parser) | ||
271 | 114 | mock_urlread.assert_called_once_with(url) | ||
272 | 115 | self.parser.error.assert_called_once_with( | ||
273 | 116 | 'unable to open bundle URL: bad wolf') | ||
274 | 117 | |||
275 | 118 | def test_error_parsing_bundle_contents(self): | ||
276 | 119 | # A parser error is invoked if an error occurs parsing the bundle YAML. | ||
277 | 95 | bundle_file = self.make_bundle_file() | 120 | bundle_file = self.make_bundle_file() |
278 | 96 | options = self.make_options(bundle_file, bundle_name='no-such') | 121 | options = self.make_options(bundle_file, bundle_name='no-such') |
279 | 97 | manage._validate_bundle(options, self.parser) | 122 | manage._validate_bundle(options, self.parser) |
280 | 98 | 123 | ||
281 | === modified file 'quickstart/tests/test_utils.py' | |||
282 | --- quickstart/tests/test_utils.py 2013-10-30 10:16:36 +0000 | |||
283 | +++ quickstart/tests/test_utils.py 2013-10-31 08:49:45 +0000 | |||
284 | @@ -16,7 +16,11 @@ | |||
285 | 16 | 16 | ||
286 | 17 | """Tests for the Juju Quickstart utility functions and classes.""" | 17 | """Tests for the Juju Quickstart utility functions and classes.""" |
287 | 18 | 18 | ||
288 | 19 | import httplib | ||
289 | 20 | import json | ||
290 | 21 | import socket | ||
291 | 19 | import unittest | 22 | import unittest |
292 | 23 | import urllib2 | ||
293 | 20 | 24 | ||
294 | 21 | import mock | 25 | import mock |
295 | 22 | import yaml | 26 | import yaml |
296 | @@ -71,6 +75,35 @@ | |||
297 | 71 | utils.call('echo', 'we are the borg!') | 75 | utils.call('echo', 'we are the borg!') |
298 | 72 | 76 | ||
299 | 73 | 77 | ||
300 | 78 | class TestGetCharmUrl(helpers.UrlReadTestsMixin, unittest.TestCase): | ||
301 | 79 | |||
302 | 80 | def test_charm_url(self): | ||
303 | 81 | # The Juju GUI charm URL is correctly returned. | ||
304 | 82 | contents = json.dumps({'charm': {'url': 'cs:precise/juju-gui-42'}}) | ||
305 | 83 | with self.patch_urlread(contents=contents) as mock_urlread: | ||
306 | 84 | charm_url = utils.get_charm_url() | ||
307 | 85 | self.assertEqual('cs:precise/juju-gui-42', charm_url) | ||
308 | 86 | mock_urlread.assert_called_once_with(utils.CHARMWORLD_API) | ||
309 | 87 | |||
310 | 88 | def test_io_error(self): | ||
311 | 89 | # IOErrors are properly propagated. | ||
312 | 90 | with self.patch_urlread(error=True) as mock_urlread: | ||
313 | 91 | with self.assertRaises(IOError) as context_manager: | ||
314 | 92 | utils.get_charm_url() | ||
315 | 93 | mock_urlread.assert_called_once_with(utils.CHARMWORLD_API) | ||
316 | 94 | self.assertEqual('bad wolf', str(context_manager.exception)) | ||
317 | 95 | |||
318 | 96 | def test_value_error(self): | ||
319 | 97 | # A ValueError is raised if the API response is not valid. | ||
320 | 98 | contents = json.dumps({'charm': {}}) | ||
321 | 99 | with self.patch_urlread(contents=contents) as mock_urlread: | ||
322 | 100 | with self.assertRaises(ValueError) as context_manager: | ||
323 | 101 | utils.get_charm_url() | ||
324 | 102 | mock_urlread.assert_called_once_with(utils.CHARMWORLD_API) | ||
325 | 103 | self.assertEqual( | ||
326 | 104 | 'unable to find the charm URL', str(context_manager.exception)) | ||
327 | 105 | |||
328 | 106 | |||
329 | 74 | class TestGetDefaultEnvName(helpers.CallTestsMixin, unittest.TestCase): | 107 | class TestGetDefaultEnvName(helpers.CallTestsMixin, unittest.TestCase): |
330 | 75 | 108 | ||
331 | 76 | def test_environment_variable(self): | 109 | def test_environment_variable(self): |
332 | @@ -354,6 +387,46 @@ | |||
333 | 354 | utils.unit_changes(self.unit, changeset)) | 387 | utils.unit_changes(self.unit, changeset)) |
334 | 355 | 388 | ||
335 | 356 | 389 | ||
336 | 390 | class TestUrlread(unittest.TestCase): | ||
337 | 391 | |||
338 | 392 | def patch_urlopen(self, contents=None, error=None): | ||
339 | 393 | """Patch the urllib2.urlopen function. | ||
340 | 394 | |||
341 | 395 | If contents is not None, the read() method of the returned mock object | ||
342 | 396 | returns the given contents. | ||
343 | 397 | If an error is provided, the call raises the error. | ||
344 | 398 | """ | ||
345 | 399 | mock_urlopen = mock.Mock() | ||
346 | 400 | if contents is not None: | ||
347 | 401 | mock_urlopen().read.return_value = contents | ||
348 | 402 | if error is not None: | ||
349 | 403 | mock_urlopen.side_effect = error | ||
350 | 404 | mock_urlopen.reset_mock() | ||
351 | 405 | return mock.patch('urllib2.urlopen', mock_urlopen) | ||
352 | 406 | |||
353 | 407 | def test_contents(self): | ||
354 | 408 | # The URL contents are correctly returned. | ||
355 | 409 | with self.patch_urlopen(contents='URL contents') as mock_urlopen: | ||
356 | 410 | contents = utils.urlread('http://example.com/path/') | ||
357 | 411 | self.assertEqual('URL contents', contents) | ||
358 | 412 | mock_urlopen.assert_called_once_with('http://example.com/path/') | ||
359 | 413 | |||
360 | 414 | def test_errors(self): | ||
361 | 415 | # An IOError is raised if an error occurs connecting to the API. | ||
362 | 416 | errors = { | ||
363 | 417 | 'httplib HTTPException': httplib.HTTPException, | ||
364 | 418 | 'socket error': socket.error, | ||
365 | 419 | 'urllib2 URLError': urllib2.URLError, | ||
366 | 420 | } | ||
367 | 421 | for message, exception_class in errors.items(): | ||
368 | 422 | exception = exception_class(message) | ||
369 | 423 | with self.patch_urlopen(error=exception) as mock_urlopen: | ||
370 | 424 | with self.assertRaises(IOError) as context_manager: | ||
371 | 425 | utils.urlread('http://example.com/path/') | ||
372 | 426 | mock_urlopen.assert_called_once_with('http://example.com/path/') | ||
373 | 427 | self.assertEqual(message, str(context_manager.exception)) | ||
374 | 428 | |||
375 | 429 | |||
376 | 357 | class TestUtf8(unittest.TestCase): | 430 | class TestUtf8(unittest.TestCase): |
377 | 358 | 431 | ||
378 | 359 | def test_unicode(self): | 432 | def test_unicode(self): |
379 | 360 | 433 | ||
380 | === modified file 'quickstart/utils.py' | |||
381 | --- quickstart/utils.py 2013-10-30 10:16:36 +0000 | |||
382 | +++ quickstart/utils.py 2013-10-31 08:49:45 +0000 | |||
383 | @@ -17,16 +17,22 @@ | |||
384 | 17 | """Juju Quickstart utility functions and classes.""" | 17 | """Juju Quickstart utility functions and classes.""" |
385 | 18 | 18 | ||
386 | 19 | import collections | 19 | import collections |
387 | 20 | import httplib | ||
388 | 21 | import json | ||
389 | 20 | import logging | 22 | import logging |
390 | 21 | import os | 23 | import os |
391 | 22 | import pipes | 24 | import pipes |
392 | 23 | import re | 25 | import re |
393 | 26 | import socket | ||
394 | 24 | import subprocess | 27 | import subprocess |
395 | 28 | import urllib2 | ||
396 | 25 | 29 | ||
397 | 26 | import yaml | 30 | import yaml |
398 | 27 | 31 | ||
399 | 28 | # Compile the regular expression used to parse the "juju switch" output. | 32 | # Compile the regular expression used to parse the "juju switch" output. |
400 | 29 | _juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n') | 33 | _juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n') |
401 | 34 | # Define the URL containing information about the last Juju GUI charm version. | ||
402 | 35 | CHARMWORLD_API = 'http://manage.jujucharms.com/api/3/charm/precise/juju-gui' | ||
403 | 30 | 36 | ||
404 | 31 | 37 | ||
405 | 32 | def call(*args): | 38 | def call(*args): |
406 | @@ -52,6 +58,19 @@ | |||
407 | 52 | return retcode, output, error | 58 | return retcode, output, error |
408 | 53 | 59 | ||
409 | 54 | 60 | ||
410 | 61 | def get_charm_url(): | ||
411 | 62 | """Return the charm URL of the latest Juju GUI charm revision. | ||
412 | 63 | |||
413 | 64 | Raise an IOError if any problems occur connecting to the API endpoint. | ||
414 | 65 | Raise a ValueError if the API returns invalid data. | ||
415 | 66 | """ | ||
416 | 67 | charm_info = json.loads(urlread(CHARMWORLD_API)) | ||
417 | 68 | charm_url = charm_info.get('charm', {}).get('url') | ||
418 | 69 | if charm_url is None: | ||
419 | 70 | raise ValueError('unable to find the charm URL') | ||
420 | 71 | return charm_url | ||
421 | 72 | |||
422 | 73 | |||
423 | 55 | def get_default_env_name(): | 74 | def get_default_env_name(): |
424 | 56 | """Return the current Juju environment name. | 75 | """Return the current Juju environment name. |
425 | 57 | 76 | ||
426 | @@ -212,6 +231,20 @@ | |||
427 | 212 | return change | 231 | return change |
428 | 213 | 232 | ||
429 | 214 | 233 | ||
430 | 234 | def urlread(url): | ||
431 | 235 | """Open the given URL and return the page contents. | ||
432 | 236 | |||
433 | 237 | Raise an IOError if any problems occur. | ||
434 | 238 | """ | ||
435 | 239 | try: | ||
436 | 240 | response = urllib2.urlopen(url) | ||
437 | 241 | except urllib2.URLError as err: | ||
438 | 242 | raise IOError(err.reason) | ||
439 | 243 | except (httplib.HTTPException, socket.error, urllib2.HTTPError) as err: | ||
440 | 244 | raise IOError(str(err)) | ||
441 | 245 | return response.read() | ||
442 | 246 | |||
443 | 247 | |||
444 | 215 | def utf8(value): | 248 | def utf8(value): |
445 | 216 | """Return the utf8 encoded version of the given value. | 249 | """Return the utf8 encoded version of the given value. |
446 | 217 | 250 |
Reviewers: mp+193297_ code.launchpad. net,
Message:
Please take a look.
Description:
Deploy a bundle from a HTTP(S) url.
The bundle argument can be a file path or
a http/https URL pointing to a bundle YAML.
Also get the URL of the last Juju GUI charm
revision from charmworld (API 2 for now).
Tests: `make check`.
QA: dpaste. com/1435065/ plain/ dpaste. com/1435065/ plain/`
I copied a bundle YAML over here:
http://
You should be able to deploy that bundle
by running the following (after juju switching to ec2 or similar):
`.venv/bin/python juju-quickstart http://
Remember to destroy your ec2 environment...
https:/ /code.launchpad .net/~frankban/ juju-quickstart /open-urls/ +merge/ 193297
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/19870043/
Affected files (+224, -35 lines): manage. py tests/helpers. py tests/test_ app.py tests/test_ manage. py tests/test_ utils.py
A [revision details]
M quickstart/app.py
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/utils.py