Merge lp:~frankban/juju-quickstart/support-json into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 10
Proposed branch: lp:~frankban/juju-quickstart/support-json
Merge into: lp:juju-quickstart
Diff against target: 564 lines (+191/-50)
12 files modified
.bzrignore (+1/-0)
HACKING.rst (+1/-1)
README.rst (+1/-1)
quickstart/__init__.py (+1/-1)
quickstart/app.py (+20/-10)
quickstart/manage.py (+36/-10)
quickstart/tests/helpers.py (+26/-5)
quickstart/tests/test_app.py (+43/-12)
quickstart/tests/test_manage.py (+38/-2)
quickstart/tests/test_utils.py (+19/-6)
quickstart/utils.py (+4/-1)
setup.py (+1/-1)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/support-json
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+194172@code.launchpad.net

Description of the change

JSON support + minor fixes.

This branch includes official JSON
support for bundle deployments.

This branch also contains a number of
minor fixes/improvements:

- Added the possibility to provide a
  customized Juju GUI charm URL.

- Added support for providing a bundle
  directory (a dir containing a
  bundles.yaml file): fix bug 1247181.

- Some documentation clean up.

- Added an option to decline opening the
  browser.

- The Juju env is bootstrapped passing
  --debug when quickstart is run in debug
  mode.

- Bumped version up.

Tests: `make check`.

QA:
I uploaded a bundle JSON here:
http://pastebin.com/raw.php?i=4mDCcagp
So it is possible to check the new features
by running the following (expect a scary output):
.venv/bin/python juju-quickstart --debug --no-browser \
 --gui-charm-url cs:~juju-gui/precise/juju-gui-116 \
 -e ec2 http://pastebin.com/raw.php?i=4mDCcagp
At the end of the process, locate the GUI url
printed before the last debug messages, and check
the GUI is up, running and the bundle is deploying.
Remember to destroy your ec2 env.
Thank you!

https://codereview.appspot.com/22300044/

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

Reviewers: mp+194172_code.launchpad.net,

Message:
Please take a look.

Description:
JSON support + minor fixes.

This branch includes official JSON
support for bundle deployments.

This branch also contains a number of
minor fixes/improvements:

- Added the possibility to provide a
   customized Juju GUI charm URL.

- Added support for providing a bundle
   directory (a dir containing a
   bundles.yaml file): fix bug 1247181.

- Some documentation clean up.

- Added an option to decline opening the
   browser.

- The Juju env is bootstrapped passing
   --debug when quickstart is run in debug
   mode.

- Bumped version up.

Tests: `make check`.

QA:
I uploaded a bundle JSON here:
http://pastebin.com/raw.php?i=4mDCcagp
So it is possible to check the new features
by running the following (expect a scary output):
.venv/bin/python juju-quickstart --debug --no-browser \
 --gui-charm-url cs:~juju-gui/precise/juju-gui-116 \
 -e ec2 http://pastebin.com/raw.php?i=4mDCcagp
At the end of the process, locate the GUI url
printed before the last debug messages, and check
the GUI is up, running and the bundle is deploying.
Remember to destroy your ec2 env.
Thank you!

https://code.launchpad.net/~frankban/juju-quickstart/support-json/+merge/194172

(do not edit description out of merge proposal)

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

Affected files (+190, -50 lines):
   M .bzrignore
   M HACKING.rst
   M README.rst
   A [revision details]
   M quickstart/__init__.py
   M quickstart/app.py
   M quickstart/manage.py
   M quickstart/tests/helpers.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_manage.py
   M quickstart/tests/test_utils.py
   M quickstart/utils.py
   M setup.py

Revision history for this message
Madison Scott-Clary (makyo) wrote :
Revision history for this message
Gary Poster (gary) wrote :

LGTM with trivial. Thank you!

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

https://codereview.appspot.com/22300044/diff/1/quickstart/app.py#newcode123
quickstart/app.py:123: and the optional Juju GUI charm URL. If the charm
URL is not provided, the
Maybe give an example of the expected format, e.g.
cs:~juju-gui/precise/juju-gui-116 ? Maybe that's more
valuable/important for the commandline help.

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

https://codereview.appspot.com/22300044/diff/1/quickstart/manage.py#newcode181
quickstart/manage.py:181: 'provided, the last release of the GUI will be
deployed')
This would be the ideal place to include an example IMO, like mentioning
that you can get the dev charm by using cs:~juju-gui/precise/juju-gui

https://codereview.appspot.com/22300044/

16. By Francesco Banconi

Changes as per review.

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

*** Submitted:

JSON support + minor fixes.

This branch includes official JSON
support for bundle deployments.

This branch also contains a number of
minor fixes/improvements:

- Added the possibility to provide a
   customized Juju GUI charm URL.

- Added support for providing a bundle
   directory (a dir containing a
   bundles.yaml file): fix bug 1247181.

- Some documentation clean up.

- Added an option to decline opening the
   browser.

- The Juju env is bootstrapped passing
   --debug when quickstart is run in debug
   mode.

- Bumped version up.

Tests: `make check`.

QA:
I uploaded a bundle JSON here:
http://pastebin.com/raw.php?i=4mDCcagp
So it is possible to check the new features
by running the following (expect a scary output):
.venv/bin/python juju-quickstart --debug --no-browser \
 --gui-charm-url cs:~juju-gui/precise/juju-gui-116 \
 -e ec2 http://pastebin.com/raw.php?i=4mDCcagp
At the end of the process, locate the GUI url
printed before the last debug messages, and check
the GUI is up, running and the bundle is deploying.
Remember to destroy your ec2 env.
Thank you!

R=
CC=
https://codereview.appspot.com/22300044

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

https://codereview.appspot.com/22300044/diff/1/quickstart/app.py#newcode123
quickstart/app.py:123: and the optional Juju GUI charm URL. If the charm
URL is not provided, the
On 2013/11/06 17:40:43, gary.poster wrote:
> Maybe give an example of the expected format, e.g.
> cs:~juju-gui/precise/juju-gui-116 ? Maybe that's more
valuable/important for
> the commandline help.

Done.

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

https://codereview.appspot.com/22300044/diff/1/quickstart/manage.py#newcode181
quickstart/manage.py:181: 'provided, the last release of the GUI will be
deployed')
On 2013/11/06 17:40:43, gary.poster wrote:
> This would be the ideal place to include an example IMO, like
mentioning that
> you can get the dev charm by using cs:~juju-gui/precise/juju-gui

Done.

https://codereview.appspot.com/22300044/

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file '.bzrignore'
--- .bzrignore 2013-10-14 15:54:00 +0000
+++ .bzrignore 2013-11-06 17:54:24 +0000
@@ -1,4 +1,5 @@
1.coverage1.coverage
2.DS_Store
2.emacs*3.emacs*
3.venv4.venv
4build5build
56
=== modified file 'HACKING.rst'
--- HACKING.rst 2013-10-14 15:24:29 +0000
+++ HACKING.rst 2013-11-06 17:54:24 +0000
@@ -5,7 +5,7 @@
5environment in very few steps. The environment is bootstrapped and set up so5environment in very few steps. The environment is bootstrapped and set up so
6that it can be managed using a Web interface (the Juju GUI).6that it can be managed using a Web interface (the Juju GUI).
77
8Bundle support is a planned feature, and will allow for setting up a complete8Bundle deployments are also supported, and allow for setting up a complete
9topology of services in one simple command.9topology of services in one simple command.
1010
11Creating a development environment11Creating a development environment
1212
=== modified file 'README.rst'
--- README.rst 2013-10-14 15:24:29 +0000
+++ README.rst 2013-11-06 17:54:24 +0000
@@ -5,5 +5,5 @@
5environment in very few steps. The environment is bootstrapped and set up so5environment in very few steps. The environment is bootstrapped and set up so
6that it can be managed using a Web interface (the Juju GUI).6that it can be managed using a Web interface (the Juju GUI).
77
8Bundle support is a planned feature, and will allow for setting up a complete8Bundle deployments are also supported, and allow for setting up a complete
9topology of services in one simple command.9topology of services in one simple command.
1010
=== modified file 'quickstart/__init__.py'
--- quickstart/__init__.py 2013-10-30 10:16:36 +0000
+++ quickstart/__init__.py 2013-11-06 17:54:24 +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, 2, 0)22VERSION = (0, 3, 0)
2323
2424
25def get_version():25def get_version():
2626
=== modified file 'quickstart/app.py'
--- quickstart/app.py 2013-10-30 17:37:46 +0000
+++ quickstart/app.py 2013-11-06 17:54:24 +0000
@@ -48,13 +48,18 @@
48 return 'juju-quickstart: error: {}'.format(self.message)48 return 'juju-quickstart: error: {}'.format(self.message)
4949
5050
51def bootstrap(env_name):51def bootstrap(env_name, debug=False):
52 """Bootstrap the Juju environment with the given name.52 """Bootstrap the Juju environment with the given name.
5353
54 If debug is True, bootstrap the environment passing the --debug flag.
55
54 Return when the bootstrap node is ready.56 Return when the bootstrap node is ready.
55 Raise a ProgramExit if any error occurs in the bootstrap process.57 Raise a ProgramExit if any error occurs in the bootstrap process.
56 """58 """
57 retcode, _, error = utils.call('juju', 'bootstrap', '-e', env_name)59 cmd = ['juju', 'bootstrap', '-e', env_name]
60 if debug:
61 cmd.append('--debug')
62 retcode, _, error = utils.call(*cmd)
58 if retcode:63 if retcode:
59 raise ProgramExit(error)64 raise ProgramExit(error)
60 # Call "juju status" multiple times until the bootstrap node is ready.65 # Call "juju status" multiple times until the bootstrap node is ready.
@@ -111,20 +116,25 @@
111 return env116 return env
112117
113118
114def deploy_gui(env, service_name):119def deploy_gui(env, service_name, charm_url=None):
115 """Deploy and expose the given service, reusing the bootstrap node.120 """Deploy and expose the given service, reusing the bootstrap node.
116121
117 Receive an authenticated Juju Environment instance, the name of the service122 Receive an authenticated Juju Environment instance, the name of the service
118 and the corresponding charm URL.123 and the optional Juju GUI charm URL, e.g. cs:~juju-gui/precise/juju-gui-42.
124 If the charm URL is not provided, the function tries to retrieve it from
125 charmworld. In this case a default charm URL is used if charmworld is not
126 available.
119127
120 Raise a ProgramExit if the API server returns an error response.128 Raise a ProgramExit if the API server returns an error response.
121 """129 """
122 try:130 if charm_url is None:
123 charm_url = utils.get_charm_url()131 try:
124 except (IOError, ValueError) as err:132 charm_url = utils.get_charm_url()
125 msg = 'unable to retrieve the Juju GUI charm URL from the API: {}'133 except (IOError, ValueError) as err:
126 logging.warn(msg.format(err))134 msg = 'unable to retrieve the Juju GUI charm URL from the API: {}'
127 charm_url = DEFAULT_CHARM_URL135 logging.warn(msg.format(err))
136 charm_url = DEFAULT_CHARM_URL
137 print('charm URL: {}'.format(charm_url))
128 try:138 try:
129 env.deploy(service_name, charm_url, to=0)139 env.deploy(service_name, charm_url, to=0)
130 env.expose(service_name)140 env.expose(service_name)
131141
=== modified file 'quickstart/manage.py'
--- quickstart/manage.py 2013-10-30 17:37:46 +0000
+++ quickstart/manage.py 2013-11-06 17:54:24 +0000
@@ -62,6 +62,8 @@
62 else:62 else:
63 # Load the bundle file.63 # Load the bundle file.
64 bundle_file = os.path.abspath(os.path.expanduser(bundle))64 bundle_file = os.path.abspath(os.path.expanduser(bundle))
65 if os.path.isdir(bundle_file):
66 bundle_file = os.path.join(bundle_file, 'bundles.yaml')
65 try:67 try:
66 bundle_yaml = open(bundle_file).read()68 bundle_yaml = open(bundle_file).read()
67 except IOError as err:69 except IOError as err:
@@ -131,9 +133,19 @@
131133
132 Return the options as a namespace containing the following attributes:134 Return the options as a namespace containing the following attributes:
133 - admin_secret: the password to use to access the Juju API;135 - admin_secret: the password to use to access the Juju API;
136 - bundle: the optional bundle (path or URL) to be deployed;
137 - charm_url: the Juju GUI charm URL or None if not specified;
138 - debug: whether debug mode is activated;
134 - env_file: the absolute path of the Juju environments.yaml file;139 - env_file: the absolute path of the Juju environments.yaml file;
135 - env_name: the name of the Juju environment to use;140 - env_name: the name of the Juju environment to use;
136 - env_type: the provider type of the selected Juju environment.141 - env_type: the provider type of the selected Juju environment;
142 - open_browser: whether the GUI browser must be opened.
143
144 The following attributes will also be included in the namespace if a bundle
145 deployment is requested:
146 - bundle_name: the name of the bundle to be deployed;
147 - bundle_services: a list of service names included in the bundle;
148 - bundle_yaml: the YAML encoded contents of the bundle.
137149
138 Exit with an error if the provided arguments are not valid.150 Exit with an error if the provided arguments are not valid.
139 """151 """
@@ -146,24 +158,39 @@
146 parser = argparse.ArgumentParser(description=quickstart.__doc__)158 parser = argparse.ArgumentParser(description=quickstart.__doc__)
147 parser.add_argument(159 parser.add_argument(
148 'bundle', default=None, nargs='?',160 'bundle', default=None, nargs='?',
149 help='The bundle URL or the path to the bundle file to deploy')161 help='The optional bundle to be deployed. The bundle can be '
162 '1) a path to a YAML/JSON file or '
163 '2) a path to a directory containing a bundles.yaml file or '
164 '3) a URL (starting with http:// or https://) to a YAML/JSON')
150 parser.add_argument(165 parser.add_argument(
151 '-e', '--environment', default=default_env_name, dest='env_name',166 '-e', '--environment', default=default_env_name, dest='env_name',
152 help=env_help)167 help=env_help)
153 parser.add_argument(168 parser.add_argument(
154 '-n', '--bundle-name', default=None, dest='bundle_name',169 '-n', '--bundle-name', default=None, dest='bundle_name',
155 help='The name of the bundle to use. This must be included in the '170 help='The name of the bundle to use. This must be included in the '
156 'provided bundle file. Specifying the bundle name is not '171 'provided bundle YAML/JSON. Specifying the bundle name is not '
157 'required if the bundle file only contains one bundle. This '172 'required if the bundle YAML/JSON only contains one bundle. This '
158 'option is ignored if the bundle file is not specified')173 'option is ignored if the bundle file is not specified')
159 parser.add_argument(174 parser.add_argument(
160 '--environments-file',175 '--environments-file',
161 default=os.path.join(juju_home, 'environments.yaml'), dest='env_file',176 default=os.path.join(juju_home, 'environments.yaml'), dest='env_file',
162 help='The path to the Juju environments YAML file (%(default)s)')177 help='The path to the Juju environments YAML file (%(default)s)')
163 parser.add_argument(178 parser.add_argument(
179 '--gui-charm-url', dest='charm_url',
180 help='The Juju GUI charm URL to deploy in the environment. If not '
181 'provided, the last release of the GUI will be deployed. The '
182 'charm URL must include the charm version, e.g. '
183 'cs:~juju-gui/precise/juju-gui-116')
184 parser.add_argument(
185 '--no-browser', action='store_false', dest='open_browser',
186 help='Avoid opening the browser to the GUI at the end of the process')
187 parser.add_argument(
164 '--version', action='version', version='%(prog)s {}'.format(version))188 '--version', action='version', version='%(prog)s {}'.format(version))
165 parser.add_argument(189 parser.add_argument(
166 '--debug', action='store_true', help='Turn debug mode on')190 '--debug', action='store_true',
191 help='Turn debug mode on. When enabled, all the subcommands and API '
192 'calls are logged to stdout, and the Juju environment is '
193 'bootstrapped passing --debug')
167 # This is required by juju-core: see "juju help plugins".194 # This is required by juju-core: see "juju help plugins".
168 parser.add_argument(195 parser.add_argument(
169 '--description', action=_DescriptionAction, default=argparse.SUPPRESS,196 '--description', action=_DescriptionAction, default=argparse.SUPPRESS,
@@ -184,13 +211,13 @@
184 print('juju quickstart v{}'.format(version))211 print('juju quickstart v{}'.format(version))
185 print('bootstrapping the {} environment (type: {})'.format(212 print('bootstrapping the {} environment (type: {})'.format(
186 options.env_name, options.env_type))213 options.env_name, options.env_type))
187 app.bootstrap(options.env_name)214 app.bootstrap(options.env_name, debug=options.debug)
188 print('retrieving the Juju API address')215 print('retrieving the Juju API address')
189 api_url = app.get_api_url(options.env_name)216 api_url = app.get_api_url(options.env_name)
190 print('connecting to {}'.format(api_url))217 print('connecting to {}'.format(api_url))
191 env = app.connect(api_url, options.admin_secret)218 env = app.connect(api_url, options.admin_secret)
192 print('requesting Juju GUI deployment')219 print('requesting Juju GUI deployment')
193 app.deploy_gui(env, 'juju-gui')220 app.deploy_gui(env, 'juju-gui', charm_url=options.charm_url)
194 print('Juju GUI deployment request accepted')221 print('Juju GUI deployment request accepted')
195 address = app.watch(env, 'juju-gui')222 address = app.watch(env, 'juju-gui')
196 url = 'https://{}'.format(address)223 url = 'https://{}'.format(address)
@@ -208,7 +235,6 @@
208 gui_api_url, options.admin_secret,235 gui_api_url, options.admin_secret,
209 options.bundle_yaml, options.bundle_name)236 options.bundle_yaml, options.bundle_name)
210237
211 # XXX 2013-10-18 frankban:238 if options.open_browser:
212 # Add a command line option to decline opening the browser.239 webbrowser.open(url)
213 webbrowser.open(url)
214 print('done!')240 print('done!')
215241
=== modified file 'quickstart/tests/helpers.py'
--- quickstart/tests/helpers.py 2013-10-30 17:37:46 +0000
+++ quickstart/tests/helpers.py 2013-11-06 17:54:24 +0000
@@ -18,6 +18,7 @@
1818
19from contextlib import contextmanager19from contextlib import contextmanager
20import os20import os
21import shutil
21import tempfile22import tempfile
2223
23import mock24import mock
@@ -45,6 +46,14 @@
45 'bundle2': {'services': {'django': {}, 'nodejs': {}}},46 'bundle2': {'services': {'django': {}, 'nodejs': {}}},
46 })47 })
4748
49 def _write_bundle_file(self, bundle_file, contents):
50 """Parse and write contents into the given bundle file object."""
51 if contents is None:
52 contents = self.valid_bundle
53 elif isinstance(contents, dict):
54 contents = yaml.safe_dump(contents)
55 bundle_file.write(contents)
56
48 def make_bundle_file(self, contents=None):57 def make_bundle_file(self, contents=None):
49 """Create a Juju bundle file containing the given contents.58 """Create a Juju bundle file containing the given contents.
5059
@@ -52,16 +61,28 @@
52 self.valid_bundle.61 self.valid_bundle.
53 Return the bundle file path.62 Return the bundle file path.
54 """63 """
55 if contents is None:
56 contents = self.valid_bundle
57 elif isinstance(contents, dict):
58 contents = yaml.safe_dump(contents)
59 bundle_file = tempfile.NamedTemporaryFile(delete=False)64 bundle_file = tempfile.NamedTemporaryFile(delete=False)
60 self.addCleanup(os.remove, bundle_file.name)65 self.addCleanup(os.remove, bundle_file.name)
61 bundle_file.write(contents)66 self._write_bundle_file(bundle_file, contents)
62 bundle_file.close()67 bundle_file.close()
63 return bundle_file.name68 return bundle_file.name
6469
70 def make_bundle_dir(self, contents=None):
71 """Create a Juju bundle directory including a bundles.yaml file.
72
73 The file will contain the given contents.
74
75 If contents is None, use the valid bundle contents defined in
76 self.valid_bundle.
77 Return the bundle directory path.
78 """
79 bundle_dir = tempfile.mkdtemp()
80 self.addCleanup(shutil.rmtree, bundle_dir)
81 bundle_path = os.path.join(bundle_dir, 'bundles.yaml')
82 with open(bundle_path, 'w') as bundle_file:
83 self._write_bundle_file(bundle_file, contents)
84 return bundle_dir
85
6586
66class CallTestsMixin(object):87class CallTestsMixin(object):
67 """Easily use the quickstart.utils.call function."""88 """Easily use the quickstart.utils.call function."""
6889
=== modified file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 2013-10-30 17:37:46 +0000
+++ quickstart/tests/test_app.py 2013-11-06 17:54:24 +0000
@@ -28,6 +28,9 @@
28from quickstart.tests import helpers28from quickstart.tests import helpers
2929
3030
31mock_print = mock.patch('__builtin__.print')
32
33
31class TestProgramExit(unittest.TestCase):34class TestProgramExit(unittest.TestCase):
3235
33 def test_string_representation(self):36 def test_string_representation(self):
@@ -72,6 +75,13 @@
72 'juju', 'status', '-e', self.env_name, '--format', 'yaml')75 'juju', 'status', '-e', self.env_name, '--format', 'yaml')
73 return [call for _ in range(number)]76 return [call for _ in range(number)]
7477
78 def make_side_effects(self):
79 """Return the minimum number of side effects for a successful call."""
80 return [
81 (0, '', ''), # Add a bootstrap call.
82 (0, self.make_status_output('started'), ''), # Add a status call.
83 ]
84
75 def assert_status_retried(self, side_effects):85 def assert_status_retried(self, side_effects):
76 """Ensure the "juju status" command is retried several times.86 """Ensure the "juju status" command is retried several times.
7787
@@ -85,16 +95,20 @@
8595
86 def test_success(self):96 def test_success(self):
87 # The environment is successfully bootstrapped.97 # The environment is successfully bootstrapped.
88 side_effects = [98 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:
89 (0, '', ''), # Add a bootstrap call.
90 (0, self.make_status_output('started'), ''), # Add a status call.
91 ]
92 with self.patch_multiple_calls(side_effects) as mock_call:
93 app.bootstrap(self.env_name)99 app.bootstrap(self.env_name)
94 mock_call.assert_has_calls([100 mock_call.assert_has_calls([
95 mock.call('juju', 'bootstrap', '-e', self.env_name),101 mock.call('juju', 'bootstrap', '-e', self.env_name),
96 ] + self.make_status_calls(1))102 ] + self.make_status_calls(1))
97103
104 def test_success_debug(self):
105 # The environment is successfully bootstrapped in debug mode.
106 with self.patch_multiple_calls(self.make_side_effects()) as mock_call:
107 app.bootstrap(self.env_name, debug=True)
108 mock_call.assert_has_calls([
109 mock.call('juju', 'bootstrap', '-e', self.env_name, '--debug'),
110 ] + self.make_status_calls(1))
111
98 def test_bootstrap_failure(self):112 def test_bootstrap_failure(self):
99 # A ProgramExit is raised if an error occurs while bootstrapping.113 # A ProgramExit is raised if an error occurs while bootstrapping.
100 with self.patch_call(1, error='bad wolf') as mock_call:114 with self.patch_call(1, error='bad wolf') as mock_call:
@@ -233,6 +247,7 @@
233 self.assertIs(error, context_manager.exception)247 self.assertIs(error, context_manager.exception)
234248
235249
250@mock_print
236class TestDeployGui(ProgramExitTestsMixin, unittest.TestCase):251class TestDeployGui(ProgramExitTestsMixin, unittest.TestCase):
237252
238 charm_url = 'cs:precise/juju-gui-42'253 charm_url = 'cs:precise/juju-gui-42'
@@ -244,18 +259,20 @@
244 mock_get_charm_url = mock.Mock(side_effect=side_effect)259 mock_get_charm_url = mock.Mock(side_effect=side_effect)
245 return mock.patch('quickstart.utils.get_charm_url', mock_get_charm_url)260 return mock.patch('quickstart.utils.get_charm_url', mock_get_charm_url)
246261
247 def test_deployment(self):262 def test_deployment(self, mock_print):
248 # The function correctly deploys and exposes the service, retrieving263 # The function correctly deploys and exposes the service, retrieving
249 # the charm URL from the charmworld API.264 # the charm URL from the charmworld API.
250 env = mock.Mock()265 env = mock.Mock()
251 with self.patch_get_charm_url():266 with self.patch_get_charm_url():
252 app.deploy_gui(env, 'my-gui')267 app.deploy_gui(env, 'my-gui')
253 env.assert_has_calls([268 env.assert_has_calls([
254 mock.call.deploy('my-gui', 'cs:precise/juju-gui-42', to=0),269 mock.call.deploy('my-gui', self.charm_url, to=0),
255 mock.call.expose('my-gui')270 mock.call.expose('my-gui')
256 ])271 ])
272 mock_print.assert_called_once_with(
273 'charm URL: {}'.format(self.charm_url))
257274
258 def test_deployment_default_charm_url(self):275 def test_deployment_default_charm_url(self, mock_print):
259 # The function correctly deploys and exposes the service, even if it is276 # The function correctly deploys and exposes the service, even if it is
260 # not able to retrieve the charm URL from the charmworld API.277 # not able to retrieve the charm URL from the charmworld API.
261 env = mock.Mock()278 env = mock.Mock()
@@ -268,8 +285,22 @@
268 mock.call.deploy('my-gui', app.DEFAULT_CHARM_URL, to=0),285 mock.call.deploy('my-gui', app.DEFAULT_CHARM_URL, to=0),
269 mock.call.expose('my-gui')286 mock.call.expose('my-gui')
270 ])287 ])
271288 mock_print.assert_called_once_with(
272 def test_api_error(self):289 'charm URL: {}'.format(app.DEFAULT_CHARM_URL))
290
291 def test_deployment_provided_charm_url(self, mock_print):
292 # The function correctly deploys and exposes the service using a user
293 # provided Juju GUI charm URL.
294 env = mock.Mock()
295 charm_url = 'cs:~juju-gui/precise/juju-gui-116'
296 app.deploy_gui(env, 'my-gui', charm_url=charm_url)
297 env.assert_has_calls([
298 mock.call.deploy('my-gui', charm_url, to=0),
299 mock.call.expose('my-gui')
300 ])
301 mock_print.assert_called_once_with('charm URL: {}'.format(charm_url))
302
303 def test_api_error(self, mock_print):
273 # A ProgramExit is raised if an error occurs in one of the API calls.304 # A ProgramExit is raised if an error occurs in one of the API calls.
274 env = mock.Mock()305 env = mock.Mock()
275 env.deploy.side_effect = self.make_env_error('service already exists')306 env.deploy.side_effect = self.make_env_error('service already exists')
@@ -280,7 +311,7 @@
280 env.deploy.assert_called_once_with(311 env.deploy.assert_called_once_with(
281 'another-gui', 'cs:precise/juju-gui-42', to=0)312 'another-gui', 'cs:precise/juju-gui-42', to=0)
282313
283 def test_other_errors(self):314 def test_other_errors(self, mock_print):
284 # Any other errors occurred during the process are not trapped.315 # Any other errors occurred during the process are not trapped.
285 error = ValueError('explode!')316 error = ValueError('explode!')
286 env = mock.Mock()317 env = mock.Mock()
@@ -294,7 +325,7 @@
294 self.assertIs(error, context_manager.exception)325 self.assertIs(error, context_manager.exception)
295326
296327
297@mock.patch('__builtin__.print')328@mock_print
298class TestWatch(ProgramExitTestsMixin, unittest.TestCase):329class TestWatch(ProgramExitTestsMixin, unittest.TestCase):
299330
300 address = 'unit.example.com'331 address = 'unit.example.com'
301332
=== modified file 'quickstart/tests/test_manage.py'
--- quickstart/tests/test_manage.py 2013-10-30 17:37:46 +0000
+++ quickstart/tests/test_manage.py 2013-11-06 17:54:24 +0000
@@ -19,6 +19,8 @@
19import argparse19import argparse
20import logging20import logging
21import os21import os
22import shutil
23import tempfile
22import unittest24import unittest
2325
24import mock26import mock
@@ -80,6 +82,17 @@
80 ['mysql', 'wordpress'], sorted(options.bundle_services))82 ['mysql', 'wordpress'], sorted(options.bundle_services))
81 self.assertEqual(open(bundle_file).read(), options.bundle_yaml)83 self.assertEqual(open(bundle_file).read(), options.bundle_yaml)
8284
85 def test_resulting_options_from_dir(self):
86 # The options object is correctly set up when a bundle dir is passed.
87 bundle_dir = self.make_bundle_dir()
88 options = self.make_options(bundle_dir, bundle_name='bundle1')
89 manage._validate_bundle(options, self.parser)
90 self.assertEqual('bundle1', options.bundle_name)
91 self.assertEqual(
92 ['mysql', 'wordpress'], sorted(options.bundle_services))
93 expected = open(os.path.join(bundle_dir, 'bundles.yaml')).read()
94 self.assertEqual(expected, options.bundle_yaml)
95
83 def test_expand_user(self):96 def test_expand_user(self):
84 # The ~ construct is correctly expanded in the validation process.97 # The ~ construct is correctly expanded in the validation process.
85 bundle_file = self.make_bundle_file()98 bundle_file = self.make_bundle_file()
@@ -104,6 +117,19 @@
104 )117 )
105 self.parser.error.assert_called_once_with(expected)118 self.parser.error.assert_called_once_with(expected)
106119
120 def test_bundle_dir_not_valid(self):
121 # A parser error is invoked if the bundle dir does not contain the
122 # bundles.yaml file.
123 bundle_dir = tempfile.mkdtemp()
124 self.addCleanup(shutil.rmtree, bundle_dir)
125 options = self.make_options(bundle_dir)
126 manage._validate_bundle(options, self.parser)
127 expected = (
128 'unable to open bundle file: '
129 "[Errno 2] No such file or directory: '{}/bundles.yaml'"
130 ).format(bundle_dir)
131 self.parser.error.assert_called_once_with(expected)
132
107 def test_url_error(self):133 def test_url_error(self):
108 # A parser error is invoked if the bundle cannot be fetched from the134 # A parser error is invoked if the bundle cannot be fetched from the
109 # provided URL.135 # provided URL.
@@ -283,9 +309,12 @@
283 """Set up the options to be passed to the run function."""309 """Set up the options to be passed to the run function."""
284 options = {310 options = {
285 'admin_secret': 'Secret!',311 'admin_secret': 'Secret!',
312 'debug': False,
313 'charm_url': None,
286 'bundle': None,314 'bundle': None,
287 'env_name': 'aws',315 'env_name': 'aws',
288 'env_type': 'ec2',316 'env_type': 'ec2',
317 'open_browser': True,
289 }318 }
290 options.update(kwargs)319 options.update(kwargs)
291 return mock.Mock(**options)320 return mock.Mock(**options)
@@ -294,12 +323,13 @@
294 # The application runs correctly if no bundle is provided.323 # The application runs correctly if no bundle is provided.
295 options = self.make_options()324 options = self.make_options()
296 manage.run(options)325 manage.run(options)
297 mock_app.bootstrap.assert_called_once_with(options.env_name)326 mock_app.bootstrap.assert_called_once_with(
327 options.env_name, debug=options.debug)
298 mock_app.get_api_url.assert_called_once_with(options.env_name)328 mock_app.get_api_url.assert_called_once_with(options.env_name)
299 mock_app.connect.assert_called_once_with(329 mock_app.connect.assert_called_once_with(
300 mock_app.get_api_url(), options.admin_secret)330 mock_app.get_api_url(), options.admin_secret)
301 mock_app.deploy_gui.assert_called_once_with(331 mock_app.deploy_gui.assert_called_once_with(
302 mock_app.connect(), 'juju-gui')332 mock_app.connect(), 'juju-gui', charm_url=options.charm_url)
303 mock_app.watch.assert_called_once_with(mock_app.connect(), 'juju-gui')333 mock_app.watch.assert_called_once_with(mock_app.connect(), 'juju-gui')
304 mock_open.assert_called_once_with(334 mock_open.assert_called_once_with(
305 'https://{}'.format(mock_app.watch()))335 'https://{}'.format(mock_app.watch()))
@@ -315,3 +345,9 @@
315 mock_app.deploy_bundle.assert_called_once_with(345 mock_app.deploy_bundle.assert_called_once_with(
316 'wss://gui.example.com:443/ws', options.admin_secret,346 'wss://gui.example.com:443/ws', options.admin_secret,
317 'mybundle: contents', 'mybundle')347 'mybundle: contents', 'mybundle')
348
349 def test_no_browser(self, mock_app, mock_open):
350 # It is possible to avoid opening the GUI in the browser.
351 options = self.make_options(open_browser=False)
352 manage.run(options)
353 self.assertFalse(mock_open.called)
318354
=== modified file 'quickstart/tests/test_utils.py'
--- quickstart/tests/test_utils.py 2013-10-31 08:33:49 +0000
+++ quickstart/tests/test_utils.py 2013-11-06 17:54:24 +0000
@@ -157,6 +157,14 @@
157 helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin,157 helpers.BundleFileTestsMixin, helpers.ValueErrorTestsMixin,
158 unittest.TestCase):158 unittest.TestCase):
159159
160 def assert_bundle(
161 self, expected_name, expected_services, contents,
162 bundle_name=None):
163 """Ensure parsing the given contents returns the expected values."""
164 name, services = utils.parse_bundle(contents, bundle_name=bundle_name)
165 self.assertEqual(expected_name, name)
166 self.assertEqual(set(expected_services), set(services))
167
160 def test_invalid_yaml(self):168 def test_invalid_yaml(self):
161 # A ValueError is raised if the bundle contents are not a valid YAML.169 # A ValueError is raised if the bundle contents are not a valid YAML.
162 with self.assertRaises(ValueError) as context_manager:170 with self.assertRaises(ValueError) as context_manager:
@@ -230,15 +238,20 @@
230 contents = yaml.safe_dump({238 contents = yaml.safe_dump({
231 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}},239 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}},
232 })240 })
233 name, services = utils.parse_bundle(contents)241 self.assert_bundle('mybundle', ['mysql', 'wordpress'], contents)
234 self.assertEqual('mybundle', name)
235 self.assertEqual(['mysql', 'wordpress'], sorted(services))
236242
237 def test_success_multiple_bundles(self):243 def test_success_multiple_bundles(self):
238 # The function succeeds with multiple bundles.244 # The function succeeds with multiple bundles.
239 name, services = utils.parse_bundle(self.valid_bundle, 'bundle2')245 self.assert_bundle(
240 self.assertEqual('bundle2', name)246 'bundle2', ['django', 'nodejs'], self.valid_bundle, 'bundle2')
241 self.assertEqual(['django', 'nodejs'], sorted(services))247
248 def test_success_json(self):
249 # Since JSON is a subset of YAML, the function also support JSON
250 # encoded bundles.
251 contents = json.dumps({
252 'mybundle': {'services': {'wordpress': {}, 'mysql': {}}},
253 })
254 self.assert_bundle('mybundle', ['mysql', 'wordpress'], contents)
242255
243256
244class TestParseEnvFile(257class TestParseEnvFile(
245258
=== modified file 'quickstart/utils.py'
--- quickstart/utils.py 2013-10-31 08:33:49 +0000
+++ quickstart/utils.py 2013-11-06 17:54:24 +0000
@@ -101,7 +101,10 @@
101101
102102
103def parse_bundle(bundle_yaml, bundle_name=None):103def parse_bundle(bundle_yaml, bundle_name=None):
104 """Parse the provided bundle YAML decoded contents.104 """Parse the provided bundle YAML encoded contents.
105
106 Since a valid JSON is a subset of YAML this function can be used also to
107 parse JSON encoded contents.
105108
106 Return a tuple containing the bundle name and the list of services included109 Return a tuple containing the bundle name and the list of services included
107 in the bundle.110 in the bundle.
108111
=== modified file 'setup.py'
--- setup.py 2013-10-30 10:16:36 +0000
+++ setup.py 2013-11-06 17:54:24 +0000
@@ -57,7 +57,7 @@
57 long_description=open(description_path).read(),57 long_description=open(description_path).read(),
58 author='The Juju GUI team',58 author='The Juju GUI team',
59 author_email='juju-gui@lists.ubuntu.com',59 author_email='juju-gui@lists.ubuntu.com',
60 url='https://launchpad.net/juju-gui/juju-quickstart',60 url='https://launchpad.net/juju-quickstart',
61 keywords='juju quickstart plugin',61 keywords='juju quickstart plugin',
62 packages=find_packages(),62 packages=find_packages(),
63 package_data={PROJECT_NAME: data_files},63 package_data={PROJECT_NAME: data_files},

Subscribers

People subscribed via source and target branches