Merge lp:~frankban/juju-quickstart/charm-url-warning into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 21
Proposed branch: lp:~frankban/juju-quickstart/charm-url-warning
Merge into: lp:juju-quickstart
Diff against target: 899 lines (+636/-39)
10 files modified
quickstart/__init__.py (+1/-1)
quickstart/app.py (+2/-6)
quickstart/charms.py (+118/-0)
quickstart/manage.py (+40/-2)
quickstart/settings.py (+10/-1)
quickstart/tests/test_app.py (+129/-25)
quickstart/tests/test_charms.py (+205/-0)
quickstart/tests/test_manage.py (+74/-1)
quickstart/tests/test_utils.py (+31/-1)
quickstart/utils.py (+26/-2)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/charm-url-warning
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+196244@code.launchpad.net

Description of the change

Improve charm URL handling.

Validate the user provided GUI charm URL.
Also add a missing test for complete coverage
(obsessive-compulsive mode on).

Tests: `make check`.

QA:

1)
Provide invalid charm URLs, the program
should immediately stop the execution
and exit with an error, e.g.:

.venv/bin/python juju-quickstart --gui-charm-url invalid

.venv/bin/python juju-quickstart --gui-charm-url local:precise/juju-gui-80

.venv/bin/python juju-quickstart --gui-charm-url http:~juju-gui/precise/juju-gui-80

.venv/bin/python juju-quickstart --gui-charm-url cs:precise/juju-gui-1 bundle:~jorge/mediawiki-simple/4/mediawiki-simple

.venv/bin/python juju-quickstart --gui-charm-url cs:saucy/juju-gui-80

2)
Run the program passing a customized charm URL, e.g.:

.venv/bin/python juju-quickstart --gui-charm-url cs:~juju-gui/precise/juju-gui-128

You should see the "using a customized juju-gui charm" warning
printed during the service deployment step.

Re-execute the command above: quickstart should reuse the
service in the environment and the warning is printed again.

Destroy the environment.

3)
Now manually deploy an outdated version of the GUI charm:

(sudo) juju bootstrap
juju deploy cs:precise/juju-gui-79

Run quickstart:
.venv/bin/python juju-quickstart

You should see the "charm is outdated" warning, then quickstart
waits for the outdated GUI to be deployed and ready.

Destroy the environment.

4)
Run quickstart normally:
.venv/bin/python juju-quickstart
The last official GUI charm (cs:precise/juju-gui-80)
is installed and no warnings are logged.

Destroy the environment.
Done, thank you!

https://codereview.appspot.com/30760043/

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

Reviewers: mp+196244_code.launchpad.net,

Message:
Please take a look.

Description:
Improve charm URL handling.

Validate the user provided GUI charm URL.
Also add a missing test for complete coverage
(obsessive-compulsive mode on).

Tests: `make check`.

QA:

1)
Provide invalid charm URLs, the program
should immediately stop the execution
and exit with an error, e.g.:

.venv/bin/python juju-quickstart --gui-charm-url invalid

.venv/bin/python juju-quickstart --gui-charm-url
local:precise/juju-gui-80

.venv/bin/python juju-quickstart --gui-charm-url
http:~juju-gui/precise/juju-gui-80

.venv/bin/python juju-quickstart --gui-charm-url cs:precise/juju-gui-1
bundle:~jorge/mediawiki-simple/4/mediawiki-simple

.venv/bin/python juju-quickstart --gui-charm-url cs:saucy/juju-gui-80

2)
Run the program passing a customized charm URL, e.g.:

.venv/bin/python juju-quickstart --gui-charm-url
cs:~juju-gui/precise/juju-gui-128

You should see the "using a customized juju-gui charm" warning
printed during the service deployment step.

Re-execute the command above: quickstart should reuse the
service in the environment and the warning is printed again.

Destroy the environment.

3)
Now manually deploy an outdated version of the GUI charm:

(sudo) juju bootstrap
juju deploy cs:precise/juju-gui-79

Run quickstart:
.venv/bin/python juju-quickstart

You should see the "charm is outdated" warning, then quickstart
waits for the outdated GUI to be deployed and ready.

Destroy the environment.

4)
Run quickstart normally:
.venv/bin/python juju-quickstart
The last official GUI charm (cs:precise/juju-gui-80)
is installed and no warnings are logged.

Destroy the environment.
Done, thank you!

https://code.launchpad.net/~frankban/juju-quickstart/charm-url-warning/+merge/196244

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/30760043/

Affected files (+637, -39 lines):
   A [revision details]
   M quickstart/__init__.py
   M quickstart/app.py
   A quickstart/charms.py
   M quickstart/manage.py
   M quickstart/settings.py
   M quickstart/tests/test_app.py
   A quickstart/tests/test_charms.py
   M quickstart/tests/test_manage.py
   M quickstart/tests/test_utils.py
   M quickstart/utils.py

Revision history for this message
Richard Harding (rharding) wrote :

I'm worried about the dupe code. We've got another project doing charm
parsing and such. This is done in the juju store and charmworld. I
wonder if we can cheat and check if the charm can be found via a network
connection to either of those stores rather than dupe the code.

The hard coded series support just seems like a land mine to forget to
update the deployer and then get bug reports down the road. Maybe we can
at least put in precise/trusty now and file a bug on the charm we need
to test and get it into trusty?

The code looks ok with a couple of notes below, but I'd like to discuss
if there's a lighter maintenance way to go.

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

https://codereview.appspot.com/30760043/diff/1/quickstart/app.py#newcode236
quickstart/app.py:236: utils.check_gui_charm_url(charm_url)
I'm a bit hmmm on the check method just being inline. Other things are
doing logging, raising exeptions, etc. I'd have expected this to check
it and, based on the response, take some logic to exit from here.

https://codereview.appspot.com/30760043/diff/1/quickstart/charms.py
File quickstart/charms.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/charms.py#newcode29
quickstart/charms.py:29: def parse_url(url):
yuck, another place of duped logic for urls. Is there any way we
can/should push this up to charmworld?

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py
File quickstart/manage.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py#newcode110
quickstart/manage.py:110: if charm.series not in
settings.JUJU_GUI_SUPPORTED_SERIES:
is settings coming from juju or is there another copy of the list in
here?

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py#newcode112
quickstart/manage.py:112: if (
should this be moved to some _checkIsValidBundle method?

https://codereview.appspot.com/30760043/diff/1/quickstart/tests/test_app.py
File quickstart/tests/test_app.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/tests/test_app.py#newcode651
quickstart/tests/test_app.py:651: # An existing but unexpected charm URL
is correctly found and logged.
Is there a test for a non-precise charm?

https://codereview.appspot.com/30760043/diff/1/quickstart/utils.py
File quickstart/utils.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/utils.py#newcode66
quickstart/utils.py:66: def check_gui_charm_url(charm_url):
I see, it's just logging. I still thing it reads strange in app to check
and then do nothing with the check. Maybe it's a warn_invalid_charm_url
method?

https://codereview.appspot.com/30760043/

Revision history for this message
Richard Harding (rharding) wrote :

Per irc chat, LGTM on the code. It'll be a nice day when we all share a
single proof library for that stuff.

Will QA now.

https://codereview.appspot.com/30760043/

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

LGTM modulo small suggestions and Rick's concerns. QA OK though I did
not do #3.

https://codereview.appspot.com/30760043/diff/1/quickstart/charms.py
File quickstart/charms.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/charms.py#newcode29
quickstart/charms.py:29: def parse_url(url):
This function looks very complete. But as Rick said it duplicates a lot
of functionality used by Charmworld. Would be nice to consolidate
them...but where?

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py
File quickstart/manage.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py#newcode111
quickstart/manage.py:111: return parser.error('unsupported charm series:
{}'.format(charm))
If you just printed the charm.series it would be more consistent with
other error messages.

https://codereview.appspot.com/30760043/diff/1/quickstart/settings.py
File quickstart/settings.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/settings.py#newcode41
quickstart/settings.py:41: # The series supported by the Juju GUI charm.
Since the word 'series' is both singular and plural, just for clarity
I'd change the above to say "The set of series..." to indicate in the
future it may be more than one. May be overkill since you clearly
define a tuple below...

https://codereview.appspot.com/30760043/

Revision history for this message
Richard Harding (rharding) wrote :
27. By Francesco Banconi

Changes as per review.

28. By Francesco Banconi

Merged trunk.

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (4.4 KiB)

*** Submitted:

Improve charm URL handling.

Validate the user provided GUI charm URL.
Also add a missing test for complete coverage
(obsessive-compulsive mode on).

Tests: `make check`.

QA:

1)
Provide invalid charm URLs, the program
should immediately stop the execution
and exit with an error, e.g.:

.venv/bin/python juju-quickstart --gui-charm-url invalid

.venv/bin/python juju-quickstart --gui-charm-url
local:precise/juju-gui-80

.venv/bin/python juju-quickstart --gui-charm-url
http:~juju-gui/precise/juju-gui-80

.venv/bin/python juju-quickstart --gui-charm-url cs:precise/juju-gui-1
bundle:~jorge/mediawiki-simple/4/mediawiki-simple

.venv/bin/python juju-quickstart --gui-charm-url cs:saucy/juju-gui-80

2)
Run the program passing a customized charm URL, e.g.:

.venv/bin/python juju-quickstart --gui-charm-url
cs:~juju-gui/precise/juju-gui-128

You should see the "using a customized juju-gui charm" warning
printed during the service deployment step.

Re-execute the command above: quickstart should reuse the
service in the environment and the warning is printed again.

Destroy the environment.

3)
Now manually deploy an outdated version of the GUI charm:

(sudo) juju bootstrap
juju deploy cs:precise/juju-gui-79

Run quickstart:
.venv/bin/python juju-quickstart

You should see the "charm is outdated" warning, then quickstart
waits for the outdated GUI to be deployed and ready.

Destroy the environment.

4)
Run quickstart normally:
.venv/bin/python juju-quickstart
The last official GUI charm (cs:precise/juju-gui-80)
is installed and no warnings are logged.

Destroy the environment.
Done, thank you!

R=rharding, bac
CC=
https://codereview.appspot.com/30760043

https://codereview.appspot.com/30760043/diff/1/quickstart/charms.py
File quickstart/charms.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/charms.py#newcode29
quickstart/charms.py:29: def parse_url(url):
On 2013/11/22 14:31:51, bac wrote:
> This function looks very complete. But as Rick said it duplicates a
lot of
> functionality used by Charmworld. Would be nice to consolidate
them...but
> where?

Agreed. We discussed on IRC about the need for an external
charms/bundles validation library where to refactor this kind of code.
Many projects can then take advantage of such a library, e.g.
quickstart, proof, deployer, guiserver, charmworld...

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py
File quickstart/manage.py (right):

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py#newcode110
quickstart/manage.py:110: if charm.series not in
settings.JUJU_GUI_SUPPORTED_SERIES:
On 2013/11/22 13:47:33, rharding wrote:
> is settings coming from juju or is there another copy of the list in
here?

The list is defined in quickstart.

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py#newcode111
quickstart/manage.py:111: return parser.error('unsupported charm series:
{}'.format(charm))
On 2013/11/22 14:31:51, bac wrote:
> If you just printed the charm.series it would be more consistent with
other
> error messages.

Done.

https://codereview.appspot.com/30760043/diff/1/quickstart/manage.py#newcode112
quickstart/manage.py:11...

Read more...

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

Hey Rick and Brad,
thank you for the great suggestions in you reviews!

https://codereview.appspot.com/30760043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'quickstart/__init__.py'
--- quickstart/__init__.py 2013-11-18 22:50:55 +0000
+++ quickstart/__init__.py 2013-11-22 15:07:41 +0000
@@ -19,7 +19,7 @@
19that it can be managed using a Web interface (the Juju GUI).19that it can be managed using a Web interface (the Juju GUI).
20"""20"""
2121
22VERSION = (0, 4, 1)22VERSION = (0, 4, 2)
2323
2424
25def get_version():25def get_version():
2626
=== modified file 'quickstart/app.py'
--- quickstart/app.py 2013-11-21 19:45:01 +0000
+++ quickstart/app.py 2013-11-22 15:07:41 +0000
@@ -233,7 +233,7 @@
233 msg = 'unable to retrieve the {} charm URL from the API: {}'233 msg = 'unable to retrieve the {} charm URL from the API: {}'
234 logging.warn(msg.format(service_name, err))234 logging.warn(msg.format(service_name, err))
235 charm_url = settings.DEFAULT_CHARM_URL235 charm_url = settings.DEFAULT_CHARM_URL
236 print('charm URL: {}'.format(charm_url))236 utils.check_gui_charm_url(charm_url)
237 # Deploy the service without units.237 # Deploy the service without units.
238 try:238 try:
239 env.deploy(service_name, charm_url, num_units=0)239 env.deploy(service_name, charm_url, num_units=0)
@@ -244,11 +244,7 @@
244 else:244 else:
245 # We already have the service in the environment.245 # We already have the service in the environment.
246 print('service {} already deployed'.format(service_name))246 print('service {} already deployed'.format(service_name))
247 charm_url = service_data['CharmURL']247 utils.check_gui_charm_url(service_data['CharmURL'])
248 print('charm URL: {}'.format(charm_url))
249 # XXX frankban: warn the user if the actual charm_url != the given
250 # charm_url, and warn louder if the charm URL does not match
251 # /\/juju-gui-\d+$/.
252 service_exposed = service_data.get('Exposed', False)248 service_exposed = service_data.get('Exposed', False)
253 # At this point the service is surely deployed in the environment: expose249 # At this point the service is surely deployed in the environment: expose
254 # it if necessary and add a unit if it is missing.250 # it if necessary and add a unit if it is missing.
255251
=== added file 'quickstart/charms.py'
--- quickstart/charms.py 1970-01-01 00:00:00 +0000
+++ quickstart/charms.py 2013-11-22 15:07:41 +0000
@@ -0,0 +1,118 @@
1# This file is part of the Juju Quickstart Plugin, which lets users set up a
2# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Juju Quickstart charms management."""
18
19import re
20
21
22# The following regular expressions are the same used in juju-core: see
23# http://bazaar.launchpad.net/~go-bot/juju-core/trunk/view/head:/charm/url.go.
24valid_user = re.compile(r'^[a-z0-9][a-zA-Z0-9+.-]+$').match
25valid_series = re.compile(r'^[a-z]+([a-z-]+[a-z])?$').match
26valid_name = re.compile(r'^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$').match
27
28
29def parse_url(url):
30 """Parse the given charm URL.
31
32 Return a tuple containing the charm URL fragments: schema, user, series,
33 name and revision. Each fragment is a string except revision (int).
34
35 Raise a ValueError with a descriptive message if the given URL is not a
36 valid charm URL.
37 """
38 # Retrieve the schema.
39 try:
40 schema, remaining = url.split(':', 1)
41 except ValueError:
42 raise ValueError('charm URL has no schema: {!r}'.format(url))
43 if schema not in ('cs', 'local'):
44 raise ValueError('charm URL has invalid schema: {!r}'.format(schema))
45 # Retrieve the optional user, the series, name and revision.
46 parts = remaining.split('/')
47 parts_length = len(parts)
48 if parts_length == 3:
49 user, series, name_revision = parts
50 if not user.startswith('~'):
51 raise ValueError(
52 'charm URL has invalid user name form: {!r}'.format(user))
53 user = user[1:]
54 if not valid_user(user):
55 raise ValueError(
56 'charm URL has invalid user name: {!r}'.format(user))
57 if schema == 'local':
58 raise ValueError(
59 'local charm URL with user name: {!r}'.format(url))
60 elif parts_length == 2:
61 user = ''
62 series, name_revision = parts
63 else:
64 raise ValueError('charm URL has invalid form: {!r}'.format(url))
65 # Validate the series.
66 if not valid_series(series):
67 raise ValueError('charm URL has invalid series: {!r}'.format(series))
68 # Validate name and revision.
69 try:
70 name, revision = name_revision.rsplit('-', 1)
71 except ValueError:
72 raise ValueError(
73 'charm URL has no revision: {!r}'.format(url))
74 if not valid_name(name):
75 raise ValueError('charm URL has invalid name: {!r}'.format(name))
76 try:
77 revision = int(revision)
78 except ValueError:
79 raise ValueError(
80 'charm URL has invalid revision: {!r}'.format(revision))
81 return schema, user, series, name, revision
82
83
84class Charm(object):
85 """Represent the charm information stored in the charm URL."""
86
87 def __init__(self, schema, user, series, name, revision):
88 """Initialize the charm. Receives the URL fragments."""
89 self.schema = schema
90 self.user = user
91 self.series = series
92 self.name = name
93 self.revision = int(revision)
94
95 @classmethod
96 def from_url(cls, url):
97 """Given a charm URL, create and return a Charm instance.
98
99 Raise a ValueError if the charm URL is not valid.
100 """
101 return cls(*parse_url(url))
102
103 def __str__(self):
104 """The string representation of a charm is its URL."""
105 return self.url()
106
107 def __repr__(self):
108 return '<Charm: {}>'.format(str(self))
109
110 def url(self):
111 """Return the charm URL."""
112 user_part = '~{}/'.format(self.user) if self.user else ''
113 return '{}:{}{}/{}-{}'.format(
114 self.schema, user_part, self.series, self.name, self.revision)
115
116 def is_local(self):
117 """Return True if this is a local charm, False otherwise."""
118 return self.schema == 'local'
0119
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2013-11-21 18:41:16 +0000
+++ quickstart/manage.py 2013-11-22 15:07:41 +0000
@@ -25,6 +25,7 @@
25import quickstart25import quickstart
26from quickstart import (26from quickstart import (
27 app,27 app,
28 charms,
28 settings,29 settings,
29 utils,30 utils,
30)31)
@@ -88,6 +89,41 @@
88 options.bundle_id = bundle_id89 options.bundle_id = bundle_id
8990
9091
92def _validate_charm_url(options, parser):
93 """Validate the provided charm URL option.
94
95 Exit with an error if:
96 - the URL is not a valid charm URL;
97 - the URL represents a local charm;
98 - the charm series is not supported;
99 - a bundle deployment has been requested but the provided charm does
100 not support bundles.
101
102 Leave the options namespace untouched.
103 """
104 try:
105 charm = charms.Charm.from_url(options.charm_url)
106 except ValueError as err:
107 return parser.error(str(err))
108 if charm.is_local():
109 return parser.error('local charms are not allowed: {}'.format(charm))
110 if charm.series not in settings.JUJU_GUI_SUPPORTED_SERIES:
111 return parser.error(
112 'unsupported charm series: {!r}'.format(charm.series))
113 if (
114 # The user requested a bundle deployment.
115 options.bundle and
116 # This is the official Juju GUI charm.
117 charm.name == settings.JUJU_GUI_CHARM_NAME and
118 not charm.user and
119 # The charm at this revision does not support bundle deployments.
120 charm.revision < settings.MINIMUM_CHARM_REVISION_FOR_BUNDLES
121 ):
122 return parser.error(
123 'bundle deployments not supported by the requested charm '
124 'revision: {}'.format(charm))
125
126
91def _validate_env(options, parser):127def _validate_env(options, parser):
92 """Validate and process the provided environment related options.128 """Validate and process the provided environment related options.
93129
@@ -210,6 +246,8 @@
210 _validate_env(options, parser)246 _validate_env(options, parser)
211 if options.bundle is not None:247 if options.bundle is not None:
212 _validate_bundle(options, parser)248 _validate_bundle(options, parser)
249 if options.charm_url is not None:
250 _validate_charm_url(options, parser)
213 # Set up logging.251 # Set up logging.
214 _configure_logging(logging.DEBUG if options.debug else logging.INFO)252 _configure_logging(logging.DEBUG if options.debug else logging.INFO)
215 return options253 return options
@@ -235,8 +273,8 @@
235 # local provider.273 # local provider.
236 machine = None if options.env_type == 'local' else '0'274 machine = None if options.env_type == 'local' else '0'
237 unit_name = app.deploy_gui(275 unit_name = app.deploy_gui(
238 env, settings.JUJU_GUI_NAME, machine, charm_url=options.charm_url,276 env, settings.JUJU_GUI_SERVICE_NAME, machine,
239 check_preexisting=already_bootstrapped)277 charm_url=options.charm_url, check_preexisting=already_bootstrapped)
240 address = app.watch(env, unit_name)278 address = app.watch(env, unit_name)
241 url = 'https://{}'.format(address)279 url = 'https://{}'.format(address)
242 print('url: {}\npassword: {}'.format(url, options.admin_secret))280 print('url: {}\npassword: {}'.format(url, options.admin_secret))
243281
=== modified file 'quickstart/settings.py'
--- quickstart/settings.py 2013-11-14 10:52:31 +0000
+++ quickstart/settings.py 2013-11-22 15:07:41 +0000
@@ -32,5 +32,14 @@
32# Retrieve the current juju-core home.32# Retrieve the current juju-core home.
33JUJU_HOME = os.getenv('JUJU_HOME', '~/.juju')33JUJU_HOME = os.getenv('JUJU_HOME', '~/.juju')
3434
35# The name of the Juju GUI charm.
36JUJU_GUI_CHARM_NAME = 'juju-gui'
37
35# The name of the Juju GUI service.38# The name of the Juju GUI service.
36JUJU_GUI_NAME = 'juju-gui'39JUJU_GUI_SERVICE_NAME = JUJU_GUI_CHARM_NAME
40
41# The set of series supported by the Juju GUI charm.
42JUJU_GUI_SUPPORTED_SERIES = ('precise',)
43
44# The minimum Juju GUI charm revision supporting bundle deployments.
45MINIMUM_CHARM_REVISION_FOR_BUNDLES = 80
3746
=== modified file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 2013-11-21 18:41:16 +0000
+++ quickstart/tests/test_app.py 2013-11-22 15:07:41 +0000
@@ -61,6 +61,7 @@
61 return jujuclient.EnvError({'Error': message})61 return jujuclient.EnvError({'Error': message})
6262
6363
64@mock_print
64class TestEnsureDependencies(65class TestEnsureDependencies(
65 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):66 helpers.CallTestsMixin, ProgramExitTestsMixin, unittest.TestCase):
6667
@@ -69,7 +70,7 @@
69 app.ensure_dependencies()70 app.ensure_dependencies()
70 return mock_call71 return mock_call
7172
72 def test_success_install(self):73 def test_success_install(self, mock_print):
73 call = self.call_ensure_dependencies(74 call = self.call_ensure_dependencies(
74 (75 (
75 (127, '', 'no juju'),76 (127, '', 'no juju'),
@@ -83,14 +84,18 @@
83 call.assert_has_calls([84 call.assert_has_calls([
84 mock.call('juju', 'version'),85 mock.call('juju', 'version'),
85 mock.call('lxc-ls'),86 mock.call('lxc-ls'),
86 mock.call('sudo', 'apt-add-repository', '-y',87 mock.call('sudo', 'apt-add-repository', '-y', 'ppa:juju/stable'),
87 'ppa:juju/stable'),
88 mock.call('sudo', 'apt-get', 'update'),88 mock.call('sudo', 'apt-get', 'update'),
89 mock.call('sudo', 'apt-get', 'install', '-y', 'juju-core',89 mock.call('sudo', 'apt-get', 'install', '-y', 'juju-core', 'lxc'),
90 'lxc'),90 ])
91 mock_print.assert_has_calls([
92 mock.call(
93 'The following packages need to be installed: juju-core, lxc'),
94 mock.call(
95 'sudo privileges are required for package installation.'),
91 ])96 ])
9297
93 def test_success_no_install(self):98 def test_success_no_install(self, mock_print):
94 call = self.call_ensure_dependencies(99 call = self.call_ensure_dependencies(
95 [100 [
96 (0, '1.16', ''),101 (0, '1.16', ''),
@@ -102,7 +107,30 @@
102 )107 )
103 self.assertEqual(call.call_count, 2)108 self.assertEqual(call.call_count, 2)
104109
105 def test_failure(self):110 def test_success_one_install(self, mock_print):
111 # One missing installation is correctly handled.
112 call = self.call_ensure_dependencies([
113 (0, '1.16', ''),
114 (127, '', 'no lxc'),
115 (0, 'add repo', ''),
116 (0, 'update', ''),
117 (0, 'install', ''),
118 ])
119 self.assertEqual(call.call_count, 5)
120 call.assert_has_calls([
121 mock.call('juju', 'version'),
122 mock.call('lxc-ls'),
123 mock.call('sudo', 'apt-add-repository', '-y', 'ppa:juju/stable'),
124 mock.call('sudo', 'apt-get', 'update'),
125 mock.call('sudo', 'apt-get', 'install', '-y', 'lxc'),
126 ])
127 mock_print.assert_has_calls([
128 mock.call('The following package needs to be installed: lxc'),
129 mock.call(
130 'sudo privileges are required for package installation.'),
131 ])
132
133 def test_failure(self, mock_print):
106 with self.assert_program_exit('install failure'):134 with self.assert_program_exit('install failure'):
107 call = self.call_ensure_dependencies(135 call = self.call_ensure_dependencies(
108 [136 [
@@ -374,7 +402,7 @@
374 ProgramExitTestsMixin, helpers.WatcherDataTestsMixin,402 ProgramExitTestsMixin, helpers.WatcherDataTestsMixin,
375 unittest.TestCase):403 unittest.TestCase):
376404
377 charm_url = 'cs:precise/juju-gui-42'405 charm_url = 'cs:precise/juju-gui-100'
378406
379 def make_env(self, unit_name=None, service_data=None, unit_data=None):407 def make_env(self, unit_name=None, service_data=None, unit_data=None):
380 """Create and return a mock environment object.408 """Create and return a mock environment object.
@@ -403,6 +431,44 @@
403 mock_get_charm_url = mock.Mock(side_effect=side_effect)431 mock_get_charm_url = mock.Mock(side_effect=side_effect)
404 return mock.patch('quickstart.utils.get_charm_url', mock_get_charm_url)432 return mock.patch('quickstart.utils.get_charm_url', mock_get_charm_url)
405433
434 def check_provided_charm_url(
435 self, charm_url, mock_print, expected_logs=None):
436 """Ensure the service is deployed and exposed with the given charm URL.
437
438 Also check the expected warnings, if they are provided, are logged.
439 """
440 env = self.make_env(unit_name='my-gui/42')
441 with helpers.assert_logs(expected_logs or [], level='warn'):
442 app.deploy_gui(env, 'my-gui', '0', charm_url=charm_url)
443 env.assert_has_calls([
444 mock.call.deploy('my-gui', charm_url, num_units=0),
445 mock.call.expose('my-gui'),
446 mock.call.add_unit('my-gui', machine_spec='0'),
447 ])
448 mock_print.assert_has_calls([
449 mock.call('requesting my-gui deployment'),
450 mock.call('charm URL: {}'.format(charm_url)),
451 ])
452
453 def check_existing_charm_url(
454 self, charm_url, mock_print, expected_logs=None):
455 """Ensure the service is correctly found with the given charm URL.
456
457 Also check the expected warnings, if they are provided, are logged.
458 """
459 service_data = {'CharmURL': charm_url}
460 env = self.make_env(unit_name='my-gui/42', service_data=service_data)
461 with helpers.assert_logs(expected_logs or [], level='warn'):
462 app.deploy_gui(env, 'my-gui', '0', check_preexisting=True)
463 env.assert_has_calls([
464 mock.call.get_status(),
465 mock.call.add_unit('my-gui', machine_spec='0'),
466 ])
467 mock_print.assert_has_calls([
468 mock.call('service my-gui already deployed'),
469 mock.call('charm URL: {}'.format(charm_url)),
470 ])
471
406 def test_deployment(self, mock_print):472 def test_deployment(self, mock_print):
407 # The function correctly deploys and exposes the service, retrieving473 # The function correctly deploys and exposes the service, retrieving
408 # the charm URL from the charmworld API.474 # the charm URL from the charmworld API.
@@ -461,22 +527,6 @@
461 mock.call('charm URL: {}'.format(settings.DEFAULT_CHARM_URL)),527 mock.call('charm URL: {}'.format(settings.DEFAULT_CHARM_URL)),
462 ])528 ])
463529
464 def test_provided_charm_url(self, mock_print):
465 # The function correctly deploys and exposes the service using a user
466 # provided Juju GUI charm URL.
467 env = self.make_env(unit_name='my-gui/42')
468 charm_url = 'cs:~juju-gui/precise/juju-gui-116'
469 app.deploy_gui(env, 'my-gui', '0', charm_url=charm_url)
470 env.assert_has_calls([
471 mock.call.deploy('my-gui', charm_url, num_units=0),
472 mock.call.expose('my-gui'),
473 mock.call.add_unit('my-gui', machine_spec='0'),
474 ])
475 mock_print.assert_has_calls([
476 mock.call('requesting my-gui deployment'),
477 mock.call('charm URL: {}'.format(charm_url)),
478 ])
479
480 def test_existing_service(self, mock_print):530 def test_existing_service(self, mock_print):
481 # The deployment is executed reusing an already deployed service.531 # The deployment is executed reusing an already deployed service.
482 env = self.make_env(unit_name='my-gui/42', service_data={})532 env = self.make_env(unit_name='my-gui/42', service_data={})
@@ -551,6 +601,60 @@
551 mock.call.add_unit('my-gui', machine_spec=None),601 mock.call.add_unit('my-gui', machine_spec=None),
552 ])602 ])
553603
604 def test_offical_charm_url_provided(self, mock_print):
605 # The function correctly deploys and exposes the service using a user
606 # provided revision of the Juju GUI charm URL.
607 self.check_provided_charm_url('cs:precise/juju-gui-4242', mock_print)
608
609 def test_customized_charm_url_provided(self, mock_print):
610 # A customized charm URL is correctly recognized and logged if provided
611 # by the user.
612 self.check_provided_charm_url(
613 'cs:~juju-gui/precise/juju-gui-42', mock_print,
614 expected_logs=['using a customized juju-gui charm'])
615
616 def test_outdated_charm_url_provided(self, mock_print):
617 # An outdated charm URL is correctly recognized and logged if provided
618 # by the user.
619 self.check_provided_charm_url(
620 'cs:precise/juju-gui-1', mock_print,
621 expected_logs=[
622 'charm is outdated and may not support bundle deployments'])
623
624 def test_unexpected_charm_url_provided(self, mock_print):
625 # An unexpected charm URL is correctly recognized and logged if
626 # provided by the user.
627 self.check_provided_charm_url(
628 'cs:precise/exterminate-the-gui-666', mock_print,
629 expected_logs=[
630 'unexpected URL for the juju-gui charm: '
631 'the service may not work as expected'])
632
633 def test_offical_charm_url_existing(self, mock_print):
634 # An existing official charm URL is correctly found.
635 self.check_existing_charm_url('cs:precise/juju-gui-4242', mock_print)
636
637 def test_customized_charm_url_existing(self, mock_print):
638 # An existing customized charm URL is correctly found and logged.
639 self.check_existing_charm_url(
640 'cs:~juju-gui/precise/juju-gui-42', mock_print,
641 expected_logs=['using a customized juju-gui charm'])
642
643 def test_outdated_charm_url_existing(self, mock_print):
644 # An existing but outdated charm URL is correctly found and logged.
645 self.check_existing_charm_url(
646 'cs:precise/juju-gui-1', mock_print,
647 expected_logs=[
648 'charm is outdated and may not support bundle deployments'])
649
650 def test_unexpected_charm_url_existing(self, mock_print):
651 # An existing but unexpected charm URL is correctly found and logged.
652 self.check_existing_charm_url(
653 'cs:precise/exterminate-the-gui-666', mock_print,
654 expected_logs=[
655 'unexpected URL for the juju-gui charm: '
656 'the service may not work as expected'])
657
554 def test_status_error(self, mock_print):658 def test_status_error(self, mock_print):
555 # A ProgramExit is raised if an error occurs in the status API call.659 # A ProgramExit is raised if an error occurs in the status API call.
556 env = self.make_env()660 env = self.make_env()
@@ -597,7 +701,7 @@
597 with self.assertRaises(ValueError) as context_manager:701 with self.assertRaises(ValueError) as context_manager:
598 app.deploy_gui(env, 'juju-gui', '0')702 app.deploy_gui(env, 'juju-gui', '0')
599 env.deploy.assert_called_once_with(703 env.deploy.assert_called_once_with(
600 'juju-gui', 'cs:precise/juju-gui-42', num_units=0)704 'juju-gui', self.charm_url, num_units=0)
601 env.expose.assert_called_once_with('juju-gui')705 env.expose.assert_called_once_with('juju-gui')
602 self.assertIs(error, context_manager.exception)706 self.assertIs(error, context_manager.exception)
603707
604708
=== added file 'quickstart/tests/test_charms.py'
--- quickstart/tests/test_charms.py 1970-01-01 00:00:00 +0000
+++ quickstart/tests/test_charms.py 2013-11-22 15:07:41 +0000
@@ -0,0 +1,205 @@
1# This file is part of the Juju Quickstart Plugin, which lets users set up a
2# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
3# Copyright (C) 2013 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify it under
6# the terms of the GNU Affero General Public License version 3, as published by
7# the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful, but WITHOUT
10# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
11# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12# Affero General Public License for more details.
13#
14# You should have received a copy of the GNU Affero General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17"""Tests for the Juju Quickstart charms management."""
18
19import unittest
20
21from quickstart import charms
22from quickstart.tests import helpers
23
24
25class TestParseUrl(helpers.ValueErrorTestsMixin, unittest.TestCase):
26
27 def test_no_schema_error(self):
28 # A ValueError is raised if the URL schema is missing.
29 expected = "charm URL has no schema: 'precise/juju-gui'"
30 with self.assert_value_error(expected):
31 charms.parse_url('precise/juju-gui')
32
33 def test_invalid_schema_error(self):
34 # A ValueError is raised if the URL schema is not valid.
35 expected = "charm URL has invalid schema: 'http'"
36 with self.assert_value_error(expected):
37 charms.parse_url('http:precise/juju-gui')
38
39 def test_invalid_user_form_error(self):
40 # A ValueError is raised if the user form is not valid.
41 expected = "charm URL has invalid user name form: 'jean-luc'"
42 with self.assert_value_error(expected):
43 charms.parse_url('cs:jean-luc/precise/juju-gui')
44
45 def test_invalid_user_name_error(self):
46 # A ValueError is raised if the user name is not valid.
47 expected = "charm URL has invalid user name: 'jean:luc'"
48 with self.assert_value_error(expected):
49 charms.parse_url('cs:~jean:luc/precise/juju-gui')
50
51 def test_local_user_name_error(self):
52 # A ValueError is raised if a user is specified on a local charm.
53 expected = (
54 'local charm URL with user name: '
55 "'local:~jean-luc/precise/juju-gui'")
56 with self.assert_value_error(expected):
57 charms.parse_url('local:~jean-luc/precise/juju-gui')
58
59 def test_invalid_form_error(self):
60 # A ValueError is raised if the URL is not valid.
61 expected = "charm URL has invalid form: 'cs:~user/series/name/what-?'"
62 with self.assert_value_error(expected):
63 charms.parse_url('cs:~user/series/name/what-?')
64
65 def test_invalid_series_error(self):
66 # A ValueError is raised if the series is not valid.
67 expected = "charm URL has invalid series: 'boo!'"
68 with self.assert_value_error(expected):
69 charms.parse_url('cs:boo!/juju-gui-42')
70
71 def test_no_revision_error(self):
72 # A ValueError is raised if the charm revision is missing.
73 expected = "charm URL has no revision: 'cs:series/name'"
74 with self.assert_value_error(expected):
75 charms.parse_url('cs:series/name')
76
77 def test_invalid_revision_error(self):
78 # A ValueError is raised if the charm revision is not valid.
79 expected = "charm URL has invalid revision: 'revision'"
80 with self.assert_value_error(expected):
81 charms.parse_url('cs:series/name-revision')
82
83 def test_invalid_name_error(self):
84 # A ValueError is raised if the charm name is not valid.
85 expected = "charm URL has invalid name: 'not:valid'"
86 with self.assert_value_error(expected):
87 charms.parse_url('cs:precise/not:valid-42')
88
89 def test_success_with_user(self):
90 # A charm URL including the user is correctly parsed.
91 schema, user, series, name, revision = charms.parse_url(
92 'cs:~who/precise/juju-gui-42')
93 self.assertEqual('cs', schema)
94 self.assertEqual('who', user)
95 self.assertEqual('precise', series)
96 self.assertEqual('juju-gui', name)
97 self.assertEqual(42, revision)
98
99 def test_success_without_user(self):
100 # A charm URL not including the user is correctly parsed.
101 schema, user, series, name, revision = charms.parse_url(
102 'cs:trusty/django-1')
103 self.assertEqual('cs', schema)
104 self.assertEqual('', user)
105 self.assertEqual('trusty', series)
106 self.assertEqual('django', name)
107 self.assertEqual(1, revision)
108
109 def test_success_local_charm(self):
110 # A local charm URL is correctly parsed.
111 schema, user, series, name, revision = charms.parse_url(
112 'local:saucy/wordpress-100')
113 self.assertEqual('local', schema)
114 self.assertEqual('', user)
115 self.assertEqual('saucy', series)
116 self.assertEqual('wordpress', name)
117 self.assertEqual(100, revision)
118
119
120class TestCharm(helpers.ValueErrorTestsMixin, unittest.TestCase):
121
122 def make_charm(
123 self, schema='cs', user='myuser', series='precise',
124 name='juju-gui', revision=42):
125 """Create and return a Charm instance."""
126 return charms.Charm(schema, user, series, name, revision)
127
128 def test_attributes(self):
129 # All charm attributes are correctly stored.
130 charm = self.make_charm()
131 self.assertEqual('cs', charm.schema)
132 self.assertEqual('myuser', charm.user)
133 self.assertEqual('precise', charm.series)
134 self.assertEqual('juju-gui', charm.name)
135 self.assertEqual(42, charm.revision)
136
137 def test_revision_as_string(self):
138 # Revision is converted to an int.
139 charm = self.make_charm(revision='47')
140 self.assertEqual(47, charm.revision)
141
142 def test_from_url(self):
143 # A Charm can be instantiated from a charm URL.
144 charm = charms.Charm.from_url('cs:~who/trusty/django-1')
145 self.assertEqual('cs', charm.schema)
146 self.assertEqual('who', charm.user)
147 self.assertEqual('trusty', charm.series)
148 self.assertEqual('django', charm.name)
149 self.assertEqual(1, charm.revision)
150
151 def test_from_url_without_user(self):
152 # Official charm store URLs are properly handled.
153 charm = charms.Charm.from_url('cs:saucy/django-123')
154 self.assertEqual('cs', charm.schema)
155 self.assertEqual('', charm.user)
156 self.assertEqual('saucy', charm.series)
157 self.assertEqual('django', charm.name)
158 self.assertEqual(123, charm.revision)
159
160 def test_from_url_local(self):
161 # Local charms URLs are properly handled.
162 charm = charms.Charm.from_url('local:precise/my-local-charm-42')
163 self.assertEqual('local', charm.schema)
164 self.assertEqual('', charm.user)
165 self.assertEqual('precise', charm.series)
166 self.assertEqual('my-local-charm', charm.name)
167 self.assertEqual(42, charm.revision)
168
169 def test_from_url_error(self):
170 # A ValueError is raised by the from_url class method if the provided
171 # URL is not a valid charm URL.
172 expected = "charm URL has invalid form: 'cs:not-a-charm-url'"
173 with self.assert_value_error(expected):
174 charms.Charm.from_url('cs:not-a-charm-url')
175
176 def test_string(self):
177 # The string representation of a charm instance is its URL.
178 charm = self.make_charm()
179 self.assertEqual('cs:~myuser/precise/juju-gui-42', str(charm))
180
181 def test_repr(self):
182 # A charm instance is correctly represented.
183 charm = self.make_charm()
184 self.assertEqual(
185 '<Charm: cs:~myuser/precise/juju-gui-42>', repr(charm))
186
187 def test_charm_store_url(self):
188 # A charm store URL is correctly returned.
189 charm = self.make_charm(schema='cs')
190 self.assertEqual('cs:~myuser/precise/juju-gui-42', charm.url())
191
192 def test_local_url(self):
193 # A local charm URL is correctly returned.
194 charm = self.make_charm(schema='local', user='')
195 self.assertEqual('local:precise/juju-gui-42', charm.url())
196
197 def test_charm_store_charm(self):
198 # The is_local method returns False for charm store charms.
199 charm = self.make_charm(schema='cs')
200 self.assertFalse(charm.is_local())
201
202 def test_local_charm(self):
203 # The is_local method returns True for local charms.
204 charm = self.make_charm(schema='local')
205 self.assertTrue(charm.is_local())
0206
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2013-11-20 11:40:10 +0000
+++ quickstart/tests/test_manage.py 2013-11-22 15:07:41 +0000
@@ -178,6 +178,69 @@
178 self.parser.error.assert_called_once_with(expected)178 self.parser.error.assert_called_once_with(expected)
179179
180180
181class TestValidateCharmUrl(unittest.TestCase):
182
183 def setUp(self):
184 self.parser = mock.Mock()
185
186 def make_options(self, charm_url, has_bundle=False):
187 """Return a mock options object which includes the passed arguments."""
188 options = mock.Mock(charm_url=charm_url, bundle=None)
189 if has_bundle:
190 options.bundle = 'bundle:~who/django/42/django'
191 return options
192
193 def test_invalid_url_error(self):
194 # A parser error is invoked if the charm URL is not valid.
195 options = self.make_options('cs:invalid')
196 manage._validate_charm_url(options, self.parser)
197 expected = "charm URL has invalid form: 'cs:invalid'"
198 self.parser.error.assert_called_once_with(expected)
199
200 def test_local_charm_error(self):
201 # A parser error is invoked if a local charm is provided.
202 options = self.make_options('local:precise/juju-gui-100')
203 manage._validate_charm_url(options, self.parser)
204 expected = 'local charms are not allowed: local:precise/juju-gui-100'
205 self.parser.error.assert_called_once_with(expected)
206
207 def test_unsupported_series_error(self):
208 # A parser error is invoked if the charm series is not supported.
209 options = self.make_options('cs:nosuch/juju-gui-100')
210 manage._validate_charm_url(options, self.parser)
211 expected = "unsupported charm series: 'nosuch'"
212 self.parser.error.assert_called_once_with(expected)
213
214 def test_outdated_charm_error(self):
215 # A parser error is invoked if a bundle deployment has been requested
216 # but the provided charm does not support bundles.
217 options = self.make_options('cs:precise/juju-gui-1', has_bundle=True)
218 manage._validate_charm_url(options, self.parser)
219 expected = (
220 'bundle deployments not supported by the requested charm '
221 'revision: cs:precise/juju-gui-1')
222 self.parser.error.assert_called_once_with(expected)
223
224 def test_outdated_allowed_without_bundles(self):
225 # An outdated charm is allowed if no bundles are provided.
226 options = self.make_options('cs:precise/juju-gui-1', has_bundle=False)
227 manage._validate_charm_url(options, self.parser)
228 self.assertFalse(self.parser.error.called)
229
230 def test_success(self):
231 # The functions completes without error if the charm URL is valid.
232 good = (
233 'cs:precise/juju-gui-100',
234 'cs:~juju-gui/precise/juju-gui-42',
235 'cs:~who/precise/juju-gui-42',
236 'cs:~who/precise/my-juju-gui-42',
237 )
238 for charm_url in good:
239 options = self.make_options(charm_url)
240 manage._validate_charm_url(options, self.parser)
241 self.assertFalse(self.parser.error.called, charm_url)
242
243
181class TestValidateEnv(helpers.EnvFileTestsMixin, unittest.TestCase):244class TestValidateEnv(helpers.EnvFileTestsMixin, unittest.TestCase):
182245
183 def setUp(self):246 def setUp(self):
@@ -316,6 +379,16 @@
316 self.assertIsInstance(options, argparse.Namespace)379 self.assertIsInstance(options, argparse.Namespace)
317 self.assertIsInstance(parser, argparse.ArgumentParser)380 self.assertIsInstance(parser, argparse.ArgumentParser)
318381
382 @mock.patch('quickstart.manage._validate_charm_url')
383 def test_charm_url(self, mock_validate_charm_url):
384 # The charm URL validation process is started if a URL is provided.
385 self.call_setup(
386 ['--gui-charm-url', 'cs:precise/juju-gui-42'], exit_called=False)
387 self.assertTrue(mock_validate_charm_url.called)
388 options, parser = mock_validate_charm_url.call_args_list[0][0]
389 self.assertIsInstance(options, argparse.Namespace)
390 self.assertIsInstance(parser, argparse.ArgumentParser)
391
319 def test_configure_logging(self):392 def test_configure_logging(self):
320 # Logging is properly set up at the info level.393 # Logging is properly set up at the info level.
321 logger = logging.getLogger()394 logger = logging.getLogger()
@@ -359,7 +432,7 @@
359 mock_app.connect.assert_called_once_with(432 mock_app.connect.assert_called_once_with(
360 mock_app.get_api_url(), options.admin_secret)433 mock_app.get_api_url(), options.admin_secret)
361 mock_app.deploy_gui.assert_called_once_with(434 mock_app.deploy_gui.assert_called_once_with(
362 mock_app.connect(), settings.JUJU_GUI_NAME, '0',435 mock_app.connect(), settings.JUJU_GUI_SERVICE_NAME, '0',
363 charm_url=options.charm_url,436 charm_url=options.charm_url,
364 check_preexisting=mock_app.bootstrap())437 check_preexisting=mock_app.bootstrap())
365 mock_app.watch.assert_called_once_with(438 mock_app.watch.assert_called_once_with(
366439
=== modified file 'quickstart/tests/test_utils.py'
--- quickstart/tests/test_utils.py 2013-11-20 20:30:08 +0000
+++ quickstart/tests/test_utils.py 2013-11-22 15:07:41 +0000
@@ -78,6 +78,36 @@
78 utils.call('echo', 'we are the borg!')78 utils.call('echo', 'we are the borg!')
7979
8080
81@mock.patch('__builtin__.print', mock.Mock())
82class TestCheckGuiCharmUrl(unittest.TestCase):
83
84 def test_customized(self):
85 # A customized charm URL is properly logged.
86 expected = 'using a customized juju-gui charm'
87 with helpers.assert_logs([expected], level='warn'):
88 utils.check_gui_charm_url('cs:~juju-gui/precise/juju-gui-28')
89
90 def test_outdated(self):
91 # An outdated charm URL is properly logged.
92 expected = 'charm is outdated and may not support bundle deployments'
93 with helpers.assert_logs([expected], level='warn'):
94 utils.check_gui_charm_url('cs:precise/juju-gui-1')
95
96 def test_unexpected(self):
97 # An unexpected charm URL is properly logged.
98 expected = (
99 'unexpected URL for the juju-gui charm: the service may not work '
100 'as expected')
101 with helpers.assert_logs([expected], level='warn'):
102 utils.check_gui_charm_url('cs:precise/another-gui-42')
103
104 def test_official(self):
105 # No warnings are logged if an up to date charm is passed.
106 with mock.patch('logging.warn') as mock_warn:
107 utils.check_gui_charm_url('cs:precise/juju-gui-100')
108 self.assertFalse(mock_warn.called)
109
110
81class TestConvertBundleUrl(helpers.ValueErrorTestsMixin, unittest.TestCase):111class TestConvertBundleUrl(helpers.ValueErrorTestsMixin, unittest.TestCase):
82112
83 def test_conversion(self):113 def test_conversion(self):
@@ -338,7 +368,7 @@
338 def test_yaml_gui_in_services(self):368 def test_yaml_gui_in_services(self):
339 # A ValueError is raised if the bundle contains juju-gui.369 # A ValueError is raised if the bundle contains juju-gui.
340 contents = yaml.safe_dump({370 contents = yaml.safe_dump({
341 'mybundle': {'services': {'juju-gui': {}}},371 'mybundle': {'services': {settings.JUJU_GUI_SERVICE_NAME: {}}},
342 })372 })
343 expected = 'bundle mybundle contains an instance of juju-gui. ' \373 expected = 'bundle mybundle contains an instance of juju-gui. ' \
344 'quickstart will install the latest version of the Juju GUI ' \374 'quickstart will install the latest version of the Juju GUI ' \
345375
=== modified file 'quickstart/utils.py'
--- quickstart/utils.py 2013-11-20 20:30:08 +0000
+++ quickstart/utils.py 2013-11-22 15:07:41 +0000
@@ -16,6 +16,7 @@
1616
17"""Juju Quickstart utility functions and classes."""17"""Juju Quickstart utility functions and classes."""
1818
19from __future__ import print_function
19import collections20import collections
20import httplib21import httplib
21import json22import json
@@ -29,7 +30,10 @@
2930
30import yaml31import yaml
3132
32from quickstart import settings33from quickstart import (
34 charms,
35 settings
36)
3337
34# Compile the regular expression used to parse the "juju switch" output.38# Compile the regular expression used to parse the "juju switch" output.
35_juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n')39_juju_switch_expression = re.compile(r'Current environment: "([\w-]+)"\n')
@@ -59,6 +63,26 @@
59 return retcode, output, error63 return retcode, output, error
6064
6165
66def check_gui_charm_url(charm_url):
67 """Print (to stdout or to logs) info and warnings about the charm URL."""
68 print('charm URL: {}'.format(charm_url))
69 charm = charms.Charm.from_url(charm_url)
70 charm_name = settings.JUJU_GUI_CHARM_NAME
71 if charm.name == charm_name:
72 if charm.user or charm.is_local():
73 # This is not the official Juju GUI charm.
74 logging.warn('using a customized {} charm'.format(charm_name))
75 elif charm.revision < settings.MINIMUM_CHARM_REVISION_FOR_BUNDLES:
76 # This is the official Juju GUI charm, but it is outdated.
77 logging.warn(
78 'charm is outdated and may not support bundle deployments')
79 else:
80 # This does not seem to be a Juju GUI charm.
81 logging.warn(
82 'unexpected URL for the {} charm: '
83 'the service may not work as expected'.format(charm_name))
84
85
62def convert_bundle_url(bundle_url):86def convert_bundle_url(bundle_url):
63 """Return the equivalent YAML HTTPS location for the fiven bundle URL.87 """Return the equivalent YAML HTTPS location for the fiven bundle URL.
6488
@@ -189,7 +213,7 @@
189 if not bundle_services:213 if not bundle_services:
190 msg = 'bundle {} does not include any services'.format(bundle_name)214 msg = 'bundle {} does not include any services'.format(bundle_name)
191 raise ValueError(msg)215 raise ValueError(msg)
192 if settings.JUJU_GUI_NAME in bundle_services:216 if settings.JUJU_GUI_SERVICE_NAME in bundle_services:
193 raise ValueError('bundle {} contains an instance of juju-gui. '217 raise ValueError('bundle {} contains an instance of juju-gui. '
194 'quickstart will install the latest version of the '218 'quickstart will install the latest version of the '
195 'Juju GUI automatically, please remove juju-gui from '219 'Juju GUI automatically, please remove juju-gui from '

Subscribers

People subscribed via source and target branches