Merge lp:~frankban/juju-quickstart/bundle-v4-api into lp:juju-quickstart
- bundle-v4-api
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 140 |
Proposed branch: | lp:~frankban/juju-quickstart/bundle-v4-api |
Merge into: | lp:juju-quickstart |
Diff against target: |
369 lines (+111/-36) 7 files modified
README.rst (+1/-1) quickstart/app.py (+22/-4) quickstart/models/bundles.py (+17/-12) quickstart/settings.py (+9/-3) quickstart/tests/models/test_bundles.py (+18/-1) quickstart/tests/test_app.py (+34/-4) tox.ini (+10/-11) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/bundle-v4-api |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
j.c.sackett (community) | Approve | ||
Madison Scott-Clary (community) | code | Approve | |
Review via email: mp+267807@code.launchpad.net |
Commit message
Description of the change
Send the actual bundle version to GUI server.
When deploying bundles, if not taken from the
charm store (in which case they are always v4 bundles),
check the bundle syntax version and properly
inform the GUI server about whether a legacy
or new-style bundle is being deployed.
This is done if the GUI unit supports new bundle
v4, otherwise still try to proceed with the
deployment process, pretending a v3 bundle has been
provided.
Drop support for utopic and add support for wily.
Also update default Juju GUI revisions.
Tests: `make check`.
QA: deploy a bundle v4, preferably with unit placement,
e.g.: `devenv/
The uncommitted state should co-locate units in the proper way,
thanks to the GUI fixes from Madison. Now remove the -u flag:
`devenv/
that also the GUI server is able to properly deploy v4 bundles.
But also try the usual mediawiki-single, etc...
Run quickstart again, run the interactive mode, ensure
quickstart works as usual, this branch will be the 2.2.1 release.
j.c.sackett (jcsackett) wrote : | # |
QA notes:
When running with -u everything worked great.
When running without the -u I got the following error in the GUI.
"n error occurred while deploying the bundle: Invalid service placement nova-compute to ceph/0 Invalid service placement nova-compute to ceph/1 Invalid service placement nova-compute to ceph/2 Invalid service placement ceph-radosgw to lxc:ceph/1 Invalid service placement neutron-api to lxc:ceph/0 Invalid service placement nova-cloud-
Not sure if this is a qa error or an indication that I've got the wrong thing running. I'm running trusty/juju-gui-38, which is the minimum listed revision for V4 bundles; is there a restriction on which providers this works on? I'm using a local env.
Francesco Banconi (frankban) wrote : | # |
Thanks for the review JC, replied in line.
- 146. By Francesco Banconi
-
Fix typo.
Francesco Banconi (frankban) wrote : | # |
I cannot dupe this QA problem.
Could you please run "devenv/
j.c.sackett (jcsackett) wrote : | # |
On EC2 I'm getting the same issue, log is https:/
Francesco Banconi (frankban) wrote : | # |
From the logs it seems that quickstart is not considering the bundle a v4 one ("Version": 4 is not included in the params of the Import WebSocket request).
This is not the case here locally. So could you please make another attempt, just to exclude local misconfigurations? "make clean", then "make" and then "devenv/
Madison Scott-Clary (makyo) wrote : | # |
LGTM, thanks for the update
j.c.sackett (jcsackett) wrote : | # |
After blowing away the branch and getting this again, it seems to work. I see Version: 4 in the bundle output and the bundle has deployed.
QA OK.
Preview Diff
1 | === modified file 'README.rst' |
2 | --- README.rst 2015-08-07 10:24:11 +0000 |
3 | +++ README.rst 2015-08-12 16:25:17 +0000 |
4 | @@ -34,7 +34,7 @@ |
5 | ------------------ |
6 | |
7 | Juju Quickstart is available on Ubuntu releases 12.04 LTS (precise), 14.04 LTS |
8 | -(trusty), 15.04 (vivid) and on OS X (10.7 and later). |
9 | +(trusty), 15.04 (vivid), 15.10 (wily) and on OS X (10.7 and later). |
10 | |
11 | Starting from version 1.5.0, Juju Quickstart only supports Juju >= 1.18.1. |
12 | |
13 | |
14 | === modified file 'quickstart/app.py' |
15 | --- quickstart/app.py 2015-06-05 13:07:47 +0000 |
16 | +++ quickstart/app.py 2015-08-12 16:25:17 +0000 |
17 | @@ -683,10 +683,28 @@ |
18 | |
19 | print('requesting a deployment of {} with the following services:\n' |
20 | ' {}'.format(bundle, services)) |
21 | - # XXX frankban 2015-02-26: use new bundle format if the GUI server is |
22 | - # capable of handling bundle deployments with the API version 4. |
23 | - yaml = bundle.serialize_legacy() |
24 | - version = 3 |
25 | + version = bundle.version |
26 | + |
27 | + if version > 3: |
28 | + # The bundle uses a new-style syntax. Check whether the currently used |
29 | + # GUI server is capable of handling new-style bundles. |
30 | + revision = settings.MINIMUM_REVISIONS_FOR_V4_BUNDLES[charm_ref.series] |
31 | + if jujugui.is_promulgated(charm_ref) and charm_ref.revision < revision: |
32 | + logging.warn( |
33 | + 'new style bundle syntax is not supported by currently ' |
34 | + 'deployed Juju GUI charm ({}): trying to deploy the bundle ' |
35 | + 'using old syntax'.format(charm_ref)) |
36 | + # Pretend the bundle is a legacy one. Note that this might not work |
37 | + # depending on whether new style unit placement is used in the |
38 | + # bundle contents. |
39 | + yaml = bundle.serialize_legacy() |
40 | + version = 3 |
41 | + else: |
42 | + yaml = bundle.serialize() |
43 | + else: |
44 | + # The bundle uses the legacy syntax. |
45 | + yaml = bundle.serialize_legacy() |
46 | + |
47 | # XXX frankban 2015-02-26: find and implement a better way to increase the |
48 | # bundle deployments count. |
49 | bundle_id = None if ref is None else ref.charmworld_id |
50 | |
51 | === modified file 'quickstart/models/bundles.py' |
52 | --- quickstart/models/bundles.py 2015-05-13 08:46:33 +0000 |
53 | +++ quickstart/models/bundles.py 2015-08-12 16:25:17 +0000 |
54 | @@ -63,7 +63,7 @@ |
55 | class Bundle(object): |
56 | """Store information about a charm store bundle entity""" |
57 | |
58 | - def __init__(self, data, reference=None): |
59 | + def __init__(self, data, reference=None, version=4): |
60 | """Initialize the bundle. |
61 | |
62 | The data argument is the bundle YAML decoded content. |
63 | @@ -72,6 +72,7 @@ |
64 | """ |
65 | self.data = data |
66 | self.reference = reference |
67 | + self.version = version |
68 | |
69 | def __str__(self): |
70 | """Return the byte string representation of this bundle.""" |
71 | @@ -118,7 +119,7 @@ |
72 | |
73 | |
74 | def from_source(source, name=None): |
75 | - """Return a bundle YAML encoded string and id from the given source. |
76 | + """Return a bundle instance from the given source. |
77 | |
78 | The source argument is a string, and can be provided as: |
79 | |
80 | @@ -167,16 +168,18 @@ |
81 | is_remote = source.startswith('http://') or source.startswith('https://') |
82 | if has_extension and not is_remote: |
83 | # The source refers to a local file. |
84 | - data = _parse_and_flatten_yaml(_retrieve_from_file(source), name) |
85 | - return Bundle(data) |
86 | + content = _retrieve_from_file(source) |
87 | + data, version = _parse_and_flatten_yaml(content, name) |
88 | + return Bundle(data, version=version) |
89 | |
90 | try: |
91 | reference = references.Reference.from_jujucharms_url(source) |
92 | except ValueError: |
93 | if is_remote: |
94 | # The source is an arbitrary URL to a YAML/JSON content. |
95 | - data = _parse_and_flatten_yaml(_retrieve_from_url(source), name) |
96 | - return Bundle(data) |
97 | + content = _retrieve_from_url(source) |
98 | + data, version = _parse_and_flatten_yaml(content, name) |
99 | + return Bundle(data, version=version) |
100 | # No other options are available. |
101 | raise |
102 | |
103 | @@ -214,7 +217,8 @@ |
104 | Note that charmworld URLs always represent a bundle. |
105 | |
106 | Return a Bundle instance which includes the retrieved data and the bundle |
107 | - reference corresponding to the given charmworld URL. |
108 | + reference corresponding to the given charmworld URL. The returned bundle |
109 | + is retrieved from the charm store and always uses version 4 syntax. |
110 | Raise a ValueError if the provided URL is not valid, or if the bundle |
111 | content is not valid. |
112 | Raise a IOError if a problem is encountered while fetching the YAML |
113 | @@ -325,8 +329,8 @@ |
114 | |
115 | The content is provided as a YAML encoded string and can be either a new |
116 | style flat bundle or a legacy bundle format. |
117 | - In both cases, the returned YAML decoded data represents a new style |
118 | - bundle (API version 4). |
119 | + In both cases, the returned YAML decoded data represents a single flat |
120 | + bundle. The version is returned as an integer number. |
121 | |
122 | Raise a ValueError if: |
123 | - the bundle YAML contents are not parsable by YAML; |
124 | @@ -338,17 +342,18 @@ |
125 | """ |
126 | data = charmstore.load_bundle_yaml(content) |
127 | _ensure_is_dict(data) |
128 | + version = 4 |
129 | if is_legacy_bundle(data): |
130 | data = _flatten_data(data, name) |
131 | + version = 3 |
132 | validate(data) |
133 | - return data |
134 | + return data, version |
135 | |
136 | |
137 | def _flatten_data(data, name): |
138 | """Retrieve the bundle content from data for a specific bundle name. |
139 | |
140 | - The returned YAML decoded data represents a new style bundle |
141 | - (API version 4). |
142 | + The returned YAML decoded data represents a single flat bundle. |
143 | |
144 | Raise a ValueError if: |
145 | - the YAML data is not properly structured; |
146 | |
147 | === modified file 'quickstart/settings.py' |
148 | --- quickstart/settings.py 2015-06-17 14:02:03 +0000 |
149 | +++ quickstart/settings.py 2015-08-12 16:25:17 +0000 |
150 | @@ -37,8 +37,8 @@ |
151 | # temporary connection/charm store errors. |
152 | # Keep this list sorted by release date (older first). |
153 | DEFAULT_CHARM_URLS = collections.OrderedDict(( |
154 | - ('precise', 'cs:precise/juju-gui-117'), |
155 | - ('trusty', 'cs:trusty/juju-gui-32'), |
156 | + ('precise', 'cs:precise/juju-gui-121'), |
157 | + ('trusty', 'cs:trusty/juju-gui-38'), |
158 | )) |
159 | |
160 | # The quickstart app short description. |
161 | @@ -61,7 +61,8 @@ |
162 | |
163 | # The possible values for the environments.yaml default-series field. |
164 | JUJU_DEFAULT_SERIES = ( |
165 | - 'precise', 'quantal', 'raring', 'saucy', 'trusty', 'utopic', 'vivid') |
166 | + 'precise', 'quantal', 'raring', 'saucy', 'trusty', 'utopic', 'vivid', |
167 | + 'wily') |
168 | |
169 | # Retrieve the current juju-core home. |
170 | JUJU_HOME = os.getenv('JUJU_HOME', '~/.juju') |
171 | @@ -96,3 +97,8 @@ |
172 | # deployments. Assume not listed series to always support uncommitted state. |
173 | MINIMUM_REVISIONS_FOR_UNCOMMITTED_BUNDLES = collections.defaultdict( |
174 | lambda: 0, {'precise': 115, 'trusty': 28}) |
175 | + |
176 | +# The minimum Juju GUI charm revision supporting bundle syntax version 4. |
177 | +# Assume not listed series to always support v4 syntax. |
178 | +MINIMUM_REVISIONS_FOR_V4_BUNDLES = collections.defaultdict( |
179 | + lambda: 0, {'precise': 121, 'trusty': 38}) |
180 | |
181 | === modified file 'quickstart/tests/models/test_bundles.py' |
182 | --- quickstart/tests/models/test_bundles.py 2015-05-13 08:46:33 +0000 |
183 | +++ quickstart/tests/models/test_bundles.py 2015-08-12 16:25:17 +0000 |
184 | @@ -44,9 +44,19 @@ |
185 | self.bundle_data, reference=self.reference) |
186 | |
187 | def test_attributes(self): |
188 | - # The bundle data and the optional reference are stored as attributes. |
189 | + # The bundle data, the optional reference and the default version are |
190 | + # stored as attributes. |
191 | self.assertEqual(self.bundle_data, self.bundle.data) |
192 | self.assertEqual(self.reference, self.bundle.reference) |
193 | + self.assertEqual(4, self.bundle.version) |
194 | + |
195 | + def test_version_and_no_reference(self): |
196 | + # The bundle syntax version can be provided as an argument. The bundle |
197 | + # reference is optional. |
198 | + bundle = bundles.Bundle(self.bundle_data, version=3) |
199 | + self.assertEqual(self.bundle_data, bundle.data) |
200 | + self.assertIsNone(bundle.reference) |
201 | + self.assertEqual(3, bundle.version) |
202 | |
203 | def test_string(self): |
204 | # The bundle correctly represents itself as a string. |
205 | @@ -144,6 +154,7 @@ |
206 | bundle = bundles.from_source('bundle:mediawiki/single') |
207 | self.assertEqual(self.bundle_data, bundle.data) |
208 | self.assertEqual('cs:bundle/mediawiki-single', bundle.reference.id()) |
209 | + self.assertEqual(4, bundle.version) |
210 | # The charmworld id is properly set when passing charmworld URLs. |
211 | # XXX frankban 2015-03-09: remove this check once we get rid of the |
212 | # charmworld id concept. |
213 | @@ -184,6 +195,7 @@ |
214 | bundle = bundles.from_source('bundle:mediawiki/bundle1') |
215 | self.assertEqual(self.bundle_data, bundle.data) |
216 | self.assertEqual('cs:bundle/mediawiki', bundle.reference.id()) |
217 | + self.assertEqual(4, bundle.version) |
218 | # The charmworld id is properly set when passing charmworld URLs. |
219 | # XXX frankban 2015-03-09: remove this check once we get rid of the |
220 | # charmworld id concept. |
221 | @@ -334,6 +346,7 @@ |
222 | bundle = bundles.from_source('django') |
223 | self.assertEqual(self.bundle_data, bundle.data) |
224 | self.assertEqual('cs:bundle/django', bundle.reference.id()) |
225 | + self.assertEqual(4, bundle.version) |
226 | mock_urlread.assert_called_once_with( |
227 | settings.CHARMSTORE_API + 'bundle/django/archive/bundle.yaml') |
228 | |
229 | @@ -402,6 +415,7 @@ |
230 | bundle = bundles.from_source(path) |
231 | self.assertEqual(self.bundle_data, bundle.data) |
232 | self.assertIsNone(bundle.reference) |
233 | + self.assertEqual(4, bundle.version) |
234 | |
235 | def test_local_file_legacy_bundle(self): |
236 | # A bundle instance can be created from a local file source including |
237 | @@ -419,6 +433,7 @@ |
238 | bundle = bundles.from_source(path) |
239 | self.assertEqual(legacy_bundle_data['bundle'], bundle.data) |
240 | self.assertIsNone(bundle.reference) |
241 | + self.assertEqual(3, bundle.version) |
242 | |
243 | def test_local_file_not_found(self): |
244 | # An IOError is raised if a local file source cannot be found. |
245 | @@ -485,6 +500,7 @@ |
246 | bundle = bundles.from_source('https://1.2.3.4') |
247 | self.assertEqual(self.bundle_data, bundle.data) |
248 | self.assertIsNone(bundle.reference) |
249 | + self.assertEqual(4, bundle.version) |
250 | mock_urlread.assert_called_once_with('https://1.2.3.4') |
251 | |
252 | def test_remote_url_legacy_bundle(self): |
253 | @@ -496,6 +512,7 @@ |
254 | bundle = bundles.from_source('https://1.2.3.4:8000', 'bundle2') |
255 | self.assertEqual(self.legacy_bundle_data['bundle2'], bundle.data) |
256 | self.assertIsNone(bundle.reference) |
257 | + self.assertEqual(3, bundle.version) |
258 | mock_urlread.assert_called_once_with('https://1.2.3.4:8000') |
259 | |
260 | def test_remote_url_not_reachable(self): |
261 | |
262 | === modified file 'quickstart/tests/test_app.py' |
263 | --- quickstart/tests/test_app.py 2015-06-05 13:07:47 +0000 |
264 | +++ quickstart/tests/test_app.py 2015-08-12 16:25:17 +0000 |
265 | @@ -1759,10 +1759,8 @@ |
266 | env, uncommitted = mock.Mock(), False |
267 | token = app.deploy_bundle(env, self.bundle, uncommitted, self.charm) |
268 | self.assertIsNone(token) |
269 | - # For the time being, the bundle version 3 is deployed by default. |
270 | - expected_yaml = yaml.safe_dump({'bundle': self.bundle_data}) |
271 | env.deploy_bundle.assert_called_once_with( |
272 | - expected_yaml, 3, bundle_id=None) |
273 | + yaml.safe_dump(self.bundle_data), 4, bundle_id=None) |
274 | self.assertFalse(env.set_changes.called) |
275 | self.assertFalse(env.close.called) |
276 | self.assertEqual(2, mock_print.call_count) |
277 | @@ -1775,6 +1773,38 @@ |
278 | 'use the GUI to check the bundle deployment progress'), |
279 | ]) |
280 | |
281 | + def test_legacy_bundle_deployment(self, mock_print): |
282 | + # A bundle with legacy v3 bundle syntax is successfully deployed. |
283 | + env, uncommitted = mock.Mock(), False |
284 | + bundle = bundles.Bundle(self.bundle_data, version=3) |
285 | + token = app.deploy_bundle(env, bundle, uncommitted, self.charm) |
286 | + self.assertIsNone(token) |
287 | + expected_yaml = yaml.safe_dump({'bundle': self.bundle_data}) |
288 | + env.deploy_bundle.assert_called_once_with( |
289 | + expected_yaml, 3, bundle_id=None) |
290 | + self.assertFalse(env.set_changes.called) |
291 | + self.assertFalse(env.close.called) |
292 | + |
293 | + def test_bundle_deployment_v4_not_supported(self, mock_print): |
294 | + # The bundle deployment falls back to version 3 syntax if the Juju GUI |
295 | + # unit is not recent enough. In this case there is no guarantee that |
296 | + # the deployment succeeds. |
297 | + env, uncommitted = mock.Mock(), False |
298 | + charm = references.Reference.from_string('cs:trusty/juju-gui-36') |
299 | + expected_logs = [ |
300 | + 'new style bundle syntax is not supported by currently deployed ' |
301 | + 'Juju GUI charm (cs:trusty/juju-gui-36): trying to deploy the ' |
302 | + 'bundle using old syntax' |
303 | + ] |
304 | + with helpers.assert_logs(expected_logs, level='warn'): |
305 | + token = app.deploy_bundle(env, self.bundle, uncommitted, charm) |
306 | + self.assertIsNone(token) |
307 | + expected_yaml = yaml.safe_dump({'bundle': self.bundle_data}) |
308 | + env.deploy_bundle.assert_called_once_with( |
309 | + expected_yaml, 3, bundle_id=None) |
310 | + self.assertFalse(env.set_changes.called) |
311 | + self.assertFalse(env.close.called) |
312 | + |
313 | def test_bundle_deployment_with_id(self, mock_print): |
314 | # If the bundle reference includes the charmworld id, it is passed when |
315 | # calling the GUI server API. |
316 | @@ -1787,7 +1817,7 @@ |
317 | bundle = bundles.Bundle(self.bundle_data, reference=ref) |
318 | app.deploy_bundle(env, bundle, uncommitted, self.charm) |
319 | env.deploy_bundle.assert_called_once_with( |
320 | - self.bundle.serialize_legacy(), 3, bundle_id='django/single') |
321 | + self.bundle.serialize(), 4, bundle_id='django/single') |
322 | |
323 | def test_api_error(self, mock_print): |
324 | # A ProgramExit is raised if an error occurs while deploying the |
325 | |
326 | === modified file 'tox.ini' |
327 | --- tox.ini 2015-08-07 10:24:11 +0000 |
328 | +++ tox.ini 2015-08-12 16:25:17 +0000 |
329 | @@ -18,7 +18,7 @@ |
330 | # The envlist option must only include platform scenarios. In essence, those |
331 | # are used to test Juju Quickstart with the specific dependencies present in |
332 | # each supported OS platform. |
333 | -envlist = pypi,ppa,trusty,utopic,vivid |
334 | +envlist = pypi,ppa,trusty,vivid,wily |
335 | |
336 | # Define the base requirements and commands to use when testing the program. |
337 | |
338 | @@ -86,16 +86,6 @@ |
339 | PyYAML==3.10 |
340 | urwid==1.1.1 |
341 | |
342 | -[testenv:utopic] |
343 | -deps = |
344 | - {[testenv]deps} |
345 | - # Ubuntu 14.10 (utopic) distro dependencies. |
346 | - websocket-client==0.12.0 |
347 | - jujuclient==0.17.5 |
348 | - jujubundlelib==0.1.9 |
349 | - PyYAML==3.11 |
350 | - urwid==1.2.1 |
351 | - |
352 | [testenv:vivid] |
353 | deps = |
354 | {[testenv]deps} |
355 | @@ -106,6 +96,15 @@ |
356 | PyYAML==3.11 |
357 | urwid==1.2.1 |
358 | |
359 | +[testenv:wily] |
360 | +deps = |
361 | + {[testenv]deps} |
362 | + # Ubuntu 15.10 (wily) distro dependencies. |
363 | + websocket-client==0.18.0 |
364 | + jujuclient==0.50.1 |
365 | + jujubundlelib==0.1.9 |
366 | + PyYAML==3.11 |
367 | + urwid==1.2.1 |
368 | |
369 | # Define additional tox targets to be used to validate and lint the code. |
370 |
Francesco--
Thanks, this looks good. I noted one typo, and I have one question about what things look like to the end user in the worst case; otherwise LGTM.