Merge lp:~frankban/juju-quickstart/uncommitted-bundles into lp:juju-quickstart
- uncommitted-bundles
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 135 |
Proposed branch: | lp:~frankban/juju-quickstart/uncommitted-bundles |
Merge into: | lp:juju-quickstart |
Diff against target: |
665 lines (+332/-57) 13 files modified
quickstart/__init__.py (+1/-1) quickstart/app.py (+62/-6) quickstart/juju.py (+9/-0) quickstart/jujugui.py (+28/-0) quickstart/jujutools.py (+2/-5) quickstart/manage.py (+31/-21) quickstart/settings.py (+7/-2) quickstart/tests/functional/test_functional.py (+8/-0) quickstart/tests/test_app.py (+99/-16) quickstart/tests/test_juju.py (+12/-0) quickstart/tests/test_jujugui.py (+46/-0) quickstart/tests/test_manage.py (+23/-2) tox.ini (+4/-4) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/uncommitted-bundles |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Brad Crittenden (community) | code + qa | Approve | |
Review via email: mp+261200@code.launchpad.net |
Commit message
Add support for uncommitted bundles.
Description of the change
Add support for uncommitted bundles.
Introduce the -u/--uncommitted flag, which enables
uncommitted bundle support.
Improve output messages and tokens handling.
Also update jujubundlelib dep to latest version.
TESTS:
`make fcheck` and wait a while for the functional tests
to complete.
QA:
- deploy bundles as usual:
`devenv/
- deploy uncommitted bundles:
`devenv/
Francesco Banconi (frankban) wrote : | # |
Richard Harding (rharding) wrote : | # |
LGTM, I think most of my questions are answered in some way or another.
I understand that this is really checking is the version of the charm
ready, not the gui, so checking the version.js isn't a perfect match. I
still can't help but feel that the charm revision is tied to the gui
release inside and maybe it's a better overall solution to this.
However, this is a perfectly reasonable way to go about it and should
work out most of the time.
https:/
File quickstart/app.py (right):
https:/
quickstart/
deployment '
should we hint at a juju-gui upgrade here?
https:/
quickstart/
with the following '
"uncommitted deployment" to "uncomitted loading"? deployment just
strikes me as very...deployed.
https:/
quickstart/
does this open the gui url as it does when you just run juju-quickstart
without the deployment?
https:/
File quickstart/
https:/
quickstart/
I don't get this part. Can this not work if the gui you deployed is a
local deployment of the GUI?
Should this better be a check for a flag/config/call to the GUI source
instead? Can it just check something like a check against version.js in
some way?
https:/
File quickstart/
https:/
quickstart/
oic, so we assume. I still wonder about using version.js vs
is_promulgated.
- 145. By Francesco Banconi
-
Changes as per review.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
https:/
File quickstart/app.py (right):
https:/
quickstart/
deployment '
On 2015/06/05 12:28:41, rharding wrote:
> should we hint at a juju-gui upgrade here?
Good idea, done.
https:/
quickstart/
with the following '
On 2015/06/05 12:28:41, rharding wrote:
> "uncommitted deployment" to "uncomitted loading"? deployment just
strikes me as
> very...deployed.
Done.
https:/
quickstart/
On 2015/06/05 12:28:41, rharding wrote:
> does this open the gui url as it does when you just run
juju-quickstart without
> the deployment?
This token, if returned, is used to build the GUI URL.
Opening the GUI on the browser is done by manage.run.
https:/
File quickstart/
https:/
quickstart/
On 2015/06/05 12:28:42, rharding wrote:
> oic, so we assume. I still wonder about using version.js vs
is_promulgated.
Well, some of the features we check (e.g. bundle deployment, uncommitted
bundles or new API endpoints) must be implemented principally on the
server (because qucikstart interacts mostly with the GUI server via its
API), and that's why in the cases we know the promulgated revision we
can safely check for their availability.
Given this is quickstart, I'd expect that we are dealing with
promulgated GUI charms 99% of the times, as you already mentioned. For
the remaining cases we trust the user and we expect her to be smart
enough to handle possible (and graceful) quickstart errors, which is the
only bad thing that can happen if the current customized charm does not
support a requested feature.
Brad Crittenden (bac) wrote : | # |
Francesco I can no longer log in to Rietveld so I'm making comments here only.
The code looks good. I'll do QA now.
Brad Crittenden (bac) wrote : | # |
QA was OK.
Preview Diff
1 | === modified file 'quickstart/__init__.py' |
2 | --- quickstart/__init__.py 2015-05-22 11:33:00 +0000 |
3 | +++ quickstart/__init__.py 2015-06-05 13:07:56 +0000 |
4 | @@ -45,7 +45,7 @@ |
5 | Once Juju has been installed, the command can also be run as a juju plugin, |
6 | without the hyphen ("juju quickstart"). |
7 | """ |
8 | -VERSION = (2, 1, 2) |
9 | +VERSION = (2, 2, 0) |
10 | |
11 | |
12 | def get_version(): |
13 | |
14 | === modified file 'quickstart/app.py' |
15 | --- quickstart/app.py 2015-05-22 17:11:26 +0000 |
16 | +++ quickstart/app.py 2015-06-05 13:07:56 +0000 |
17 | @@ -621,23 +621,79 @@ |
18 | return dict((k, v.get('value')) for k, v in config.items()) |
19 | |
20 | |
21 | -def deploy_bundle(env, bundle): |
22 | +def deploy_bundle(env, bundle, uncommitted, charm_ref): |
23 | """Deploy the given bundle connecting to the given environment. |
24 | |
25 | - Receive the environment connection to use for deploying the bundle and the |
26 | - bundle object as an instance of "quickstart.models.bundles.Bundle". |
27 | - |
28 | - Raise a ProgramExit if the API server returns an error response. |
29 | + Receive: |
30 | + - env: the environment connection to use for deploying the bundle; |
31 | + - bundle: the bundle to be deployed, as an instance of |
32 | + "quickstart.models.bundles.Bundle"; |
33 | + - uncommitted: a flag indicating whether the given bundle must be |
34 | + deployed in uncommitted mode; |
35 | + - charm_ref: the Juju GUI charm reference, used in the case uncommitted |
36 | + is True in order to verify that the GUI service supports uncommitted |
37 | + bundle deployments. |
38 | + |
39 | + If uncommitted is True, do not actually deploy the workload, but instead |
40 | + register it as a set of changes on the GUI server, and return a token |
41 | + identifying the change set, to be reused later by the Juju GUI. |
42 | + If uncommitted is False, start the bundle deployment and return None. |
43 | + |
44 | + Raise a ProgramExit if any of the API server calls return an error. |
45 | """ |
46 | + services = ', '.join(bundle.services().keys()) |
47 | + ref = bundle.reference |
48 | + details = '' |
49 | + if ref is not None: |
50 | + details = ( |
51 | + '\nmore details about this bundle can be found at\n' |
52 | + ' {}'.format(ref.jujucharms_url())) |
53 | + |
54 | + if uncommitted: |
55 | + # Ensure the currently deployed Juju GUI service supports change sets. |
56 | + series = charm_ref.series |
57 | + revision = settings.MINIMUM_REVISIONS_FOR_UNCOMMITTED_BUNDLES[series] |
58 | + if jujugui.is_promulgated(charm_ref) and charm_ref.revision < revision: |
59 | + logging.warn( |
60 | + 'uncommitted bundles loading is not supported by currently ' |
61 | + 'deployed Juju GUI charm ({}): requested bundle deployment ' |
62 | + 'will be ignored'.format(charm_ref)) |
63 | + logging.warn( |
64 | + 'run "juju upgrade-charm {}" to enable uncommitted bundles ' |
65 | + 'support'.format(settings.JUJU_GUI_SERVICE_NAME)) |
66 | + return None |
67 | + # Register the changes in the GUI server. |
68 | + print('requesting uncommitted bundle loading of {} with the following ' |
69 | + 'services:\n {}'.format(bundle, services)) |
70 | + try: |
71 | + response = env.set_changes(bundle.serialize()) |
72 | + except jujuclient.EnvError as err: |
73 | + raise ProgramExit( |
74 | + 'bad API server response: {}'.format(err.message)) |
75 | + errors = response.get('Errors') |
76 | + if errors: |
77 | + # This is unexpected, the bundle at this point is already validated |
78 | + # using juju bundle lib. |
79 | + msg = ', '.join(errors) |
80 | + raise ProgramExit('cannot generate the change set: {}'.format(msg)) |
81 | + print('the bundle is ready to be deployed\n' |
82 | + 'use the GUI to configure the bundle and to start the ' |
83 | + 'deployment process{}'.format(details)) |
84 | + return response['Token'] |
85 | + |
86 | + print('requesting a deployment of {} with the following services:\n' |
87 | + ' {}'.format(bundle, services)) |
88 | # XXX frankban 2015-02-26: use new bundle format if the GUI server is |
89 | # capable of handling bundle deployments with the API version 4. |
90 | yaml = bundle.serialize_legacy() |
91 | version = 3 |
92 | # XXX frankban 2015-02-26: find and implement a better way to increase the |
93 | # bundle deployments count. |
94 | - ref = bundle.reference |
95 | bundle_id = None if ref is None else ref.charmworld_id |
96 | try: |
97 | env.deploy_bundle(yaml, version, bundle_id=bundle_id) |
98 | except jujuclient.EnvError as err: |
99 | raise ProgramExit('bad API server response: {}'.format(err.message)) |
100 | + print('bundle deployment request accepted\nuse the GUI to check the ' |
101 | + 'bundle deployment progress{}'.format(details)) |
102 | + return None |
103 | |
104 | === modified file 'quickstart/juju.py' |
105 | --- quickstart/juju.py 2015-02-26 05:03:10 +0000 |
106 | +++ quickstart/juju.py 2015-06-05 13:07:56 +0000 |
107 | @@ -75,6 +75,15 @@ |
108 | } |
109 | return self._rpc(request) |
110 | |
111 | + def set_changes(self, yaml): |
112 | + """Register the bundle change set in the GUI server.""" |
113 | + request = { |
114 | + 'Type': 'ChangeSet', |
115 | + 'Request': 'SetChanges', |
116 | + 'Params': {'YAML': yaml}, |
117 | + } |
118 | + return self._rpc(request) |
119 | + |
120 | def create_auth_token(self): |
121 | """Make an auth token creation request. |
122 | |
123 | |
124 | === modified file 'quickstart/jujugui.py' |
125 | --- quickstart/jujugui.py 2015-05-22 16:04:02 +0000 |
126 | +++ quickstart/jujugui.py 2015-06-05 13:07:56 +0000 |
127 | @@ -28,6 +28,22 @@ |
128 | from quickstart import settings |
129 | |
130 | |
131 | +def add_tokens_to_url(url, auth_token, changes_token): |
132 | + """Return an URL including the optional authentication and changes tokens. |
133 | + |
134 | + If auth_token and changes_token are both None, the given URL is returned |
135 | + without changes. |
136 | + """ |
137 | + query = [] |
138 | + if auth_token is not None: |
139 | + query.append('authtoken=' + auth_token) |
140 | + if changes_token is not None: |
141 | + query.append('changestoken=' + changes_token) |
142 | + if query: |
143 | + return '{}/?{}'.format(url, '&'.join(query)) |
144 | + return url |
145 | + |
146 | + |
147 | def build_options(port, source): |
148 | """Create a configuration dict suitable to be used when deploying the GUI. |
149 | |
150 | @@ -43,6 +59,18 @@ |
151 | return options or None |
152 | |
153 | |
154 | +def is_promulgated(reference): |
155 | + """Report whether the given reference represents a GUI promulgated charm. |
156 | + |
157 | + The given reference is an instance of "jujubundlelib.references.Reference". |
158 | + """ |
159 | + return ( |
160 | + reference.name == settings.JUJU_GUI_CHARM_NAME and |
161 | + not reference.user and |
162 | + not reference.is_local() |
163 | + ) |
164 | + |
165 | + |
166 | def normalize_config(options): |
167 | """Normalize the Juju GUI configuration options. |
168 | |
169 | |
170 | === modified file 'quickstart/jujutools.py' |
171 | --- quickstart/jujutools.py 2015-05-22 16:04:02 +0000 |
172 | +++ quickstart/jujutools.py 2015-06-05 13:07:56 +0000 |
173 | @@ -19,6 +19,7 @@ |
174 | from __future__ import unicode_literals |
175 | |
176 | from quickstart import ( |
177 | + jujugui, |
178 | serializers, |
179 | settings, |
180 | ) |
181 | @@ -56,11 +57,7 @@ |
182 | # If a customized Juju GUI charm is in use, there is no way to check if the |
183 | # GUI server is recent enough to support the new Juju API endpoints. |
184 | # In these cases, assume the customized charm is recent enough. |
185 | - if ( |
186 | - charm_ref.name != settings.JUJU_GUI_CHARM_NAME or |
187 | - charm_ref.user or |
188 | - charm_ref.is_local() |
189 | - ): |
190 | + if not jujugui.is_promulgated(charm_ref): |
191 | return complete_url |
192 | # This is the promulgated Juju GUI charm. Check if it supports new APIs. |
193 | revision, series = charm_ref.revision, charm_ref.series |
194 | |
195 | === modified file 'quickstart/manage.py' |
196 | --- quickstart/manage.py 2015-05-22 16:04:02 +0000 |
197 | +++ quickstart/manage.py 2015-06-05 13:07:56 +0000 |
198 | @@ -374,6 +374,8 @@ |
199 | - open_browser: whether the GUI browser must be opened; |
200 | - platform: The host platform; |
201 | - port: the optional Juju GUI port, or None; |
202 | + - uncommitted: whether the provided bundle must be deployed using the |
203 | + Juju GUI uncommitted state; |
204 | - upload_tools: whether to upload local version of tools; |
205 | - upload_series: the comma-separated series list for which tools will |
206 | be uploaded, or None if not set. |
207 | @@ -444,15 +446,18 @@ |
208 | '-i', '--interactive', action='store_true', dest='interactive', |
209 | help='Start the environments management interactive session') |
210 | parser.add_argument( |
211 | - '--environments-file', dest='env_file', |
212 | - default=os.path.join(settings.JUJU_HOME, 'environments.yaml'), |
213 | - help='The path to the Juju environments YAML file\n(%(default)s)') |
214 | + '-u', '--uncommitted', action='store_true', dest='uncommitted', |
215 | + help='Do not start the bundle deployment. Open the GUI showing\n' |
216 | + 'an uncommitted bundle state instead, so that the bundle\n' |
217 | + 'can be finely tuned and tweaked before committing the\n' |
218 | + 'changes. This option is ignored if no bundle deployment\n' |
219 | + 'is requested.') |
220 | parser.add_argument( |
221 | '--gui-charm-url', dest='charm_url', |
222 | help='The Juju GUI charm URL to deploy in the environment.\n' |
223 | 'If not provided, the last release of the GUI will be\n' |
224 | 'deployed. The charm URL must include the charm version,\n' |
225 | - 'e.g. "cs:~juju-gui/precise/juju-gui-162". This option is\n' |
226 | + 'e.g. "cs:~juju-gui/trusty/juju-gui-51". This option is\n' |
227 | 'ignored if the GUI is already present in the environment') |
228 | parser.add_argument( |
229 | '--gui-port', dest='port', type=int, |
230 | @@ -466,6 +471,10 @@ |
231 | '--no-browser', action='store_false', dest='open_browser', |
232 | help='Avoid opening the browser to the GUI at the end of the\nprocess') |
233 | parser.add_argument( |
234 | + '--environments-file', dest='env_file', |
235 | + default=os.path.join(settings.JUJU_HOME, 'environments.yaml'), |
236 | + help='The path to the Juju environments YAML file\n(%(default)s)') |
237 | + parser.add_argument( |
238 | '--distro-only', action='store_true', dest='distro_only', |
239 | default=default_distro_only, help=distro_only_help) |
240 | parser.add_argument( |
241 | @@ -628,29 +637,30 @@ |
242 | '{}:{}'.format(address, gui_config['port']), juju_version, env_uuid, |
243 | path_prefix='ws', charm_ref=charm_ref, |
244 | insecure=not gui_config['secure']) |
245 | + |
246 | + # We need to connect to an API WebSocket server supporting bundle |
247 | + # deployments and automatic auth-token login. The GUI builtin server, |
248 | + # listening on the Juju GUI address, exposes a suitable API. |
249 | gui_env = app.connect(gui_api_url, username, password) |
250 | - |
251 | + changes_token = None |
252 | # Handle bundle deployment. |
253 | if options.bundle_source is not None: |
254 | - services = ', '.join(options.bundle.services().keys()) |
255 | - print('requesting a deployment of {} with the following services:\n' |
256 | - ' {}'.format(options.bundle, services)) |
257 | - if options.bundle.reference is not None: |
258 | - print('more details about this bundle can be found at\n' |
259 | - ' {}'.format(options.bundle.reference.jujucharms_url())) |
260 | - # We need to connect to an API WebSocket server supporting bundle |
261 | - # deployments. The GUI builtin server, listening on the Juju GUI |
262 | - # address, exposes an API suitable for deploying bundles. |
263 | - app.deploy_bundle(gui_env, options.bundle) |
264 | - print('bundle deployment request accepted\n' |
265 | - 'use the GUI to check the bundle deployment progress') |
266 | + changes_token = app.deploy_bundle( |
267 | + gui_env, options.bundle, options.uncommitted, charm_ref) |
268 | + # Handle automatic auth-token login. |
269 | + auth_token = app.create_auth_token(gui_env) |
270 | + gui_env.close() |
271 | |
272 | + # Calculate the complete URL and optionally open the default browser. |
273 | + url = jujugui.add_tokens_to_url(url, auth_token, changes_token) |
274 | if options.open_browser: |
275 | - token = app.create_auth_token(gui_env) |
276 | - if token is not None: |
277 | - url += '/?authtoken={}'.format(token) |
278 | + print('opening the browser at\n{}'.format(url)) |
279 | webbrowser.open(url) |
280 | - gui_env.close() |
281 | + else: |
282 | + print('not opening the browser\n' |
283 | + 'you can automatically log in into the Juju GUI by visiting\n' |
284 | + '{}\n(the login token will expire in two minutes)'.format(url)) |
285 | + |
286 | print( |
287 | 'done!\n\n' |
288 | 'Run "juju quickstart -e {env_name}" again if you want\n' |
289 | |
290 | === modified file 'quickstart/settings.py' |
291 | --- quickstart/settings.py 2015-03-09 17:50:28 +0000 |
292 | +++ quickstart/settings.py 2015-06-05 13:07:56 +0000 |
293 | @@ -37,8 +37,8 @@ |
294 | # temporary connection/charm store errors. |
295 | # Keep this list sorted by release date (older first). |
296 | DEFAULT_CHARM_URLS = collections.OrderedDict(( |
297 | - ('precise', 'cs:precise/juju-gui-108'), |
298 | - ('trusty', 'cs:trusty/juju-gui-21'), |
299 | + ('precise', 'cs:precise/juju-gui-115'), |
300 | + ('trusty', 'cs:trusty/juju-gui-28'), |
301 | )) |
302 | |
303 | # The quickstart app short description. |
304 | @@ -91,3 +91,8 @@ |
305 | # new endpoints. |
306 | MINIMUM_REVISIONS_FOR_NEW_API_ENDPOINT = collections.defaultdict( |
307 | lambda: 0, {'precise': 107, 'trusty': 19}) |
308 | + |
309 | +# The minimum Juju GUI charm revision supporting uncommitted bundle |
310 | +# deployments. Assume not listed series to always support uncommitted state. |
311 | +MINIMUM_REVISIONS_FOR_UNCOMMITTED_BUNDLES = collections.defaultdict( |
312 | + lambda: 0, {'precise': 115, 'trusty': 28}) |
313 | |
314 | === modified file 'quickstart/tests/functional/test_functional.py' |
315 | --- quickstart/tests/functional/test_functional.py 2015-05-13 08:31:51 +0000 |
316 | +++ quickstart/tests/functional/test_functional.py 2015-06-05 13:07:56 +0000 |
317 | @@ -180,3 +180,11 @@ |
318 | self.assertEqual(0, retcode) |
319 | self.assertIn('bundle deployment request accepted', output) |
320 | self.assertEqual('', error) |
321 | + |
322 | + def test_uncommitted_bundle_deployment(self): |
323 | + # The application can be used to deploy uncommitted bundles. |
324 | + retcode, output, error = run_quickstart( |
325 | + self.env_name, '-u', 'u/openstack-charmers/openstack/25') |
326 | + self.assertEqual(0, retcode) |
327 | + self.assertIn('the bundle is ready to be deployed', output) |
328 | + self.assertEqual('', error) |
329 | |
330 | === modified file 'quickstart/tests/test_app.py' |
331 | --- quickstart/tests/test_app.py 2015-05-22 17:11:26 +0000 |
332 | +++ quickstart/tests/test_app.py 2015-06-05 13:07:56 +0000 |
333 | @@ -1747,49 +1747,132 @@ |
334 | app.get_service_config(env, 'my-service') |
335 | |
336 | |
337 | +@helpers.mock_print |
338 | class TestDeployBundle(ProgramExitTestsMixin, unittest.TestCase): |
339 | |
340 | - bundle_data = {'services': {}} |
341 | + bundle_data = {'services': {'django': {}, 'haproxy': {}}} |
342 | bundle = bundles.Bundle(bundle_data) |
343 | + charm = references.Reference.from_string('cs:trusty/juju-gui-42') |
344 | |
345 | - def test_bundle_deployment(self): |
346 | + def test_bundle_deployment(self, mock_print): |
347 | # A bundle is successfully deployed. |
348 | - env = mock.Mock() |
349 | - app.deploy_bundle(env, self.bundle) |
350 | + env, uncommitted = mock.Mock(), False |
351 | + token = app.deploy_bundle(env, self.bundle, uncommitted, self.charm) |
352 | + self.assertIsNone(token) |
353 | # For the time being, the bundle version 3 is deployed by default. |
354 | expected_yaml = yaml.safe_dump({'bundle': self.bundle_data}) |
355 | env.deploy_bundle.assert_called_once_with( |
356 | expected_yaml, 3, bundle_id=None) |
357 | + self.assertFalse(env.set_changes.called) |
358 | self.assertFalse(env.close.called) |
359 | + self.assertEqual(2, mock_print.call_count) |
360 | + mock_print.assert_has_calls([ |
361 | + mock.call( |
362 | + 'requesting a deployment of bundle with the following ' |
363 | + 'services:\n django, haproxy'), |
364 | + mock.call( |
365 | + 'bundle deployment request accepted\n' |
366 | + 'use the GUI to check the bundle deployment progress'), |
367 | + ]) |
368 | |
369 | - def test_bundle_deployment_with_id(self): |
370 | + def test_bundle_deployment_with_id(self, mock_print): |
371 | # If the bundle reference includes the charmworld id, it is passed when |
372 | # calling the GUI server API. |
373 | # XXX frankban 2015-02-26: remove this test once we get rid of the |
374 | # charmworld id concept. |
375 | - env = mock.Mock() |
376 | + env, uncommitted = mock.Mock(), False |
377 | ref = references.Reference.from_fully_qualified_url( |
378 | 'cs:bundle/django-single-42') |
379 | ref.charmworld_id = 'django/single' |
380 | bundle = bundles.Bundle(self.bundle_data, reference=ref) |
381 | - app.deploy_bundle(env, bundle) |
382 | + app.deploy_bundle(env, bundle, uncommitted, self.charm) |
383 | env.deploy_bundle.assert_called_once_with( |
384 | self.bundle.serialize_legacy(), 3, bundle_id='django/single') |
385 | |
386 | - def test_api_error(self): |
387 | - # A ProgramExit is raised if an error occurs in one of the API calls. |
388 | - env = mock.Mock() |
389 | + def test_api_error(self, mock_print): |
390 | + # A ProgramExit is raised if an error occurs while deploying the |
391 | + # workload. |
392 | + env, uncommitted = mock.Mock(), False |
393 | env.deploy_bundle.side_effect = self.make_env_error( |
394 | 'bundle deployment failure') |
395 | expected_error = 'bad API server response: bundle deployment failure' |
396 | with self.assert_program_exit(expected_error): |
397 | - app.deploy_bundle(env, self.bundle) |
398 | + app.deploy_bundle(env, self.bundle, uncommitted, self.charm) |
399 | |
400 | - def test_other_errors(self): |
401 | - # Any other errors occurred during the process are not trapped. |
402 | - env = mock.Mock() |
403 | + def test_other_errors(self, mock_print): |
404 | + # Any other errors occurred during the bundle deployment process are |
405 | + # not trapped. |
406 | + env, uncommitted = mock.Mock(), False |
407 | error = ValueError('explode!') |
408 | env.deploy_bundle.side_effect = error |
409 | with self.assertRaises(ValueError) as context_manager: |
410 | - app.deploy_bundle(env, self.bundle) |
411 | - self.assertIs(error, context_manager.exception) |
412 | + app.deploy_bundle(env, self.bundle, uncommitted, self.charm) |
413 | + self.assertIs(error, context_manager.exception) |
414 | + |
415 | + def test_uncommitted_bundle_deployment(self, mock_print): |
416 | + # A bundle is successfully registered as an uncommitted set of changes |
417 | + # on the GUI server. |
418 | + env, uncommitted = mock.Mock(), True |
419 | + env.set_changes.return_value = {'Token': 'mytoken'} |
420 | + token = app.deploy_bundle(env, self.bundle, uncommitted, self.charm) |
421 | + self.assertEqual('mytoken', token) |
422 | + expected_yaml = yaml.safe_dump(self.bundle_data) |
423 | + env.set_changes.assert_called_once_with(expected_yaml) |
424 | + self.assertFalse(env.deploy_bundle.called) |
425 | + self.assertEqual(2, mock_print.call_count) |
426 | + mock_print.assert_has_calls([ |
427 | + mock.call( |
428 | + 'requesting uncommitted bundle loading of bundle with the ' |
429 | + 'following services:\n django, haproxy'), |
430 | + mock.call( |
431 | + 'the bundle is ready to be deployed\n' |
432 | + 'use the GUI to configure the bundle and to start the ' |
433 | + 'deployment process'), |
434 | + ]) |
435 | + |
436 | + def test_uncommitted_api_error(self, mock_print): |
437 | + # A ProgramExit is raised if an error occurs while registering the |
438 | + # change set to the GUI server. |
439 | + env, uncommitted = mock.Mock(), True |
440 | + env.set_changes.side_effect = self.make_env_error('bad wolf') |
441 | + with self.assert_program_exit('bad API server response: bad wolf'): |
442 | + app.deploy_bundle(env, self.bundle, uncommitted, self.charm) |
443 | + |
444 | + def test_uncommitted_bundle_validation_error(self, mock_print): |
445 | + # A ProgramExit is raised if the GUI server returns bundle validation |
446 | + # errors in its response. This is really unlikely to happen. |
447 | + env, uncommitted = mock.Mock(), True |
448 | + env.set_changes.return_value = {'Errors': ['error 1', 'error2']} |
449 | + expected_error = 'cannot generate the change set: error 1, error2' |
450 | + with self.assert_program_exit(expected_error): |
451 | + app.deploy_bundle(env, self.bundle, uncommitted, self.charm) |
452 | + |
453 | + def test_uncommitted_other_errors(self, mock_print): |
454 | + # Any other errors occurred during the change set process are not |
455 | + # trapped. |
456 | + env, uncommitted = mock.Mock(), True |
457 | + error = ValueError('explode!') |
458 | + env.set_changes.side_effect = error |
459 | + with self.assertRaises(ValueError) as context_manager: |
460 | + app.deploy_bundle(env, self.bundle, uncommitted, self.charm) |
461 | + self.assertIs(error, context_manager.exception) |
462 | + |
463 | + def test_uncommitted_not_supported(self, mock_print): |
464 | + # The uncommitted set of changes cannot be registered if the Juju GUI |
465 | + # service deployed in the environment does not support it. |
466 | + env, uncommitted = mock.Mock(), True |
467 | + charm = references.Reference.from_string('cs:trusty/juju-gui-0') |
468 | + expected_logs = [ |
469 | + 'uncommitted bundles loading is not supported by currently ' |
470 | + 'deployed Juju GUI charm (cs:trusty/juju-gui-0): requested bundle ' |
471 | + 'deployment will be ignored', |
472 | + 'run "juju upgrade-charm juju-gui" to enable uncommitted bundles ' |
473 | + 'support', |
474 | + ] |
475 | + with helpers.assert_logs(expected_logs, level='warn'): |
476 | + token = app.deploy_bundle(env, self.bundle, uncommitted, charm) |
477 | + self.assertIsNone(token) |
478 | + # No calls to deploy the bundle have been made. |
479 | + self.assertFalse(env.deploy_bundle.called) |
480 | + self.assertFalse(env.set_changes.called) |
481 | + self.assertFalse(mock_print.called) |
482 | |
483 | === modified file 'quickstart/tests/test_juju.py' |
484 | --- quickstart/tests/test_juju.py 2015-05-22 10:48:55 +0000 |
485 | +++ quickstart/tests/test_juju.py 2015-06-05 13:07:56 +0000 |
486 | @@ -305,7 +305,19 @@ |
487 | }) |
488 | |
489 | @patch_rpc |
490 | + def test_set_changes(self, mock_rpc): |
491 | + # The ChangeSet:SetChanges API call is properly generated. |
492 | + self.env.set_changes('YAML content') |
493 | + expected = { |
494 | + 'Type': 'ChangeSet', |
495 | + 'Request': 'SetChanges', |
496 | + 'Params': {'YAML': 'YAML content'}, |
497 | + } |
498 | + mock_rpc.assert_called_once_with(expected) |
499 | + |
500 | + @patch_rpc |
501 | def test_create_auth_token(self, mock_rpc): |
502 | + # The GUIToken:Create API call is properly generated. |
503 | self.env.create_auth_token() |
504 | expected = dict(Type='GUIToken', Request='Create') |
505 | mock_rpc.assert_called_once_with(expected) |
506 | |
507 | === modified file 'quickstart/tests/test_jujugui.py' |
508 | --- quickstart/tests/test_jujugui.py 2015-05-22 16:04:02 +0000 |
509 | +++ quickstart/tests/test_jujugui.py 2015-06-05 13:07:56 +0000 |
510 | @@ -27,6 +27,32 @@ |
511 | from quickstart.tests import helpers |
512 | |
513 | |
514 | +class TestAddTokensToUrl(unittest.TestCase): |
515 | + |
516 | + def test_tokens(self): |
517 | + # The URL is properly generated with both authentication and changes |
518 | + # tokens. |
519 | + url = jujugui.add_tokens_to_url( |
520 | + 'https://1.2.3.4', 'myauth', 'mychanges') |
521 | + self.assertEqual( |
522 | + 'https://1.2.3.4/?authtoken=myauth&changestoken=mychanges', url) |
523 | + |
524 | + def test_auth_token_only(self): |
525 | + # The URL is properly generated with the authentication token only. |
526 | + url = jujugui.add_tokens_to_url('https://1.2.3.4', 'login', None) |
527 | + self.assertEqual('https://1.2.3.4/?authtoken=login', url) |
528 | + |
529 | + def test_changes_token_only(self): |
530 | + # The URL is properly generated with the changes token only. |
531 | + url = jujugui.add_tokens_to_url('https://1.2.3.4', None, 'changeset') |
532 | + self.assertEqual('https://1.2.3.4/?changestoken=changeset', url) |
533 | + |
534 | + def test_no_tokens(self): |
535 | + # The URL is left untouched if no tokens are provided. |
536 | + url = 'http://example.com' |
537 | + self.assertEqual(url, jujugui.add_tokens_to_url(url, None, None)) |
538 | + |
539 | + |
540 | class TestBuildOptions(unittest.TestCase): |
541 | |
542 | def test_no_options(self): |
543 | @@ -57,6 +83,26 @@ |
544 | self.assertEqual(expected_options, options) |
545 | |
546 | |
547 | +class TestIsPromulgated(unittest.TestCase): |
548 | + |
549 | + def test_promulgated(self): |
550 | + # Promulgated Juju GUI charm references are properly recognized. |
551 | + for url in ('cs:precise/juju-gui-0', 'cs:trusty/juju-gui-42'): |
552 | + ref = references.Reference.from_string(url) |
553 | + self.assertTrue(jujugui.is_promulgated(ref), url) |
554 | + |
555 | + def test_customized(self): |
556 | + # Customized Juju GUI charm references are properly recognized. |
557 | + tests = ( |
558 | + 'local:precise/juju-gui-0', |
559 | + 'cs:precise/mygui-1', |
560 | + 'cs:~who/trusty/juju-gui-2', |
561 | + ) |
562 | + for url in tests: |
563 | + ref = references.Reference.from_string(url) |
564 | + self.assertFalse(jujugui.is_promulgated(ref), url) |
565 | + |
566 | + |
567 | class TestNormalizeConfig(unittest.TestCase): |
568 | |
569 | def test_normal_options(self): |
570 | |
571 | === modified file 'quickstart/tests/test_manage.py' |
572 | --- quickstart/tests/test_manage.py 2015-05-22 14:28:23 +0000 |
573 | +++ quickstart/tests/test_manage.py 2015-06-05 13:07:56 +0000 |
574 | @@ -702,6 +702,7 @@ |
575 | 'gui_source': None, |
576 | 'open_browser': True, |
577 | 'port': None, |
578 | + 'uncommitted': False, |
579 | } |
580 | options.update(kwargs) |
581 | return mock.Mock(**options) |
582 | @@ -751,6 +752,8 @@ |
583 | 'watch': '1.2.3.5', |
584 | # Retrieve the Juju GUI service configuration. |
585 | 'get_service_config': {'port': None, 'secure': True}, |
586 | + # Optionally deploy a bundle. |
587 | + 'deploy_bundle': None, |
588 | # Create the login token for the Juju GUI. |
589 | 'create_auth_token': 'TOKEN', |
590 | } |
591 | @@ -965,7 +968,7 @@ |
592 | |
593 | def test_bundle(self, mock_app, mock_open): |
594 | # A bundle is correctly deployed by the application. |
595 | - env = self.configure_app(mock_app, create_auth_token=None) |
596 | + env = self.configure_app(mock_app) |
597 | bundle_source = 'mediawiki-single' |
598 | reference = references.Reference.from_jujucharms_url(bundle_source) |
599 | bundle = bundles.Bundle(self.bundle_data, reference=reference) |
600 | @@ -974,7 +977,25 @@ |
601 | with self.patch_get_juju_command(): |
602 | manage.run(options) |
603 | # Ensure the bundle is correctly deployed. |
604 | - mock_app.deploy_bundle.assert_called_once_with(env, bundle) |
605 | + ref = references.Reference.from_string('cs:trusty/juju-gui-42') |
606 | + mock_app.deploy_bundle.assert_called_once_with(env, bundle, False, ref) |
607 | + |
608 | + def test_uncommitted_bundle(self, mock_app, mock_open): |
609 | + # An uncommitted bundle is correctly deployed by the application. |
610 | + env = self.configure_app(mock_app, deploy_bundle='CHANGES-TOKEN') |
611 | + bundle_source = 'mediawiki-single' |
612 | + reference = references.Reference.from_jujucharms_url(bundle_source) |
613 | + bundle = bundles.Bundle(self.bundle_data, reference=reference) |
614 | + # Run the application. |
615 | + options = self.make_options( |
616 | + bundle_source=bundle_source, bundle=bundle, uncommitted=True) |
617 | + with self.patch_get_juju_command(): |
618 | + manage.run(options) |
619 | + # Ensure the bundle is correctly deployed. |
620 | + ref = references.Reference.from_string('cs:trusty/juju-gui-42') |
621 | + mock_app.deploy_bundle.assert_called_once_with(env, bundle, True, ref) |
622 | + mock_open.assert_called_once_with( |
623 | + 'https://1.2.3.5/?authtoken=TOKEN&changestoken=CHANGES-TOKEN') |
624 | |
625 | def test_local_provider(self, mock_app, mock_open): |
626 | # The application correctly handles working with local providers with |
627 | |
628 | === modified file 'tox.ini' |
629 | --- tox.ini 2015-05-11 10:55:51 +0000 |
630 | +++ tox.ini 2015-06-05 13:07:56 +0000 |
631 | @@ -72,7 +72,7 @@ |
632 | # See https://launchpad.net/~juju/+archive/ubuntu/stable. |
633 | websocket-client==0.18.0 |
634 | jujuclient==0.50.1 |
635 | - jujubundlelib==0.1.7 |
636 | + jujubundlelib==0.1.8 |
637 | urwid==1.2.1 |
638 | # The distribution PyYAML requirement is used in this case. |
639 | |
640 | @@ -82,7 +82,7 @@ |
641 | # Ubuntu 14.04 (trusty) distro dependencies. |
642 | websocket-client==0.12.0 |
643 | jujuclient==0.17.5 |
644 | - jujubundlelib==0.1.7 |
645 | + jujubundlelib==0.1.8 |
646 | PyYAML==3.10 |
647 | urwid==1.1.1 |
648 | |
649 | @@ -92,7 +92,7 @@ |
650 | # Ubuntu 14.10 (utopic) distro dependencies. |
651 | websocket-client==0.12.0 |
652 | jujuclient==0.17.5 |
653 | - jujubundlelib==0.1.7 |
654 | + jujubundlelib==0.1.8 |
655 | PyYAML==3.11 |
656 | urwid==1.2.1 |
657 | |
658 | @@ -102,7 +102,7 @@ |
659 | # Ubuntu 15.04 (vivid) distro dependencies. |
660 | websocket-client==0.18.0 |
661 | jujuclient==0.18.5 |
662 | - jujubundlelib==0.1.7 |
663 | + jujubundlelib==0.1.8 |
664 | PyYAML==3.11 |
665 | urwid==1.2.1 |
666 |
Reviewers: mp+261200_ code.launchpad. net,
Message:
Please take a look.
Description:
Add support for uncommitted bundles.
Introduce the -u/--uncommitted flag, which enables
uncommitted bundle support.
Improve output messages and tokens handling.
Also update jujubundlelib dep to latest version.
TESTS:
`make fcheck` and wait a while for the functional tests
to complete.
QA: bin/juju- quickstart mediawiki-single`; bin/juju- quickstart -u u/openstack- charmers/ openstack` ;
- deploy bundles as usual:
`devenv/
- deploy uncommitted bundles:
`devenv/
https:/ /code.launchpad .net/~frankban/ juju-quickstart /uncommitted- bundles/ +merge/ 261200
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/247800043/
Affected files (+329, -57 lines): __init_ _.py jujugui. py jujutools. py manage. py settings. py tests/functiona l/test_ functional. py tests/test_ app.py tests/test_ juju.py tests/test_ jujugui. py tests/test_ manage. 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/
M quickstart/
M quickstart/
M tox.ini