Merge lp:~frankban/juju-quickstart/uncommitted-bundles into lp:juju-quickstart

Proposed by Francesco Banconi
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
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/bin/juju-quickstart mediawiki-single`;
- deploy uncommitted bundles:
  `devenv/bin/juju-quickstart -u u/openstack-charmers/openstack`;

https://codereview.appspot.com/247800043/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

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:
- deploy bundles as usual:
   `devenv/bin/juju-quickstart mediawiki-single`;
- deploy uncommitted bundles:
   `devenv/bin/juju-quickstart -u u/openstack-charmers/openstack`;

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):
   A [revision details]
   M quickstart/__init__.py
   M quickstart/app.py
   M quickstart/juju.py
   M quickstart/jujugui.py
   M quickstart/jujutools.py
   M quickstart/manage.py
   M quickstart/settings.py
   M quickstart/tests/functional/test_functional.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_juju.py
   M quickstart/tests/test_jujugui.py
   M quickstart/tests/test_manage.py
   M tox.ini

Revision history for this message
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://codereview.appspot.com/247800043/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/247800043/diff/1/quickstart/app.py#newcode659
quickstart/app.py:659: 'deployed Juju GUI charm ({}): requested bundle
deployment '
should we hint at a juju-gui upgrade here?

https://codereview.appspot.com/247800043/diff/1/quickstart/app.py#newcode663
quickstart/app.py:663: print('requesting uncommitted deployment of {}
with the following '
"uncommitted deployment" to "uncomitted loading"? deployment just
strikes me as very...deployed.

https://codereview.appspot.com/247800043/diff/1/quickstart/app.py#newcode679
quickstart/app.py:679: return response['Token']
does this open the gui url as it does when you just run juju-quickstart
without the deployment?

https://codereview.appspot.com/247800043/diff/1/quickstart/jujugui.py
File quickstart/jujugui.py (right):

https://codereview.appspot.com/247800043/diff/1/quickstart/jujugui.py#newcode62
quickstart/jujugui.py:62: def is_promulgated(reference):
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://codereview.appspot.com/247800043/diff/1/quickstart/jujutools.py
File quickstart/jujutools.py (right):

https://codereview.appspot.com/247800043/diff/1/quickstart/jujutools.py#newcode60
quickstart/jujutools.py:60: if not jujugui.is_promulgated(charm_ref):
oic, so we assume. I still wonder about using version.js vs
is_promulgated.

https://codereview.appspot.com/247800043/

145. By Francesco Banconi

Changes as per review.

Revision history for this message
Francesco Banconi (frankban) wrote :

Please take a look.

https://codereview.appspot.com/247800043/diff/1/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/247800043/diff/1/quickstart/app.py#newcode659
quickstart/app.py:659: 'deployed Juju GUI charm ({}): requested bundle
deployment '
On 2015/06/05 12:28:41, rharding wrote:
> should we hint at a juju-gui upgrade here?

Good idea, done.

https://codereview.appspot.com/247800043/diff/1/quickstart/app.py#newcode663
quickstart/app.py:663: print('requesting uncommitted deployment of {}
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://codereview.appspot.com/247800043/diff/1/quickstart/app.py#newcode679
quickstart/app.py:679: return response['Token']
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://codereview.appspot.com/247800043/diff/1/quickstart/jujutools.py
File quickstart/jujutools.py (right):

https://codereview.appspot.com/247800043/diff/1/quickstart/jujutools.py#newcode60
quickstart/jujutools.py:60: if not jujugui.is_promulgated(charm_ref):
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.

https://codereview.appspot.com/247800043/

Revision history for this message
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.

Revision history for this message
Brad Crittenden (bac) wrote :

QA was OK.

review: Approve (code + qa)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches