Merge lp:~frankban/juju-quickstart/jujuutils into lp:juju-quickstart
- jujuutils
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 120 |
Proposed branch: | lp:~frankban/juju-quickstart/jujuutils |
Merge into: | lp:juju-quickstart |
Diff against target: |
1480 lines (+713/-599) 8 files modified
quickstart/app.py (+6/-4) quickstart/jujutools.py (+127/-0) quickstart/manage.py (+3/-2) quickstart/models/bundles.py (+118/-0) quickstart/tests/models/test_bundles.py (+254/-0) quickstart/tests/test_jujutools.py (+204/-0) quickstart/tests/test_utils.py (+0/-397) quickstart/utils.py (+1/-196) |
To merge this branch: | bzr merge lp:~frankban/juju-quickstart/jujuutils |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+249073@code.launchpad.net |
Commit message
Description of the change
Refactor/split utils code.
Move code from utils to the new jujutools and models/bundles modules.
This branch only include moved code and it's done
in preparation for the Juju API new login work.
Francesco Banconi (frankban) wrote : | # |
Brad Crittenden (bac) wrote : | # |
LGTM. Will do QA shortly.
https:/
File quickstart/
https:/
quickstart/
bundle_name=None):
Nice that you've made bundles a real person!
Brad Crittenden (bac) wrote : | # |
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Refactor/split utils code.
Move code from utils to the new jujutools and models/bundles modules.
This branch only include moved code and it's done
in preparation for the Juju API new login work.
R=bac
CC=
https:/
Francesco Banconi (frankban) wrote : | # |
Thanks for the review and QA Brad!
Preview Diff
1 | === modified file 'quickstart/app.py' |
2 | --- quickstart/app.py 2015-02-09 10:38:25 +0000 |
3 | +++ quickstart/app.py 2015-02-09 13:39:50 +0000 |
4 | @@ -30,6 +30,7 @@ |
5 | |
6 | from quickstart import ( |
7 | juju, |
8 | + jujutools, |
9 | netutils, |
10 | platform_support, |
11 | settings, |
12 | @@ -265,11 +266,11 @@ |
13 | continue |
14 | # Ensure the state server is up and the agent is started. |
15 | try: |
16 | - agent_state = utils.get_agent_state(output) |
17 | + agent_state = jujutools.get_agent_state(output) |
18 | except ValueError: |
19 | continue |
20 | if agent_state == 'started': |
21 | - return utils.get_bootstrap_node_series(output) |
22 | + return jujutools.get_bootstrap_node_series(output) |
23 | # If the agent is in an error state, there is nothing we can do, and |
24 | # it's not useful to keep trying. |
25 | if agent_state == 'error': |
26 | @@ -408,7 +409,8 @@ |
27 | status = env.get_status() |
28 | except jujuclient.EnvError as err: |
29 | raise ProgramExit('bad API response: {}'.format(err.message)) |
30 | - service_data, unit_data = utils.get_service_info(status, service_name) |
31 | + service_data, unit_data = jujutools.get_service_info( |
32 | + status, service_name) |
33 | if service_data is None: |
34 | # The service does not exist in the environment. |
35 | if charm_url is None: |
36 | @@ -429,7 +431,7 @@ |
37 | # A deployed service already exists in the environment: ignore the |
38 | # provided charm URL and just use the already deployed charm. |
39 | charm_url = service_data['CharmURL'] |
40 | - charm = utils.parse_gui_charm_url(charm_url) |
41 | + charm = jujutools.parse_gui_charm_url(charm_url) |
42 | # Deploy on the bootstrap node if the following conditions are satisfied: |
43 | # - we are not using the local provider (which uses localhost); |
44 | # - we are not using the azure provider (in which availability sets prevent |
45 | |
46 | === added file 'quickstart/jujutools.py' |
47 | --- quickstart/jujutools.py 1970-01-01 00:00:00 +0000 |
48 | +++ quickstart/jujutools.py 2015-02-09 13:39:50 +0000 |
49 | @@ -0,0 +1,127 @@ |
50 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
51 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
52 | +# Copyright (C) 2015 Canonical Ltd. |
53 | +# |
54 | +# This program is free software: you can redistribute it and/or modify it under |
55 | +# the terms of the GNU Affero General Public License version 3, as published by |
56 | +# the Free Software Foundation. |
57 | +# |
58 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
59 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
60 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
61 | +# Affero General Public License for more details. |
62 | +# |
63 | +# You should have received a copy of the GNU Affero General Public License |
64 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
65 | + |
66 | +"""Quickstart utility functions for managing Juju environments and entities.""" |
67 | + |
68 | +from __future__ import ( |
69 | + print_function, |
70 | + unicode_literals, |
71 | +) |
72 | + |
73 | +import logging |
74 | + |
75 | +from quickstart import ( |
76 | + serializers, |
77 | + settings, |
78 | +) |
79 | +from quickstart.models import charms |
80 | + |
81 | + |
82 | +def get_service_info(status, service_name): |
83 | + """Retrieve information on the given service and on its first alive unit. |
84 | + |
85 | + Return a tuple containing two values: (service data, unit data). |
86 | + Each value can be: |
87 | + - a dictionary of data about the given entity (service or unit) as |
88 | + returned by the Juju watcher; |
89 | + - None, if the entity is not present in the Juju environment. |
90 | + If the service data is None, the unit data is always None. |
91 | + """ |
92 | + services = [ |
93 | + data for entity, action, data in status if |
94 | + (entity == 'service') and (action != 'remove') and |
95 | + (data['Name'] == service_name) and (data['Life'] == 'alive') |
96 | + ] |
97 | + if not services: |
98 | + return None, None |
99 | + units = [ |
100 | + data for entity, action, data in status if |
101 | + entity == 'unit' and action != 'remove' and |
102 | + data['Service'] == service_name |
103 | + ] |
104 | + return services[0], units[0] if units else None |
105 | + |
106 | + |
107 | +def parse_gui_charm_url(charm_url): |
108 | + """Parse the given charm URL. |
109 | + |
110 | + Check if the charm looks like a Juju GUI charm. |
111 | + Print (to stdout or to logs) info and warnings about the charm URL. |
112 | + |
113 | + Return the parsed charm object as an instance of |
114 | + quickstart.models.charms.Charm. |
115 | + """ |
116 | + print('charm URL: {}'.format(charm_url)) |
117 | + charm = charms.Charm.from_url(charm_url) |
118 | + charm_name = settings.JUJU_GUI_CHARM_NAME |
119 | + if charm.name != charm_name: |
120 | + # This does not seem to be a Juju GUI charm. |
121 | + logging.warn( |
122 | + 'unexpected URL for the {} charm: ' |
123 | + 'the service may not work as expected'.format(charm_name)) |
124 | + return charm |
125 | + if charm.user or charm.is_local(): |
126 | + # This is not the official Juju GUI charm. |
127 | + logging.warn('using a customized {} charm'.format(charm_name)) |
128 | + elif charm.revision < settings.MINIMUM_REVISIONS_FOR_BUNDLES[charm.series]: |
129 | + # This is the official Juju GUI charm, but it is outdated. |
130 | + logging.warn( |
131 | + 'charm is outdated and may not support bundle deployments') |
132 | + return charm |
133 | + |
134 | + |
135 | +def parse_status_output(output, keys=None): |
136 | + """Parse the output of juju status. |
137 | + |
138 | + Return selection specified by the keys array. |
139 | + Raise a ValueError if the selection cannot be retrieved. |
140 | + """ |
141 | + if keys is None: |
142 | + keys = ['dummy'] |
143 | + try: |
144 | + status = serializers.yaml_load(output) |
145 | + except Exception as err: |
146 | + raise ValueError(b'unable to parse the output: {}'.format(err)) |
147 | + |
148 | + selection = status |
149 | + for key in keys: |
150 | + try: |
151 | + selection = selection.get(key, {}) |
152 | + except AttributeError as err: |
153 | + msg = 'invalid YAML contents: {}'.format(status) |
154 | + raise ValueError(msg.encode('utf-8')) |
155 | + if selection == {}: |
156 | + msg = '{} not found in {}'.format(':'.join(keys), status) |
157 | + raise ValueError(msg.encode('utf-8')) |
158 | + return selection |
159 | + |
160 | + |
161 | +def get_agent_state(output): |
162 | + """Parse the output of juju status for the agent state. |
163 | + |
164 | + Return the agent state. |
165 | + Raise a ValueError if the agent state cannot be retrieved. |
166 | + """ |
167 | + return parse_status_output(output, ['machines', '0', 'agent-state']) |
168 | + |
169 | + |
170 | +def get_bootstrap_node_series(output): |
171 | + """Parse the output of juju status for the agent state. |
172 | + |
173 | + Return the agent state. |
174 | + Raise a ValueError if the agent state cannot be retrieved. |
175 | + """ |
176 | + return parse_status_output(output, ['machines', '0', 'series']) |
177 | |
178 | === modified file 'quickstart/manage.py' |
179 | --- quickstart/manage.py 2015-01-30 15:27:07 +0000 |
180 | +++ quickstart/manage.py 2015-02-09 13:39:50 +0000 |
181 | @@ -43,6 +43,7 @@ |
182 | views, |
183 | ) |
184 | from quickstart.models import ( |
185 | + bundles, |
186 | charms, |
187 | envs, |
188 | jenv, |
189 | @@ -107,7 +108,7 @@ |
190 | if bundle.startswith('bundle:') or bundle.startswith(jujucharms_prefix): |
191 | # Convert "bundle:" or jujucharms.com URLs into Charmworld HTTPS ones. |
192 | try: |
193 | - bundle, bundle_id = utils.convert_bundle_url(bundle) |
194 | + bundle, bundle_id = bundles.convert_bundle_url(bundle) |
195 | except ValueError as err: |
196 | return parser.error('unable to open the bundle: {}'.format(err)) |
197 | # The next if block below will then load the bundle contents from the |
198 | @@ -130,7 +131,7 @@ |
199 | return parser.error('unable to open bundle file: {}'.format(err)) |
200 | # Validate the bundle. |
201 | try: |
202 | - bundle_name, bundle_services = utils.parse_bundle( |
203 | + bundle_name, bundle_services = bundles.parse_bundle( |
204 | bundle_yaml, options.bundle_name) |
205 | except ValueError as err: |
206 | return parser.error(bytes(err)) |
207 | |
208 | === added file 'quickstart/models/bundles.py' |
209 | --- quickstart/models/bundles.py 1970-01-01 00:00:00 +0000 |
210 | +++ quickstart/models/bundles.py 2015-02-09 13:39:50 +0000 |
211 | @@ -0,0 +1,118 @@ |
212 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
213 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
214 | +# Copyright (C) 2015 Canonical Ltd. |
215 | +# |
216 | +# This program is free software: you can redistribute it and/or modify it under |
217 | +# the terms of the GNU Affero General Public License version 3, as published by |
218 | +# the Free Software Foundation. |
219 | +# |
220 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
221 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
222 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
223 | +# Affero General Public License for more details. |
224 | +# |
225 | +# You should have received a copy of the GNU Affero General Public License |
226 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
227 | + |
228 | +"""Juju Quickstart bundles management.""" |
229 | + |
230 | +from __future__ import unicode_literals |
231 | + |
232 | +import collections |
233 | +import re |
234 | + |
235 | +from quickstart import ( |
236 | + serializers, |
237 | + settings, |
238 | +) |
239 | + |
240 | + |
241 | +# Compile the regular expression used to parse bundle URLs. |
242 | +_bundle_expression = re.compile(r""" |
243 | + # Bundle schema or bundle URL namespace on jujucharms.com. |
244 | + ^(?:bundle:|{}) |
245 | + (?:~([-\w]+)/)? # Optional user name. |
246 | + ([-\w]+)/ # Basket name. |
247 | + (?:(\d+)/)? # Optional bundle revision number. |
248 | + ([-\w]+) # Bundle name. |
249 | + /?$ # Optional trailing slash. |
250 | +""".format(settings.JUJUCHARMS_BUNDLE_URL), re.VERBOSE) |
251 | + |
252 | + |
253 | +def convert_bundle_url(bundle_url): |
254 | + """Return the equivalent YAML HTTPS location for the given bundle URL. |
255 | + |
256 | + Raise a ValueError if the given URL is not a valid bundle URL. |
257 | + """ |
258 | + match = _bundle_expression.match(bundle_url) |
259 | + if match is None: |
260 | + msg = 'invalid bundle URL: {}'.format(bundle_url) |
261 | + raise ValueError(msg.encode('utf-8')) |
262 | + user, basket, revision, name = match.groups() |
263 | + user_part = '~charmers/' if user is None else '~{}/'.format(user) |
264 | + revision_part = '' if revision is None else '{}/'.format(revision) |
265 | + bundle_id = '{}{}/{}{}'.format(user_part, basket, revision_part, name) |
266 | + return ('https://manage.jujucharms.com/bundle/{}/json'.format(bundle_id), |
267 | + bundle_id) |
268 | + |
269 | + |
270 | +def parse_bundle(bundle_yaml, bundle_name=None): |
271 | + """Parse the provided bundle YAML encoded contents. |
272 | + |
273 | + Since a valid JSON is a subset of YAML this function can be used also to |
274 | + parse JSON encoded contents. |
275 | + |
276 | + Return a tuple containing the bundle name and the list of services included |
277 | + in the bundle. |
278 | + |
279 | + Raise a ValueError if: |
280 | + - the bundle YAML contents are not parsable by YAML; |
281 | + - the YAML contents are not properly structured; |
282 | + - the bundle name is specified but not included in the bundle file; |
283 | + - the bundle name is not specified and the bundle file includes more than |
284 | + one bundle; |
285 | + - the bundle does not include services. |
286 | + """ |
287 | + # Parse the bundle file. |
288 | + try: |
289 | + bundles = serializers.yaml_load(bundle_yaml) |
290 | + except Exception as err: |
291 | + msg = b'unable to parse the bundle: {}'.format(err) |
292 | + raise ValueError(msg) |
293 | + # Ensure the bundle file is well formed and contains at least one bundle. |
294 | + if not isinstance(bundles, collections.Mapping): |
295 | + msg = 'invalid YAML contents: {}'.format(bundle_yaml) |
296 | + raise ValueError(msg.encode('utf-8')) |
297 | + try: |
298 | + name_services_map = dict( |
299 | + (key, value['services'].keys()) |
300 | + for key, value in bundles.items() |
301 | + ) |
302 | + except (AttributeError, KeyError, TypeError): |
303 | + msg = 'invalid YAML contents: {}'.format(bundle_yaml) |
304 | + raise ValueError(msg.encode('utf-8')) |
305 | + if not name_services_map: |
306 | + raise ValueError(b'no bundles found') |
307 | + # Retrieve the bundle name and services. |
308 | + if bundle_name is None: |
309 | + if len(name_services_map) > 1: |
310 | + msg = 'multiple bundles found ({}) but no bundle name specified' |
311 | + bundle_names = ', '.join(sorted(name_services_map.keys())) |
312 | + raise ValueError(msg.format(bundle_names).encode('utf-8')) |
313 | + bundle_name, bundle_services = name_services_map.items()[0] |
314 | + else: |
315 | + bundle_services = name_services_map.get(bundle_name) |
316 | + if bundle_services is None: |
317 | + msg = 'bundle {} not found in the provided list of bundles ({})' |
318 | + bundle_names = ', '.join(sorted(name_services_map.keys())) |
319 | + raise ValueError( |
320 | + msg.format(bundle_name, bundle_names).encode('utf-8')) |
321 | + if not bundle_services: |
322 | + msg = 'bundle {} does not include any services'.format(bundle_name) |
323 | + raise ValueError(msg.encode('utf-8')) |
324 | + if settings.JUJU_GUI_SERVICE_NAME in bundle_services: |
325 | + msg = ('bundle {} contains an instance of juju-gui. quickstart will ' |
326 | + 'install the latest version of the Juju GUI automatically, ' |
327 | + 'please remove juju-gui from the bundle.'.format(bundle_name)) |
328 | + raise ValueError(msg.encode('utf-8')) |
329 | + return bundle_name, bundle_services |
330 | |
331 | === added file 'quickstart/tests/models/test_bundles.py' |
332 | --- quickstart/tests/models/test_bundles.py 1970-01-01 00:00:00 +0000 |
333 | +++ quickstart/tests/models/test_bundles.py 2015-02-09 13:39:50 +0000 |
334 | @@ -0,0 +1,254 @@ |
335 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
336 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
337 | +# Copyright (C) 2015 Canonical Ltd. |
338 | +# |
339 | +# This program is free software: you can redistribute it and/or modify it under |
340 | +# the terms of the GNU Affero General Public License version 3, as published by |
341 | +# the Free Software Foundation. |
342 | +# |
343 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
344 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
345 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
346 | +# Affero General Public License for more details. |
347 | +# |
348 | +# You should have received a copy of the GNU Affero General Public License |
349 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
350 | + |
351 | +"""Tests for the Juju Quickstart bundles management.""" |
352 | + |
353 | +from __future__ import unicode_literals |
354 | + |
355 | +import json |
356 | +import unittest |
357 | + |
358 | +import yaml |
359 | + |
360 | +from quickstart import settings |
361 | +from quickstart.models import bundles |
362 | +from quickstart.tests import helpers |
363 | + |
364 | + |
365 | +class TestConvertBundleUrl(helpers.ValueErrorTestsMixin, unittest.TestCase): |
366 | + |
367 | + def test_full_bundle_url(self): |
368 | + # The HTTPS location to the YAML contents is correctly returned. |
369 | + bundle_url = 'bundle:~myuser/wiki-bundle/42/wiki' |
370 | + url, bundle_id = bundles.convert_bundle_url(bundle_url) |
371 | + self.assertEqual( |
372 | + 'https://manage.jujucharms.com' |
373 | + '/bundle/~myuser/wiki-bundle/42/wiki/json', url) |
374 | + self.assertEqual('~myuser/wiki-bundle/42/wiki', bundle_id) |
375 | + |
376 | + def test_bundle_url_right_strip(self): |
377 | + # The trailing slash in the bundle URL is removed. |
378 | + bundle_url = 'bundle:~myuser/wiki-bundle/42/wiki/' |
379 | + url, bundle_id = bundles.convert_bundle_url(bundle_url) |
380 | + self.assertEqual( |
381 | + 'https://manage.jujucharms.com' |
382 | + '/bundle/~myuser/wiki-bundle/42/wiki/json', url) |
383 | + self.assertEqual('~myuser/wiki-bundle/42/wiki', bundle_id) |
384 | + |
385 | + def test_bundle_url_no_revision(self): |
386 | + # The bundle revision is optional. |
387 | + bundle_url = 'bundle:~myuser/wiki-bundle/wiki-simple' |
388 | + url, bundle_id = bundles.convert_bundle_url(bundle_url) |
389 | + self.assertEqual( |
390 | + 'https://manage.jujucharms.com' |
391 | + '/bundle/~myuser/wiki-bundle/wiki-simple/json', url) |
392 | + self.assertEqual('~myuser/wiki-bundle/wiki-simple', bundle_id) |
393 | + |
394 | + def test_bundle_url_no_user(self): |
395 | + # If the bundle user is not specified, the bundle is assumed to be |
396 | + # promulgated and owned by "charmers". |
397 | + bundle_url = 'bundle:wiki-bundle/1/wiki' |
398 | + url, bundle_id = bundles.convert_bundle_url(bundle_url) |
399 | + self.assertEqual( |
400 | + 'https://manage.jujucharms.com' |
401 | + '/bundle/~charmers/wiki-bundle/1/wiki/json', url) |
402 | + self.assertEqual('~charmers/wiki-bundle/1/wiki', bundle_id) |
403 | + |
404 | + def test_bundle_url_short_form(self): |
405 | + # A promulgated bundle URL can just include the basket and the name. |
406 | + bundle_url = 'bundle:wiki-bundle/wiki' |
407 | + url, bundle_id = bundles.convert_bundle_url(bundle_url) |
408 | + self.assertEqual( |
409 | + 'https://manage.jujucharms.com' |
410 | + '/bundle/~charmers/wiki-bundle/wiki/json', url) |
411 | + self.assertEqual('~charmers/wiki-bundle/wiki', bundle_id) |
412 | + |
413 | + def test_full_jujucharms_url(self): |
414 | + # The HTTPS location to the YAML contents is correctly returned. |
415 | + url, bundle_id = bundles.convert_bundle_url( |
416 | + settings.JUJUCHARMS_BUNDLE_URL + '~myuser/wiki-bundle/42/wiki') |
417 | + self.assertEqual( |
418 | + 'https://manage.jujucharms.com' |
419 | + '/bundle/~myuser/wiki-bundle/42/wiki/json', url) |
420 | + self.assertEqual('~myuser/wiki-bundle/42/wiki', bundle_id) |
421 | + |
422 | + def test_jujucharms_url_right_strip(self): |
423 | + # The trailing slash in the jujucharms URL is removed. |
424 | + url, bundle_id = bundles.convert_bundle_url( |
425 | + settings.JUJUCHARMS_BUNDLE_URL + '~charmers/mediawiki/6/scalable/') |
426 | + self.assertEqual( |
427 | + 'https://manage.jujucharms.com' |
428 | + '/bundle/~charmers/mediawiki/6/scalable/json', url) |
429 | + self.assertEqual('~charmers/mediawiki/6/scalable', bundle_id) |
430 | + |
431 | + def test_jujucharms_url_no_revision(self): |
432 | + # The bundle revision is optional. |
433 | + url, bundle_id = bundles.convert_bundle_url( |
434 | + settings.JUJUCHARMS_BUNDLE_URL + '~myuser/wiki/wiki-simple/') |
435 | + self.assertEqual( |
436 | + 'https://manage.jujucharms.com' |
437 | + '/bundle/~myuser/wiki/wiki-simple/json', url) |
438 | + self.assertEqual('~myuser/wiki/wiki-simple', bundle_id) |
439 | + |
440 | + def test_jujucharms_url_no_user(self): |
441 | + # If the bundle user is not specified, the bundle is assumed to be |
442 | + # promulgated and owned by "charmers". |
443 | + url, bundle_id = bundles.convert_bundle_url( |
444 | + settings.JUJUCHARMS_BUNDLE_URL + 'mediawiki/42/single/') |
445 | + self.assertEqual( |
446 | + 'https://manage.jujucharms.com' |
447 | + '/bundle/~charmers/mediawiki/42/single/json', url) |
448 | + self.assertEqual('~charmers/mediawiki/42/single', bundle_id) |
449 | + |
450 | + def test_jujucharms_url_short_form(self): |
451 | + # A jujucharms URL for a promulgated bundle can just include the basket |
452 | + # and the name. |
453 | + url, bundle_id = bundles.convert_bundle_url( |
454 | + settings.JUJUCHARMS_BUNDLE_URL + 'wiki-bundle/wiki/') |
455 | + self.assertEqual( |
456 | + 'https://manage.jujucharms.com' |
457 | + '/bundle/~charmers/wiki-bundle/wiki/json', url) |
458 | + self.assertEqual('~charmers/wiki-bundle/wiki', bundle_id) |
459 | + |
460 | + def test_error(self): |
461 | + # A ValueError is raised if the bundle/jujucharms URL is not valid. |
462 | + bad_urls = ( |
463 | + 'bad', 'bundle:', 'bundle:~user', 'bundle:no-such', |
464 | + 'bundle:~user/name', 'bundle:~user/basket/revision/name', |
465 | + 'bundle:basket/name//', 'bundle:basket.name/bundle.name', |
466 | + settings.JUJUCHARMS_BUNDLE_URL, |
467 | + settings.JUJUCHARMS_BUNDLE_URL + 'bad', |
468 | + settings.JUJUCHARMS_BUNDLE_URL + '~user/no-such', |
469 | + settings.JUJUCHARMS_BUNDLE_URL + '~user/basket/revision/name/', |
470 | + settings.JUJUCHARMS_BUNDLE_URL + '~user/basket/42/name/error', |
471 | + 'https://jujucharms.com/charms/mediawiki/simple/', |
472 | + ) |
473 | + for url in bad_urls: |
474 | + with self.assert_value_error('invalid bundle URL: {}'.format(url)): |
475 | + bundles.convert_bundle_url(url) |
476 | + |
477 | + |
478 | +class TestParseBundle( |
479 | + helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin, |
480 | + unittest.TestCase): |
481 | + |
482 | + def assert_bundle( |
483 | + self, expected_name, expected_services, contents, |
484 | + bundle_name=None): |
485 | + """Ensure parsing the given contents returns the expected values.""" |
486 | + name, services = bundles.parse_bundle( |
487 | + contents, bundle_name=bundle_name) |
488 | + self.assertEqual(expected_name, name) |
489 | + self.assertEqual(set(expected_services), set(services)) |
490 | + |
491 | + def test_invalid_yaml(self): |
492 | + # A ValueError is raised if the bundle contents are not a valid YAML. |
493 | + with self.assertRaises(ValueError) as context_manager: |
494 | + bundles.parse_bundle(':') |
495 | + expected = 'unable to parse the bundle' |
496 | + self.assertIn(expected, bytes(context_manager.exception)) |
497 | + |
498 | + def test_yaml_invalid_type(self): |
499 | + # A ValueError is raised if the bundle contents are not well formed. |
500 | + with self.assert_value_error('invalid YAML contents: a-string'): |
501 | + bundles.parse_bundle('a-string') |
502 | + |
503 | + def test_yaml_invalid_bundle_data(self): |
504 | + # A ValueError is raised if bundles are not well formed. |
505 | + contents = yaml.safe_dump({'mybundle': 'not valid'}) |
506 | + expected = 'invalid YAML contents: {mybundle: not valid}\n' |
507 | + with self.assert_value_error(expected): |
508 | + bundles.parse_bundle(contents) |
509 | + |
510 | + def test_yaml_no_service(self): |
511 | + # A ValueError is raised if bundles do not include services. |
512 | + contents = yaml.safe_dump({'mybundle': {}}) |
513 | + expected = 'invalid YAML contents: mybundle: {}\n' |
514 | + with self.assert_value_error(expected): |
515 | + bundles.parse_bundle(contents) |
516 | + |
517 | + def test_yaml_none_bundle_services(self): |
518 | + # A ValueError is raised if services are None. |
519 | + contents = yaml.safe_dump({'mybundle': {'services': None}}) |
520 | + expected = 'invalid YAML contents: mybundle: {services: null}\n' |
521 | + with self.assert_value_error(expected): |
522 | + bundles.parse_bundle(contents) |
523 | + |
524 | + def test_yaml_invalid_bundle_services_type(self): |
525 | + # A ValueError is raised if services have an invalid type. |
526 | + contents = yaml.safe_dump({'mybundle': {'services': 42}}) |
527 | + expected = 'invalid YAML contents: mybundle: {services: 42}\n' |
528 | + with self.assert_value_error(expected): |
529 | + bundles.parse_bundle(contents) |
530 | + |
531 | + def test_yaml_no_bundles(self): |
532 | + # A ValueError is raised if the bundle contents are empty. |
533 | + with self.assert_value_error('no bundles found'): |
534 | + bundles.parse_bundle(yaml.safe_dump({})) |
535 | + |
536 | + def test_bundle_name_not_specified(self): |
537 | + # A ValueError is raised if the bundle name is not specified and the |
538 | + # contents contain more than one bundle. |
539 | + expected = ('multiple bundles found (bundle1, bundle2) ' |
540 | + 'but no bundle name specified') |
541 | + with self.assert_value_error(expected): |
542 | + bundles.parse_bundle(self.valid_bundle) |
543 | + |
544 | + def test_bundle_name_not_found(self): |
545 | + # A ValueError is raised if the given bundle is not found in the file. |
546 | + expected = ('bundle no-such not found in the provided list of bundles ' |
547 | + '(bundle1, bundle2)') |
548 | + with self.assert_value_error(expected): |
549 | + bundles.parse_bundle(self.valid_bundle, 'no-such') |
550 | + |
551 | + def test_no_services(self): |
552 | + # A ValueError is raised if the specified bundle does not contain |
553 | + # services. |
554 | + contents = yaml.safe_dump({'mybundle': {'services': {}}}) |
555 | + expected = 'bundle mybundle does not include any services' |
556 | + with self.assert_value_error(expected): |
557 | + bundles.parse_bundle(contents) |
558 | + |
559 | + def test_yaml_gui_in_services(self): |
560 | + # A ValueError is raised if the bundle contains juju-gui. |
561 | + contents = yaml.safe_dump({ |
562 | + 'mybundle': {'services': {settings.JUJU_GUI_SERVICE_NAME: {}}}, |
563 | + }) |
564 | + expected = 'bundle mybundle contains an instance of juju-gui. ' \ |
565 | + 'quickstart will install the latest version of the Juju GUI ' \ |
566 | + 'automatically, please remove juju-gui from the bundle.' |
567 | + with self.assert_value_error(expected): |
568 | + bundles.parse_bundle(contents) |
569 | + |
570 | + def test_success_no_name(self): |
571 | + # The function succeeds when an implicit bundle name is used. |
572 | + contents = yaml.safe_dump({ |
573 | + 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}}, |
574 | + }) |
575 | + self.assert_bundle('mybundle', ['mysql', 'wordpress'], contents) |
576 | + |
577 | + def test_success_multiple_bundles(self): |
578 | + # The function succeeds with multiple bundles. |
579 | + self.assert_bundle( |
580 | + 'bundle2', ['django', 'nodejs'], self.valid_bundle, 'bundle2') |
581 | + |
582 | + def test_success_json(self): |
583 | + # Since JSON is a subset of YAML, the function also support JSON |
584 | + # encoded bundles. |
585 | + contents = json.dumps({ |
586 | + 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}}, |
587 | + }) |
588 | + self.assert_bundle('mybundle', ['mysql', 'wordpress'], contents) |
589 | |
590 | === added file 'quickstart/tests/test_jujutools.py' |
591 | --- quickstart/tests/test_jujutools.py 1970-01-01 00:00:00 +0000 |
592 | +++ quickstart/tests/test_jujutools.py 2015-02-09 13:39:50 +0000 |
593 | @@ -0,0 +1,204 @@ |
594 | +# This file is part of the Juju Quickstart Plugin, which lets users set up a |
595 | +# Juju environment in very few steps (https://launchpad.net/juju-quickstart). |
596 | +# Copyright (C) 2015 Canonical Ltd. |
597 | +# |
598 | +# This program is free software: you can redistribute it and/or modify it under |
599 | +# the terms of the GNU Affero General Public License version 3, as published by |
600 | +# the Free Software Foundation. |
601 | +# |
602 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
603 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
604 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
605 | +# Affero General Public License for more details. |
606 | +# |
607 | +# You should have received a copy of the GNU Affero General Public License |
608 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
609 | + |
610 | +"""Tests for the Quickstart Juju utilities and helpers.""" |
611 | + |
612 | +from __future__ import unicode_literals |
613 | + |
614 | +import unittest |
615 | + |
616 | +import mock |
617 | +import yaml |
618 | + |
619 | +from quickstart import jujutools |
620 | +from quickstart.models import charms |
621 | +from quickstart.tests import helpers |
622 | + |
623 | + |
624 | +class TestGetServiceInfo(helpers.WatcherDataTestsMixin, unittest.TestCase): |
625 | + |
626 | + def test_service_and_unit(self): |
627 | + # The data about the given service and unit is correctly returned. |
628 | + service_change = self.make_service_change() |
629 | + unit_change = self.make_unit_change() |
630 | + status = [service_change, unit_change] |
631 | + expected = (service_change[2], unit_change[2]) |
632 | + self.assertEqual( |
633 | + expected, jujutools.get_service_info(status, 'my-gui')) |
634 | + |
635 | + def test_service_only(self): |
636 | + # The data about the given service without units is correctly returned. |
637 | + service_change = self.make_service_change() |
638 | + status = [service_change] |
639 | + expected = (service_change[2], None) |
640 | + self.assertEqual( |
641 | + expected, jujutools.get_service_info(status, 'my-gui')) |
642 | + |
643 | + def test_service_removed(self): |
644 | + # A tuple (None, None) is returned if the service is being removed. |
645 | + status = [ |
646 | + self.make_service_change(action='remove'), |
647 | + self.make_unit_change(), |
648 | + ] |
649 | + expected = (None, None) |
650 | + self.assertEqual( |
651 | + expected, jujutools.get_service_info(status, 'my-gui')) |
652 | + |
653 | + def test_another_service(self): |
654 | + # A tuple (None, None) is returned if the service is not found. |
655 | + status = [ |
656 | + self.make_service_change(data={'Name': 'another-service'}), |
657 | + self.make_unit_change(), |
658 | + ] |
659 | + expected = (None, None) |
660 | + self.assertEqual( |
661 | + expected, jujutools.get_service_info(status, 'my-gui')) |
662 | + |
663 | + def test_service_not_alive(self): |
664 | + # A tuple (None, None) is returned if the service is not alive. |
665 | + status = [ |
666 | + self.make_service_change(data={'Life': 'dying'}), |
667 | + self.make_unit_change(), |
668 | + ] |
669 | + expected = (None, None) |
670 | + self.assertEqual( |
671 | + expected, jujutools.get_service_info(status, 'my-gui')) |
672 | + |
673 | + def test_unit_removed(self): |
674 | + # The unit data is not returned if the unit is being removed. |
675 | + service_change = self.make_service_change() |
676 | + status = [service_change, self.make_unit_change(action='remove')] |
677 | + expected = (service_change[2], None) |
678 | + self.assertEqual( |
679 | + expected, jujutools.get_service_info(status, 'my-gui')) |
680 | + |
681 | + def test_another_unit(self): |
682 | + # The unit data is not returned if the unit belongs to another service. |
683 | + service_change = self.make_service_change() |
684 | + status = [ |
685 | + service_change, |
686 | + self.make_unit_change(data={'Service': 'another-service'}), |
687 | + ] |
688 | + expected = (service_change[2], None) |
689 | + self.assertEqual( |
690 | + expected, jujutools.get_service_info(status, 'my-gui')) |
691 | + |
692 | + def test_no_services(self): |
693 | + # A tuple (None, None) is returned no services are found. |
694 | + status = [self.make_unit_change()] |
695 | + expected = (None, None) |
696 | + self.assertEqual( |
697 | + expected, jujutools.get_service_info(status, 'my-gui')) |
698 | + |
699 | + def test_no_entities(self): |
700 | + # A tuple (None, None) is returned no entities are found. |
701 | + expected = (None, None) |
702 | + self.assertEqual(expected, jujutools.get_service_info([], 'my-gui')) |
703 | + |
704 | + |
705 | +@mock.patch('__builtin__.print', mock.Mock()) |
706 | +class TestParseGuiCharmUrl(unittest.TestCase): |
707 | + |
708 | + def test_charm_instance_returned(self): |
709 | + # A charm instance is correctly returned. |
710 | + charm = jujutools.parse_gui_charm_url('cs:trusty/juju-gui-42') |
711 | + self.assertIsInstance(charm, charms.Charm) |
712 | + self.assertEqual('cs:trusty/juju-gui-42', charm.url()) |
713 | + |
714 | + def test_customized(self): |
715 | + # A customized charm URL is properly logged. |
716 | + expected = 'using a customized juju-gui charm' |
717 | + with helpers.assert_logs([expected], level='warn'): |
718 | + jujutools.parse_gui_charm_url('cs:~juju-gui/precise/juju-gui-28') |
719 | + |
720 | + def test_outdated(self): |
721 | + # An outdated charm URL is properly logged. |
722 | + expected = 'charm is outdated and may not support bundle deployments' |
723 | + with helpers.assert_logs([expected], level='warn'): |
724 | + jujutools.parse_gui_charm_url('cs:precise/juju-gui-1') |
725 | + |
726 | + def test_unexpected(self): |
727 | + # An unexpected charm URL is properly logged. |
728 | + expected = ( |
729 | + 'unexpected URL for the juju-gui charm: the service may not work ' |
730 | + 'as expected') |
731 | + with helpers.assert_logs([expected], level='warn'): |
732 | + jujutools.parse_gui_charm_url('cs:precise/another-gui-42') |
733 | + |
734 | + def test_official(self): |
735 | + # No warnings are logged if an up to date charm is passed. |
736 | + with mock.patch('logging.warn') as mock_warn: |
737 | + jujutools.parse_gui_charm_url('cs:precise/juju-gui-100') |
738 | + self.assertFalse(mock_warn.called) |
739 | + |
740 | + |
741 | +class TestParseStatusOutput(helpers.ValueErrorTestsMixin, unittest.TestCase): |
742 | + |
743 | + def test_invalid_yaml(self): |
744 | + # A ValueError is raised if the output is not a valid YAML. |
745 | + with self.assertRaises(ValueError) as context_manager: |
746 | + jujutools.parse_status_output(':') |
747 | + expected = 'unable to parse the output' |
748 | + self.assertIn(expected, bytes(context_manager.exception)) |
749 | + |
750 | + def test_invalid_yaml_contents(self): |
751 | + # A ValueError is raised if the output is not well formed. |
752 | + with self.assert_value_error('invalid YAML contents: a-string'): |
753 | + jujutools.parse_status_output('a-string') |
754 | + |
755 | + def test_no_agent_state(self): |
756 | + # A ValueError is raised if the agent-state is not found in the YAML. |
757 | + data = { |
758 | + 'machines': { |
759 | + '0': {'agent-version': '1.17.0.1'}, |
760 | + }, |
761 | + } |
762 | + expected = 'machines:0:agent-state not found in {}'.format(bytes(data)) |
763 | + with self.assert_value_error(expected): |
764 | + jujutools.get_agent_state(yaml.safe_dump(data)) |
765 | + |
766 | + def test_success_agent_state(self): |
767 | + # The agent state is correctly returned. |
768 | + output = yaml.safe_dump({ |
769 | + 'machines': { |
770 | + '0': {'agent-version': '1.17.0.1', 'agent-state': 'started'}, |
771 | + }, |
772 | + }) |
773 | + agent_state = jujutools.get_agent_state(output) |
774 | + self.assertEqual('started', agent_state) |
775 | + |
776 | + def test_no_bootstrap_node_series(self): |
777 | + # A ValueError is raised if the series is not found in the YAML. |
778 | + data = { |
779 | + 'machines': { |
780 | + '0': {'agent-version': '1.17.0.1'}, |
781 | + }, |
782 | + } |
783 | + expected = 'machines:0:series not found in {}'.format(bytes(data)) |
784 | + with self.assert_value_error(expected): |
785 | + jujutools.get_bootstrap_node_series(yaml.safe_dump(data)) |
786 | + |
787 | + def test_success_bootstrap_node_series(self): |
788 | + # The bootstrap node series is correctly returned. |
789 | + output = yaml.safe_dump({ |
790 | + 'machines': { |
791 | + '0': {'agent-version': '1.17.0.1', |
792 | + 'agent-state': 'started', |
793 | + 'series': 'zydeco'}, |
794 | + }, |
795 | + }) |
796 | + bsn_series = jujutools.get_bootstrap_node_series(output) |
797 | + self.assertEqual('zydeco', bsn_series) |
798 | |
799 | === modified file 'quickstart/tests/test_utils.py' |
800 | --- quickstart/tests/test_utils.py 2014-11-11 19:08:57 +0000 |
801 | +++ quickstart/tests/test_utils.py 2015-02-09 13:39:50 +0000 |
802 | @@ -19,21 +19,17 @@ |
803 | from __future__ import unicode_literals |
804 | |
805 | import datetime |
806 | -import json |
807 | import os |
808 | import shutil |
809 | import tempfile |
810 | import unittest |
811 | |
812 | import mock |
813 | -import yaml |
814 | |
815 | from quickstart import ( |
816 | get_version, |
817 | - settings, |
818 | utils, |
819 | ) |
820 | -from quickstart.models import charms |
821 | from quickstart.tests import helpers |
822 | |
823 | |
824 | @@ -153,155 +149,6 @@ |
825 | utils.call('echo', 'we are the borg!') |
826 | |
827 | |
828 | -@mock.patch('__builtin__.print', mock.Mock()) |
829 | -class TestParseGuiCharmUrl(unittest.TestCase): |
830 | - |
831 | - def test_charm_instance_returned(self): |
832 | - # A charm instance is correctly returned. |
833 | - charm = utils.parse_gui_charm_url('cs:trusty/juju-gui-42') |
834 | - self.assertIsInstance(charm, charms.Charm) |
835 | - self.assertEqual('cs:trusty/juju-gui-42', charm.url()) |
836 | - |
837 | - def test_customized(self): |
838 | - # A customized charm URL is properly logged. |
839 | - expected = 'using a customized juju-gui charm' |
840 | - with helpers.assert_logs([expected], level='warn'): |
841 | - utils.parse_gui_charm_url('cs:~juju-gui/precise/juju-gui-28') |
842 | - |
843 | - def test_outdated(self): |
844 | - # An outdated charm URL is properly logged. |
845 | - expected = 'charm is outdated and may not support bundle deployments' |
846 | - with helpers.assert_logs([expected], level='warn'): |
847 | - utils.parse_gui_charm_url('cs:precise/juju-gui-1') |
848 | - |
849 | - def test_unexpected(self): |
850 | - # An unexpected charm URL is properly logged. |
851 | - expected = ( |
852 | - 'unexpected URL for the juju-gui charm: the service may not work ' |
853 | - 'as expected') |
854 | - with helpers.assert_logs([expected], level='warn'): |
855 | - utils.parse_gui_charm_url('cs:precise/another-gui-42') |
856 | - |
857 | - def test_official(self): |
858 | - # No warnings are logged if an up to date charm is passed. |
859 | - with mock.patch('logging.warn') as mock_warn: |
860 | - utils.parse_gui_charm_url('cs:precise/juju-gui-100') |
861 | - self.assertFalse(mock_warn.called) |
862 | - |
863 | - |
864 | -class TestConvertBundleUrl(helpers.ValueErrorTestsMixin, unittest.TestCase): |
865 | - |
866 | - def test_full_bundle_url(self): |
867 | - # The HTTPS location to the YAML contents is correctly returned. |
868 | - bundle_url = 'bundle:~myuser/wiki-bundle/42/wiki' |
869 | - url, bundle_id = utils.convert_bundle_url(bundle_url) |
870 | - self.assertEqual( |
871 | - 'https://manage.jujucharms.com' |
872 | - '/bundle/~myuser/wiki-bundle/42/wiki/json', url) |
873 | - self.assertEqual('~myuser/wiki-bundle/42/wiki', bundle_id) |
874 | - |
875 | - def test_bundle_url_right_strip(self): |
876 | - # The trailing slash in the bundle URL is removed. |
877 | - bundle_url = 'bundle:~myuser/wiki-bundle/42/wiki/' |
878 | - url, bundle_id = utils.convert_bundle_url(bundle_url) |
879 | - self.assertEqual( |
880 | - 'https://manage.jujucharms.com' |
881 | - '/bundle/~myuser/wiki-bundle/42/wiki/json', url) |
882 | - self.assertEqual('~myuser/wiki-bundle/42/wiki', bundle_id) |
883 | - |
884 | - def test_bundle_url_no_revision(self): |
885 | - # The bundle revision is optional. |
886 | - bundle_url = 'bundle:~myuser/wiki-bundle/wiki-simple' |
887 | - url, bundle_id = utils.convert_bundle_url(bundle_url) |
888 | - self.assertEqual( |
889 | - 'https://manage.jujucharms.com' |
890 | - '/bundle/~myuser/wiki-bundle/wiki-simple/json', url) |
891 | - self.assertEqual('~myuser/wiki-bundle/wiki-simple', bundle_id) |
892 | - |
893 | - def test_bundle_url_no_user(self): |
894 | - # If the bundle user is not specified, the bundle is assumed to be |
895 | - # promulgated and owned by "charmers". |
896 | - bundle_url = 'bundle:wiki-bundle/1/wiki' |
897 | - url, bundle_id = utils.convert_bundle_url(bundle_url) |
898 | - self.assertEqual( |
899 | - 'https://manage.jujucharms.com' |
900 | - '/bundle/~charmers/wiki-bundle/1/wiki/json', url) |
901 | - self.assertEqual('~charmers/wiki-bundle/1/wiki', bundle_id) |
902 | - |
903 | - def test_bundle_url_short_form(self): |
904 | - # A promulgated bundle URL can just include the basket and the name. |
905 | - bundle_url = 'bundle:wiki-bundle/wiki' |
906 | - url, bundle_id = utils.convert_bundle_url(bundle_url) |
907 | - self.assertEqual( |
908 | - 'https://manage.jujucharms.com' |
909 | - '/bundle/~charmers/wiki-bundle/wiki/json', url) |
910 | - self.assertEqual('~charmers/wiki-bundle/wiki', bundle_id) |
911 | - |
912 | - def test_full_jujucharms_url(self): |
913 | - # The HTTPS location to the YAML contents is correctly returned. |
914 | - url, bundle_id = utils.convert_bundle_url( |
915 | - settings.JUJUCHARMS_BUNDLE_URL + '~myuser/wiki-bundle/42/wiki') |
916 | - self.assertEqual( |
917 | - 'https://manage.jujucharms.com' |
918 | - '/bundle/~myuser/wiki-bundle/42/wiki/json', url) |
919 | - self.assertEqual('~myuser/wiki-bundle/42/wiki', bundle_id) |
920 | - |
921 | - def test_jujucharms_url_right_strip(self): |
922 | - # The trailing slash in the jujucharms URL is removed. |
923 | - url, bundle_id = utils.convert_bundle_url( |
924 | - settings.JUJUCHARMS_BUNDLE_URL + '~charmers/mediawiki/6/scalable/') |
925 | - self.assertEqual( |
926 | - 'https://manage.jujucharms.com' |
927 | - '/bundle/~charmers/mediawiki/6/scalable/json', url) |
928 | - self.assertEqual('~charmers/mediawiki/6/scalable', bundle_id) |
929 | - |
930 | - def test_jujucharms_url_no_revision(self): |
931 | - # The bundle revision is optional. |
932 | - url, bundle_id = utils.convert_bundle_url( |
933 | - settings.JUJUCHARMS_BUNDLE_URL + '~myuser/wiki/wiki-simple/') |
934 | - self.assertEqual( |
935 | - 'https://manage.jujucharms.com' |
936 | - '/bundle/~myuser/wiki/wiki-simple/json', url) |
937 | - self.assertEqual('~myuser/wiki/wiki-simple', bundle_id) |
938 | - |
939 | - def test_jujucharms_url_no_user(self): |
940 | - # If the bundle user is not specified, the bundle is assumed to be |
941 | - # promulgated and owned by "charmers". |
942 | - url, bundle_id = utils.convert_bundle_url( |
943 | - settings.JUJUCHARMS_BUNDLE_URL + 'mediawiki/42/single/') |
944 | - self.assertEqual( |
945 | - 'https://manage.jujucharms.com' |
946 | - '/bundle/~charmers/mediawiki/42/single/json', url) |
947 | - self.assertEqual('~charmers/mediawiki/42/single', bundle_id) |
948 | - |
949 | - def test_jujucharms_url_short_form(self): |
950 | - # A jujucharms URL for a promulgated bundle can just include the basket |
951 | - # and the name. |
952 | - url, bundle_id = utils.convert_bundle_url( |
953 | - settings.JUJUCHARMS_BUNDLE_URL + 'wiki-bundle/wiki/') |
954 | - self.assertEqual( |
955 | - 'https://manage.jujucharms.com' |
956 | - '/bundle/~charmers/wiki-bundle/wiki/json', url) |
957 | - self.assertEqual('~charmers/wiki-bundle/wiki', bundle_id) |
958 | - |
959 | - def test_error(self): |
960 | - # A ValueError is raised if the bundle/jujucharms URL is not valid. |
961 | - bad_urls = ( |
962 | - 'bad', 'bundle:', 'bundle:~user', 'bundle:no-such', |
963 | - 'bundle:~user/name', 'bundle:~user/basket/revision/name', |
964 | - 'bundle:basket/name//', 'bundle:basket.name/bundle.name', |
965 | - settings.JUJUCHARMS_BUNDLE_URL, |
966 | - settings.JUJUCHARMS_BUNDLE_URL + 'bad', |
967 | - settings.JUJUCHARMS_BUNDLE_URL + '~user/no-such', |
968 | - settings.JUJUCHARMS_BUNDLE_URL + '~user/basket/revision/name/', |
969 | - settings.JUJUCHARMS_BUNDLE_URL + '~user/basket/42/name/error', |
970 | - 'https://jujucharms.com/charms/mediawiki/simple/', |
971 | - ) |
972 | - for url in bad_urls: |
973 | - with self.assert_value_error('invalid bundle URL: {}'.format(url)): |
974 | - utils.convert_bundle_url(url) |
975 | - |
976 | - |
977 | class TestGetQuickstartBanner(unittest.TestCase): |
978 | |
979 | def patch_datetime(self): |
980 | @@ -321,79 +168,6 @@ |
981 | self.assertEqual(expected, obtained) |
982 | |
983 | |
984 | -class TestGetServiceInfo(helpers.WatcherDataTestsMixin, unittest.TestCase): |
985 | - |
986 | - def test_service_and_unit(self): |
987 | - # The data about the given service and unit is correctly returned. |
988 | - service_change = self.make_service_change() |
989 | - unit_change = self.make_unit_change() |
990 | - status = [service_change, unit_change] |
991 | - expected = (service_change[2], unit_change[2]) |
992 | - self.assertEqual(expected, utils.get_service_info(status, 'my-gui')) |
993 | - |
994 | - def test_service_only(self): |
995 | - # The data about the given service without units is correctly returned. |
996 | - service_change = self.make_service_change() |
997 | - status = [service_change] |
998 | - expected = (service_change[2], None) |
999 | - self.assertEqual(expected, utils.get_service_info(status, 'my-gui')) |
1000 | - |
1001 | - def test_service_removed(self): |
1002 | - # A tuple (None, None) is returned if the service is being removed. |
1003 | - status = [ |
1004 | - self.make_service_change(action='remove'), |
1005 | - self.make_unit_change(), |
1006 | - ] |
1007 | - expected = (None, None) |
1008 | - self.assertEqual(expected, utils.get_service_info(status, 'my-gui')) |
1009 | - |
1010 | - def test_another_service(self): |
1011 | - # A tuple (None, None) is returned if the service is not found. |
1012 | - status = [ |
1013 | - self.make_service_change(data={'Name': 'another-service'}), |
1014 | - self.make_unit_change(), |
1015 | - ] |
1016 | - expected = (None, None) |
1017 | - self.assertEqual(expected, utils.get_service_info(status, 'my-gui')) |
1018 | - |
1019 | - def test_service_not_alive(self): |
1020 | - # A tuple (None, None) is returned if the service is not alive. |
1021 | - status = [ |
1022 | - self.make_service_change(data={'Life': 'dying'}), |
1023 | - self.make_unit_change(), |
1024 | - ] |
1025 | - expected = (None, None) |
1026 | - self.assertEqual(expected, utils.get_service_info(status, 'my-gui')) |
1027 | - |
1028 | - def test_unit_removed(self): |
1029 | - # The unit data is not returned if the unit is being removed. |
1030 | - service_change = self.make_service_change() |
1031 | - status = [service_change, self.make_unit_change(action='remove')] |
1032 | - expected = (service_change[2], None) |
1033 | - self.assertEqual(expected, utils.get_service_info(status, 'my-gui')) |
1034 | - |
1035 | - def test_another_unit(self): |
1036 | - # The unit data is not returned if the unit belongs to another service. |
1037 | - service_change = self.make_service_change() |
1038 | - status = [ |
1039 | - service_change, |
1040 | - self.make_unit_change(data={'Service': 'another-service'}), |
1041 | - ] |
1042 | - expected = (service_change[2], None) |
1043 | - self.assertEqual(expected, utils.get_service_info(status, 'my-gui')) |
1044 | - |
1045 | - def test_no_services(self): |
1046 | - # A tuple (None, None) is returned no services are found. |
1047 | - status = [self.make_unit_change()] |
1048 | - expected = (None, None) |
1049 | - self.assertEqual(expected, utils.get_service_info(status, 'my-gui')) |
1050 | - |
1051 | - def test_no_entities(self): |
1052 | - # A tuple (None, None) is returned no entities are found. |
1053 | - expected = (None, None) |
1054 | - self.assertEqual(expected, utils.get_service_info([], 'my-gui')) |
1055 | - |
1056 | - |
1057 | class TestGetUbuntuCodename(helpers.CallTestsMixin, unittest.TestCase): |
1058 | |
1059 | def test_codename(self): |
1060 | @@ -461,177 +235,6 @@ |
1061 | self.assertFalse(os.path.exists(path)) |
1062 | |
1063 | |
1064 | -class TestParseBundle( |
1065 | - helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin, |
1066 | - unittest.TestCase): |
1067 | - |
1068 | - def assert_bundle( |
1069 | - self, expected_name, expected_services, contents, |
1070 | - bundle_name=None): |
1071 | - """Ensure parsing the given contents returns the expected values.""" |
1072 | - name, services = utils.parse_bundle(contents, bundle_name=bundle_name) |
1073 | - self.assertEqual(expected_name, name) |
1074 | - self.assertEqual(set(expected_services), set(services)) |
1075 | - |
1076 | - def test_invalid_yaml(self): |
1077 | - # A ValueError is raised if the bundle contents are not a valid YAML. |
1078 | - with self.assertRaises(ValueError) as context_manager: |
1079 | - utils.parse_bundle(':') |
1080 | - expected = 'unable to parse the bundle' |
1081 | - self.assertIn(expected, bytes(context_manager.exception)) |
1082 | - |
1083 | - def test_yaml_invalid_type(self): |
1084 | - # A ValueError is raised if the bundle contents are not well formed. |
1085 | - with self.assert_value_error('invalid YAML contents: a-string'): |
1086 | - utils.parse_bundle('a-string') |
1087 | - |
1088 | - def test_yaml_invalid_bundle_data(self): |
1089 | - # A ValueError is raised if bundles are not well formed. |
1090 | - contents = yaml.safe_dump({'mybundle': 'not valid'}) |
1091 | - expected = 'invalid YAML contents: {mybundle: not valid}\n' |
1092 | - with self.assert_value_error(expected): |
1093 | - utils.parse_bundle(contents) |
1094 | - |
1095 | - def test_yaml_no_service(self): |
1096 | - # A ValueError is raised if bundles do not include services. |
1097 | - contents = yaml.safe_dump({'mybundle': {}}) |
1098 | - expected = 'invalid YAML contents: mybundle: {}\n' |
1099 | - with self.assert_value_error(expected): |
1100 | - utils.parse_bundle(contents) |
1101 | - |
1102 | - def test_yaml_none_bundle_services(self): |
1103 | - # A ValueError is raised if services are None. |
1104 | - contents = yaml.safe_dump({'mybundle': {'services': None}}) |
1105 | - expected = 'invalid YAML contents: mybundle: {services: null}\n' |
1106 | - with self.assert_value_error(expected): |
1107 | - utils.parse_bundle(contents) |
1108 | - |
1109 | - def test_yaml_invalid_bundle_services_type(self): |
1110 | - # A ValueError is raised if services have an invalid type. |
1111 | - contents = yaml.safe_dump({'mybundle': {'services': 42}}) |
1112 | - expected = 'invalid YAML contents: mybundle: {services: 42}\n' |
1113 | - with self.assert_value_error(expected): |
1114 | - utils.parse_bundle(contents) |
1115 | - |
1116 | - def test_yaml_no_bundles(self): |
1117 | - # A ValueError is raised if the bundle contents are empty. |
1118 | - with self.assert_value_error('no bundles found'): |
1119 | - utils.parse_bundle(yaml.safe_dump({})) |
1120 | - |
1121 | - def test_bundle_name_not_specified(self): |
1122 | - # A ValueError is raised if the bundle name is not specified and the |
1123 | - # contents contain more than one bundle. |
1124 | - expected = ('multiple bundles found (bundle1, bundle2) ' |
1125 | - 'but no bundle name specified') |
1126 | - with self.assert_value_error(expected): |
1127 | - utils.parse_bundle(self.valid_bundle) |
1128 | - |
1129 | - def test_bundle_name_not_found(self): |
1130 | - # A ValueError is raised if the given bundle is not found in the file. |
1131 | - expected = ('bundle no-such not found in the provided list of bundles ' |
1132 | - '(bundle1, bundle2)') |
1133 | - with self.assert_value_error(expected): |
1134 | - utils.parse_bundle(self.valid_bundle, 'no-such') |
1135 | - |
1136 | - def test_no_services(self): |
1137 | - # A ValueError is raised if the specified bundle does not contain |
1138 | - # services. |
1139 | - contents = yaml.safe_dump({'mybundle': {'services': {}}}) |
1140 | - expected = 'bundle mybundle does not include any services' |
1141 | - with self.assert_value_error(expected): |
1142 | - utils.parse_bundle(contents) |
1143 | - |
1144 | - def test_yaml_gui_in_services(self): |
1145 | - # A ValueError is raised if the bundle contains juju-gui. |
1146 | - contents = yaml.safe_dump({ |
1147 | - 'mybundle': {'services': {settings.JUJU_GUI_SERVICE_NAME: {}}}, |
1148 | - }) |
1149 | - expected = 'bundle mybundle contains an instance of juju-gui. ' \ |
1150 | - 'quickstart will install the latest version of the Juju GUI ' \ |
1151 | - 'automatically, please remove juju-gui from the bundle.' |
1152 | - with self.assert_value_error(expected): |
1153 | - utils.parse_bundle(contents) |
1154 | - |
1155 | - def test_success_no_name(self): |
1156 | - # The function succeeds when an implicit bundle name is used. |
1157 | - contents = yaml.safe_dump({ |
1158 | - 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}}, |
1159 | - }) |
1160 | - self.assert_bundle('mybundle', ['mysql', 'wordpress'], contents) |
1161 | - |
1162 | - def test_success_multiple_bundles(self): |
1163 | - # The function succeeds with multiple bundles. |
1164 | - self.assert_bundle( |
1165 | - 'bundle2', ['django', 'nodejs'], self.valid_bundle, 'bundle2') |
1166 | - |
1167 | - def test_success_json(self): |
1168 | - # Since JSON is a subset of YAML, the function also support JSON |
1169 | - # encoded bundles. |
1170 | - contents = json.dumps({ |
1171 | - 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}}, |
1172 | - }) |
1173 | - self.assert_bundle('mybundle', ['mysql', 'wordpress'], contents) |
1174 | - |
1175 | - |
1176 | -class TestParseStatusOutput(helpers.ValueErrorTestsMixin, unittest.TestCase): |
1177 | - |
1178 | - def test_invalid_yaml(self): |
1179 | - # A ValueError is raised if the output is not a valid YAML. |
1180 | - with self.assertRaises(ValueError) as context_manager: |
1181 | - utils.parse_status_output(':') |
1182 | - expected = 'unable to parse the output' |
1183 | - self.assertIn(expected, bytes(context_manager.exception)) |
1184 | - |
1185 | - def test_invalid_yaml_contents(self): |
1186 | - # A ValueError is raised if the output is not well formed. |
1187 | - with self.assert_value_error('invalid YAML contents: a-string'): |
1188 | - utils.parse_status_output('a-string') |
1189 | - |
1190 | - def test_no_agent_state(self): |
1191 | - # A ValueError is raised if the agent-state is not found in the YAML. |
1192 | - data = { |
1193 | - 'machines': { |
1194 | - '0': {'agent-version': '1.17.0.1'}, |
1195 | - }, |
1196 | - } |
1197 | - expected = 'machines:0:agent-state not found in {}'.format(bytes(data)) |
1198 | - with self.assert_value_error(expected): |
1199 | - utils.get_agent_state(yaml.safe_dump(data)) |
1200 | - |
1201 | - def test_success_agent_state(self): |
1202 | - # The agent state is correctly returned. |
1203 | - output = yaml.safe_dump({ |
1204 | - 'machines': { |
1205 | - '0': {'agent-version': '1.17.0.1', 'agent-state': 'started'}, |
1206 | - }, |
1207 | - }) |
1208 | - agent_state = utils.get_agent_state(output) |
1209 | - self.assertEqual('started', agent_state) |
1210 | - |
1211 | - def test_no_bootstrap_node_series(self): |
1212 | - # A ValueError is raised if the series is not found in the YAML. |
1213 | - data = { |
1214 | - 'machines': { |
1215 | - '0': {'agent-version': '1.17.0.1'}, |
1216 | - }, |
1217 | - } |
1218 | - expected = 'machines:0:series not found in {}'.format(bytes(data)) |
1219 | - with self.assert_value_error(expected): |
1220 | - utils.get_bootstrap_node_series(yaml.safe_dump(data)) |
1221 | - |
1222 | - def test_success_bootstrap_node_series(self): |
1223 | - # The bootstrap node series is correctly returned. |
1224 | - output = yaml.safe_dump({ |
1225 | - 'machines': { |
1226 | - '0': {'agent-version': '1.17.0.1', |
1227 | - 'agent-state': 'started', |
1228 | - 'series': 'zydeco'}, |
1229 | - }, |
1230 | - }) |
1231 | - bsn_series = utils.get_bootstrap_node_series(output) |
1232 | - self.assertEqual('zydeco', bsn_series) |
1233 | - |
1234 | - |
1235 | class TestRunOnce(unittest.TestCase): |
1236 | |
1237 | def setUp(self): |
1238 | |
1239 | === modified file 'quickstart/utils.py' |
1240 | --- quickstart/utils.py 2014-11-11 19:08:57 +0000 |
1241 | +++ quickstart/utils.py 2015-02-09 13:39:50 +0000 |
1242 | @@ -14,41 +14,22 @@ |
1243 | # You should have received a copy of the GNU Affero General Public License |
1244 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
1245 | |
1246 | -"""Juju Quickstart utility functions and classes.""" |
1247 | +"""Juju Quickstart general purpose utility functions and classes.""" |
1248 | |
1249 | from __future__ import ( |
1250 | print_function, |
1251 | unicode_literals, |
1252 | ) |
1253 | |
1254 | -import collections |
1255 | import datetime |
1256 | import errno |
1257 | import functools |
1258 | import logging |
1259 | import os |
1260 | import pipes |
1261 | -import re |
1262 | import subprocess |
1263 | |
1264 | import quickstart |
1265 | -from quickstart import ( |
1266 | - serializers, |
1267 | - settings, |
1268 | -) |
1269 | -from quickstart.models import charms |
1270 | - |
1271 | - |
1272 | -# Compile the regular expression used to parse bundle URLs. |
1273 | -_bundle_expression = re.compile(r""" |
1274 | - # Bundle schema or bundle URL namespace on jujucharms.com. |
1275 | - ^(?:bundle:|{}) |
1276 | - (?:~([-\w]+)/)? # Optional user name. |
1277 | - ([-\w]+)/ # Basket name. |
1278 | - (?:(\d+)/)? # Optional bundle revision number. |
1279 | - ([-\w]+) # Bundle name. |
1280 | - /?$ # Optional trailing slash. |
1281 | -""".format(settings.JUJUCHARMS_BUNDLE_URL), re.VERBOSE) |
1282 | |
1283 | |
1284 | def add_apt_repository(repository): |
1285 | @@ -101,51 +82,6 @@ |
1286 | return retcode, output.decode('utf-8'), error.decode('utf-8') |
1287 | |
1288 | |
1289 | -def parse_gui_charm_url(charm_url): |
1290 | - """Parse the given charm URL. |
1291 | - |
1292 | - Check if the charm looks like a Juju GUI charm. |
1293 | - Print (to stdout or to logs) info and warnings about the charm URL. |
1294 | - |
1295 | - Return the parsed charm object as an instance of |
1296 | - quickstart.models.charms.Charm. |
1297 | - """ |
1298 | - print('charm URL: {}'.format(charm_url)) |
1299 | - charm = charms.Charm.from_url(charm_url) |
1300 | - charm_name = settings.JUJU_GUI_CHARM_NAME |
1301 | - if charm.name != charm_name: |
1302 | - # This does not seem to be a Juju GUI charm. |
1303 | - logging.warn( |
1304 | - 'unexpected URL for the {} charm: ' |
1305 | - 'the service may not work as expected'.format(charm_name)) |
1306 | - return charm |
1307 | - if charm.user or charm.is_local(): |
1308 | - # This is not the official Juju GUI charm. |
1309 | - logging.warn('using a customized {} charm'.format(charm_name)) |
1310 | - elif charm.revision < settings.MINIMUM_REVISIONS_FOR_BUNDLES[charm.series]: |
1311 | - # This is the official Juju GUI charm, but it is outdated. |
1312 | - logging.warn( |
1313 | - 'charm is outdated and may not support bundle deployments') |
1314 | - return charm |
1315 | - |
1316 | - |
1317 | -def convert_bundle_url(bundle_url): |
1318 | - """Return the equivalent YAML HTTPS location for the given bundle URL. |
1319 | - |
1320 | - Raise a ValueError if the given URL is not a valid bundle URL. |
1321 | - """ |
1322 | - match = _bundle_expression.match(bundle_url) |
1323 | - if match is None: |
1324 | - msg = 'invalid bundle URL: {}'.format(bundle_url) |
1325 | - raise ValueError(msg.encode('utf-8')) |
1326 | - user, basket, revision, name = match.groups() |
1327 | - user_part = '~charmers/' if user is None else '~{}/'.format(user) |
1328 | - revision_part = '' if revision is None else '{}/'.format(revision) |
1329 | - bundle_id = '{}{}/{}{}'.format(user_part, basket, revision_part, name) |
1330 | - return ('https://manage.jujucharms.com/bundle/{}/json'.format(bundle_id), |
1331 | - bundle_id) |
1332 | - |
1333 | - |
1334 | def get_quickstart_banner(): |
1335 | """Return a quickstart banner suitable for being included in files. |
1336 | |
1337 | @@ -162,31 +98,6 @@ |
1338 | '# at {} UTC.\n\n'.format(version, formatted_date)) |
1339 | |
1340 | |
1341 | -def get_service_info(status, service_name): |
1342 | - """Retrieve information on the given service and on its first alive unit. |
1343 | - |
1344 | - Return a tuple containing two values: (service data, unit data). |
1345 | - Each value can be: |
1346 | - - a dictionary of data about the given entity (service or unit) as |
1347 | - returned by the Juju watcher; |
1348 | - - None, if the entity is not present in the Juju environment. |
1349 | - If the service data is None, the unit data is always None. |
1350 | - """ |
1351 | - services = [ |
1352 | - data for entity, action, data in status if |
1353 | - (entity == 'service') and (action != 'remove') and |
1354 | - (data['Name'] == service_name) and (data['Life'] == 'alive') |
1355 | - ] |
1356 | - if not services: |
1357 | - return None, None |
1358 | - units = [ |
1359 | - data for entity, action, data in status if |
1360 | - entity == 'unit' and action != 'remove' and |
1361 | - data['Service'] == service_name |
1362 | - ] |
1363 | - return services[0], units[0] if units else None |
1364 | - |
1365 | - |
1366 | def get_ubuntu_codename(): |
1367 | """Return the codename of the current Ubuntu release (e.g. "trusty"). |
1368 | |
1369 | @@ -213,112 +124,6 @@ |
1370 | raise |
1371 | |
1372 | |
1373 | -def parse_bundle(bundle_yaml, bundle_name=None): |
1374 | - """Parse the provided bundle YAML encoded contents. |
1375 | - |
1376 | - Since a valid JSON is a subset of YAML this function can be used also to |
1377 | - parse JSON encoded contents. |
1378 | - |
1379 | - Return a tuple containing the bundle name and the list of services included |
1380 | - in the bundle. |
1381 | - |
1382 | - Raise a ValueError if: |
1383 | - - the bundle YAML contents are not parsable by YAML; |
1384 | - - the YAML contents are not properly structured; |
1385 | - - the bundle name is specified but not included in the bundle file; |
1386 | - - the bundle name is not specified and the bundle file includes more than |
1387 | - one bundle; |
1388 | - - the bundle does not include services. |
1389 | - """ |
1390 | - # Parse the bundle file. |
1391 | - try: |
1392 | - bundles = serializers.yaml_load(bundle_yaml) |
1393 | - except Exception as err: |
1394 | - msg = b'unable to parse the bundle: {}'.format(err) |
1395 | - raise ValueError(msg) |
1396 | - # Ensure the bundle file is well formed and contains at least one bundle. |
1397 | - if not isinstance(bundles, collections.Mapping): |
1398 | - msg = 'invalid YAML contents: {}'.format(bundle_yaml) |
1399 | - raise ValueError(msg.encode('utf-8')) |
1400 | - try: |
1401 | - name_services_map = dict( |
1402 | - (key, value['services'].keys()) |
1403 | - for key, value in bundles.items() |
1404 | - ) |
1405 | - except (AttributeError, KeyError, TypeError): |
1406 | - msg = 'invalid YAML contents: {}'.format(bundle_yaml) |
1407 | - raise ValueError(msg.encode('utf-8')) |
1408 | - if not name_services_map: |
1409 | - raise ValueError(b'no bundles found') |
1410 | - # Retrieve the bundle name and services. |
1411 | - if bundle_name is None: |
1412 | - if len(name_services_map) > 1: |
1413 | - msg = 'multiple bundles found ({}) but no bundle name specified' |
1414 | - bundle_names = ', '.join(sorted(name_services_map.keys())) |
1415 | - raise ValueError(msg.format(bundle_names).encode('utf-8')) |
1416 | - bundle_name, bundle_services = name_services_map.items()[0] |
1417 | - else: |
1418 | - bundle_services = name_services_map.get(bundle_name) |
1419 | - if bundle_services is None: |
1420 | - msg = 'bundle {} not found in the provided list of bundles ({})' |
1421 | - bundle_names = ', '.join(sorted(name_services_map.keys())) |
1422 | - raise ValueError( |
1423 | - msg.format(bundle_name, bundle_names).encode('utf-8')) |
1424 | - if not bundle_services: |
1425 | - msg = 'bundle {} does not include any services'.format(bundle_name) |
1426 | - raise ValueError(msg.encode('utf-8')) |
1427 | - if settings.JUJU_GUI_SERVICE_NAME in bundle_services: |
1428 | - msg = ('bundle {} contains an instance of juju-gui. quickstart will ' |
1429 | - 'install the latest version of the Juju GUI automatically, ' |
1430 | - 'please remove juju-gui from the bundle.'.format(bundle_name)) |
1431 | - raise ValueError(msg.encode('utf-8')) |
1432 | - return bundle_name, bundle_services |
1433 | - |
1434 | - |
1435 | -def parse_status_output(output, keys=None): |
1436 | - """Parse the output of juju status. |
1437 | - |
1438 | - Return selection specified by the keys array. |
1439 | - Raise a ValueError if the selection cannot be retrieved. |
1440 | - """ |
1441 | - if keys is None: |
1442 | - keys = ['dummy'] |
1443 | - try: |
1444 | - status = serializers.yaml_load(output) |
1445 | - except Exception as err: |
1446 | - raise ValueError(b'unable to parse the output: {}'.format(err)) |
1447 | - |
1448 | - selection = status |
1449 | - for key in keys: |
1450 | - try: |
1451 | - selection = selection.get(key, {}) |
1452 | - except AttributeError as err: |
1453 | - msg = 'invalid YAML contents: {}'.format(status) |
1454 | - raise ValueError(msg.encode('utf-8')) |
1455 | - if selection == {}: |
1456 | - msg = '{} not found in {}'.format(':'.join(keys), status) |
1457 | - raise ValueError(msg.encode('utf-8')) |
1458 | - return selection |
1459 | - |
1460 | - |
1461 | -def get_agent_state(output): |
1462 | - """Parse the output of juju status for the agent state. |
1463 | - |
1464 | - Return the agent state. |
1465 | - Raise a ValueError if the agent state cannot be retrieved. |
1466 | - """ |
1467 | - return parse_status_output(output, ['machines', '0', 'agent-state']) |
1468 | - |
1469 | - |
1470 | -def get_bootstrap_node_series(output): |
1471 | - """Parse the output of juju status for the agent state. |
1472 | - |
1473 | - Return the agent state. |
1474 | - Raise a ValueError if the agent state cannot be retrieved. |
1475 | - """ |
1476 | - return parse_status_output(output, ['machines', '0', 'series']) |
1477 | - |
1478 | - |
1479 | def get_juju_version(juju_command): |
1480 | """Return the current juju-core version. |
1481 |
Reviewers: mp+249073_ code.launchpad. net,
Message:
Please take a look.
Description:
Refactor/split utils code.
Move code from utils to the new jujutools and models/bundles modules.
This branch only include moved code and it's done
in preparation for the Juju API new login work.
https:/ /code.launchpad .net/~frankban/ juju-quickstart /jujuutils/ +merge/ 249073
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/204770043/
Affected files (+715, -599 lines): jujutools. py manage. py models/ bundles. py tests/models/ test_bundles. py tests/test_ jujutools. py tests/test_ utils.py
A [revision details]
M quickstart/app.py
A quickstart/
M quickstart/
A quickstart/
A quickstart/
A quickstart/
M quickstart/
M quickstart/utils.py