Merge lp:~frankban/juju-quickstart/handle-gui-ports into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 134
Proposed branch: lp:~frankban/juju-quickstart/handle-gui-ports
Merge into: lp:juju-quickstart
Diff against target: 1265 lines (+640/-133)
13 files modified
quickstart/__init__.py (+1/-1)
quickstart/app.py (+34/-3)
quickstart/jujugui.py (+84/-0)
quickstart/jujutools.py (+10/-42)
quickstart/manage.py (+75/-18)
quickstart/platform_support.py (+0/-1)
quickstart/tests/test_app.py (+93/-13)
quickstart/tests/test_juju.py (+11/-0)
quickstart/tests/test_jujugui.py (+123/-0)
quickstart/tests/test_jujutools.py (+47/-48)
quickstart/tests/test_manage.py (+125/-7)
quickstart/tests/test_utils.py (+23/-0)
quickstart/utils.py (+14/-0)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/handle-gui-ports
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+260032@code.launchpad.net

Description of the change

Handle Juju GUI server port.

The Juju GUI charm can be configured to listen
to customized ports (juju set juju-gui port=n).
This branch adds to quickstart the ability to
detect the port used by the GUI server, and
therefore connect to the right WebSocket and
Web URLs. Additionally handle the cases in which
the GUI is run in non secure mode.

Also add an option to deploy the GUI on a
customized port directly using quickstart.

Also add an hidden options (--gui-source)
intended for development use only. This allows
for building a specific GUI github branch
(e.g. frankban/my-feature-branch or juju/develop)
when installing the Juju GUI, and can be useful
while coding/QAing GUI branches.

Tests: `make check`.

QA:
- use the GUI as usual, to check for regressions:
  `devenv/bin/juju-quickstart`;
- change the juju-gui port and secure mode:
  `juju set juju-gui port=4242 secure=false`
- wait for the changes to take effect;
- use quickstart again to check that insecure mode
  and customized ports are detected:
  `devenv/bin/juju-quickstart`;
- destroy the environment;
- use quickstart again to server the GUI develop
  branch on a customized port:
  `devenv/bin/juju-quickstart --gui-port 8000 --gui-source juju/develop`;
- check that everything works correctly;
- destroy the environment;
- done, thank you!

https://codereview.appspot.com/238540043/

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

Reviewers: mp+260032_code.launchpad.net,

Message:
Please take a look.

Description:
Handle Juju GUI server port.

The Juju GUI charm can be configured to listen
to customized ports (juju set juju-gui port=n).
This branch adds to quickstart the ability to
detect the port used by the GUI server, and
therefore connect to the right WebSocket and
Web URLs. Additionally handle the cases in which
the GUI is run in non secure mode.

Also add an option to deploy the GUI on a
customized port directly using quickstart.

Also add an hidden options (--gui-source)
intended for development use only. This allows
for building a specific GUI github branch
(e.g. frankban/my-feature-branch or juju/develop)
when installing the Juju GUI, and can be useful
while coding/QAing GUI branches.

Tests: `make check`.

QA:
- use the GUI as usual, to check for regressions:
   `devenv/bin/juju-quickstart`;
- change the juju-gui port and secure mode:
   `juju set juju-gui port=4242 secure=false`
- wait for the changes to take effect;
- use quickstart again to check that insecure mode
   and customized ports are detected:
   `devenv/bin/juju-quickstart`;
- destroy the environment;
- use quickstart again to server the GUI develop
   branch on a customized port:
   `devenv/bin/juju-quickstart --gui-port 8000 --gui-source
juju/develop`;
- check that everything works correctly;
- destroy the environment;
- done, thank you!

https://code.launchpad.net/~frankban/juju-quickstart/handle-gui-ports/+merge/260032

(do not edit description out of merge proposal)

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

Affected files (+642, -133 lines):
   A [revision details]
   M quickstart/__init__.py
   M quickstart/app.py
   A quickstart/jujugui.py
   M quickstart/jujutools.py
   M quickstart/manage.py
   M quickstart/platform_support.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_juju.py
   A quickstart/tests/test_jujugui.py
   M quickstart/tests/test_jujutools.py
   M quickstart/tests/test_manage.py
   M quickstart/tests/test_utils.py
   M quickstart/utils.py

141. By Francesco Banconi

Fix typo.

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

Please take a look.

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

https://codereview.appspot.com/238540043/diff/1/quickstart/jujugui.py#newcode59
quickstart/jujugui.py:59: def parse_charm_url(charm_url):
This function was just moved.

https://codereview.appspot.com/238540043/

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

LGTM QA okay! Thanks for the work on this

https://codereview.appspot.com/238540043/

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

*** Submitted:

Handle Juju GUI server port.

The Juju GUI charm can be configured to listen
to customized ports (juju set juju-gui port=n).
This branch adds to quickstart the ability to
detect the port used by the GUI server, and
therefore connect to the right WebSocket and
Web URLs. Additionally handle the cases in which
the GUI is run in non secure mode.

Also add an option to deploy the GUI on a
customized port directly using quickstart.

Also add an hidden options (--gui-source)
intended for development use only. This allows
for building a specific GUI github branch
(e.g. frankban/my-feature-branch or juju/develop)
when installing the Juju GUI, and can be useful
while coding/QAing GUI branches.

Tests: `make check`.

QA:
- use the GUI as usual, to check for regressions:
   `devenv/bin/juju-quickstart`;
- change the juju-gui port and secure mode:
   `juju set juju-gui port=4242 secure=false`
- wait for the changes to take effect;
- use quickstart again to check that insecure mode
   and customized ports are detected:
   `devenv/bin/juju-quickstart`;
- destroy the environment;
- use quickstart again to server the GUI develop
   branch on a customized port:
   `devenv/bin/juju-quickstart --gui-port 8000 --gui-source
juju/develop`;
- check that everything works correctly;
- destroy the environment;
- done, thank you!

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

https://codereview.appspot.com/238540043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/__init__.py'
2--- quickstart/__init__.py 2015-05-13 08:27:30 +0000
3+++ quickstart/__init__.py 2015-05-25 08:11:40 +0000
4@@ -45,7 +45,7 @@
5 Once Juju has been installed, the command can also be run as a juju plugin,
6 without the hyphen ("juju quickstart").
7 """
8-VERSION = (2, 1, 1)
9+VERSION = (2, 1, 2)
10
11
12 def get_version():
13
14=== modified file 'quickstart/app.py'
15--- quickstart/app.py 2015-04-21 10:22:46 +0000
16+++ quickstart/app.py 2015-05-25 08:11:40 +0000
17@@ -31,6 +31,7 @@
18 from quickstart import (
19 charmstore,
20 juju,
21+ jujugui,
22 jujutools,
23 netutils,
24 platform_support,
25@@ -452,7 +453,7 @@
26 # A deployed service already exists in the environment: ignore the
27 # provided charm URL and just use the already deployed charm.
28 charm_url = service_data['CharmURL']
29- charm_ref = jujutools.parse_gui_charm_url(charm_url)
30+ charm_ref = jujugui.parse_charm_url(charm_url)
31 # Deploy on the bootstrap node if the following conditions are satisfied:
32 # - we are not using the local provider (which uses localhost);
33 # - we are not using the azure provider (in which availability sets prevent
34@@ -466,7 +467,9 @@
35 return charm_ref, machine, service_data, unit_data
36
37
38-def deploy_gui(env, service_name, charm_url, machine, service_data, unit_data):
39+def deploy_gui(
40+ env, service_name, charm_url, machine, options,
41+ service_data, unit_data):
42 """Deploy and expose the given service to the given machine.
43
44 Only deploy the service if not already present in the environment.
45@@ -479,6 +482,9 @@
46 service;
47 - machine: the machine where to deploy to (e.g. "0") or None if a new
48 machine must be created;
49+ - options: GUI charm options used when the Juju GUI service is not
50+ present in the environment, or None if no customized options are
51+ required;
52 - service_data: the service info as returned by the mega-watcher for
53 services, or None if the service is not present in the environment;
54 - unit_data: the unit info as returned by the mega-watcher for units,
55@@ -487,11 +493,18 @@
56 Return the name of the first running unit belonging to the given service.
57 Raise a ProgramExit if the API server returns an error response.
58 """
59+ option_str = None
60+ if options:
61+ option_str = '\n'.join(
62+ ' {}="{}"'.format(k, v) for k, v in sorted(options.items()))
63 if service_data is None:
64 # The service is not in the environment: deploy it without units.
65 print('requesting {} deployment'.format(service_name))
66+ if option_str:
67+ print('using customized {} options:\n{}'.format(
68+ service_name, option_str))
69 try:
70- env.deploy(service_name, charm_url, num_units=0)
71+ env.deploy(service_name, charm_url, num_units=0, config=options)
72 except jujuclient.EnvError as err:
73 raise ProgramExit('bad API response: {}'.format(err.message))
74 print('{} deployment request accepted'.format(service_name))
75@@ -500,6 +513,9 @@
76 # We already have the service in the environment.
77 print('service {} already deployed'.format(service_name))
78 service_exposed = service_data.get('Exposed', False)
79+ if option_str:
80+ logging.warn('ignoring customized {} options:\n{}'.format(
81+ service_name, option_str))
82 # At this point the service is surely deployed in the environment: expose
83 # it if necessary and add a unit if it is missing.
84 if not service_exposed:
85@@ -590,6 +606,21 @@
86 return address
87
88
89+def get_service_config(env, service_name):
90+ """Return the configuration options for the given service name.
91+
92+ The options are returned as a dict mapping option names to values.
93+ If an option is unset, its value is None.
94+
95+ Raise a ProgramExit if the API server returns an error response.
96+ """
97+ try:
98+ config = env.get_config(service_name)
99+ except jujuclient.EnvError as err:
100+ raise ProgramExit('bad API server response: {}'.format(err.message))
101+ return dict((k, v.get('value')) for k, v in config.items())
102+
103+
104 def deploy_bundle(env, bundle):
105 """Deploy the given bundle connecting to the given environment.
106
107
108=== added file 'quickstart/jujugui.py'
109--- quickstart/jujugui.py 1970-01-01 00:00:00 +0000
110+++ quickstart/jujugui.py 2015-05-25 08:11:40 +0000
111@@ -0,0 +1,84 @@
112+# This file is part of the Juju Quickstart Plugin, which lets users set up a
113+# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
114+# Copyright (C) 2015 Canonical Ltd.
115+#
116+# This program is free software: you can redistribute it and/or modify it under
117+# the terms of the GNU Affero General Public License version 3, as published by
118+# the Free Software Foundation.
119+#
120+# This program is distributed in the hope that it will be useful, but WITHOUT
121+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
122+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
123+# Affero General Public License for more details.
124+#
125+# You should have received a copy of the GNU Affero General Public License
126+# along with this program. If not, see <http://www.gnu.org/licenses/>.
127+
128+"""Quickstart helpers for working with the Juju GUI charm and service."""
129+
130+from __future__ import (
131+ print_function,
132+ unicode_literals,
133+)
134+
135+import logging
136+
137+from jujubundlelib import references
138+
139+from quickstart import settings
140+
141+
142+def build_options(port, source):
143+ """Create a configuration dict suitable to be used when deploying the GUI.
144+
145+ Return None if no customized options are present.
146+ """
147+ options = {}
148+ if port is not None:
149+ options['port'] = port
150+ if source is not None:
151+ user, branch = source
152+ value = 'https://github.com/{}/juju-gui.git {}'.format(user, branch)
153+ options['juju-gui-source'] = value
154+ return options or None
155+
156+
157+def normalize_config(options):
158+ """Normalize the Juju GUI configuration options.
159+
160+ For instance, set the default port if none is set in the given options.
161+ """
162+ options = options.copy()
163+ if not options['secure']:
164+ logging.warn('the Juju GUI is running in insecure mode')
165+ if options['port'] is None:
166+ options['port'] = 443
167+ return options
168+
169+
170+def parse_charm_url(charm_url):
171+ """Parse the given charm URL.
172+
173+ Check if the charm URL seems to refer to a Juju GUI charm.
174+ Print (to stdout or to logs) info and warnings about the charm URL.
175+
176+ Return the parsed charm reference object as an instance of
177+ "jujubundlelib.references.Reference".
178+ """
179+ print('charm URL: {}'.format(charm_url))
180+ ref = references.Reference.from_fully_qualified_url(charm_url)
181+ charm_name = settings.JUJU_GUI_CHARM_NAME
182+ if ref.name != charm_name:
183+ # This does not seem to be a Juju GUI charm.
184+ logging.warn(
185+ 'unexpected URL for the {} charm: '
186+ 'the service may not work as expected'.format(charm_name))
187+ return ref
188+ if ref.user or ref.is_local():
189+ # This is not the official Juju GUI charm.
190+ logging.warn('using a customized {} charm'.format(charm_name))
191+ elif ref.revision < settings.MINIMUM_REVISIONS_FOR_BUNDLES[ref.series]:
192+ # This is the official Juju GUI charm, but it is outdated.
193+ logging.warn(
194+ 'charm is outdated and may not support bundle deployments')
195+ return ref
196
197=== modified file 'quickstart/jujutools.py'
198--- quickstart/jujutools.py 2015-04-21 10:22:46 +0000
199+++ quickstart/jujutools.py 2015-05-25 08:11:40 +0000
200@@ -16,14 +16,7 @@
201
202 """Quickstart utility functions for managing Juju environments and entities."""
203
204-from __future__ import (
205- print_function,
206- unicode_literals,
207-)
208-
209-import logging
210-
211-from jujubundlelib import references
212+from __future__ import unicode_literals
213
214 from quickstart import (
215 serializers,
216@@ -32,13 +25,15 @@
217
218
219 def get_api_url(
220- api_address, juju_version, env_uuid, prefix='', charm_ref=None):
221+ api_address, juju_version, env_uuid,
222+ path_prefix='', charm_ref=None, insecure=False):
223 """Return the Juju WebSocket API endpoint.
224
225 Receives the Juju API server address, the Juju version and the unique
226 identifier of the current environment.
227
228- Optionally receive a prefix to be used in the path.
229+ Optionally a prefix to be used in the path and a flag indicating whether
230+ to use secure or insecure WebSockets.
231
232 Optionally also receive the Juju GUI charm reference as an instance of
233 "jujubundlelib.references.Reference". If provided, the function checks that
234@@ -48,10 +43,11 @@
235 The environment UUID can be None, in which case the old-style API URL
236 (not including the environment UUID) is returned.
237 """
238- base_url = 'wss://{}'.format(api_address)
239- prefix = prefix.strip('/')
240- if prefix:
241- base_url = '{}/{}'.format(base_url, prefix)
242+ schema = 'ws' if insecure else 'wss'
243+ base_url = '{}://{}'.format(schema, api_address)
244+ path_prefix = path_prefix.strip('/')
245+ if path_prefix:
246+ base_url = '{}/{}'.format(base_url, path_prefix)
247 if (env_uuid is None) or (juju_version < (1, 22, 0)):
248 return base_url
249 complete_url = '{}/environment/{}/api'.format(base_url, env_uuid)
250@@ -98,34 +94,6 @@
251 return services[0], units[0] if units else None
252
253
254-def parse_gui_charm_url(charm_url):
255- """Parse the given charm URL.
256-
257- Check if the charm URL seems to refer to a Juju GUI charm.
258- Print (to stdout or to logs) info and warnings about the charm URL.
259-
260- Return the parsed charm reference object as an instance of
261- "jujubundlelib.references.Reference".
262- """
263- print('charm URL: {}'.format(charm_url))
264- ref = references.Reference.from_fully_qualified_url(charm_url)
265- charm_name = settings.JUJU_GUI_CHARM_NAME
266- if ref.name != charm_name:
267- # This does not seem to be a Juju GUI charm.
268- logging.warn(
269- 'unexpected URL for the {} charm: '
270- 'the service may not work as expected'.format(charm_name))
271- return ref
272- if ref.user or ref.is_local():
273- # This is not the official Juju GUI charm.
274- logging.warn('using a customized {} charm'.format(charm_name))
275- elif ref.revision < settings.MINIMUM_REVISIONS_FOR_BUNDLES[ref.series]:
276- # This is the official Juju GUI charm, but it is outdated.
277- logging.warn(
278- 'charm is outdated and may not support bundle deployments')
279- return ref
280-
281-
282 def parse_status_output(output, keys=None):
283 """Parse the output of juju status.
284
285
286=== modified file 'quickstart/manage.py'
287--- quickstart/manage.py 2015-04-24 13:31:28 +0000
288+++ quickstart/manage.py 2015-05-25 08:11:40 +0000
289@@ -33,6 +33,7 @@
290 import quickstart
291 from quickstart import (
292 app,
293+ jujugui,
294 jujutools,
295 packaging,
296 platform_support,
297@@ -142,6 +143,46 @@
298 'revision: {}'.format(ref))
299
300
301+def _validate_gui_source(options, parser):
302+ """Validate the optional Juju GUI branch source.
303+
304+ Convert the GUI source value to be a tuple (user, branch).
305+ Exit with an error if the source is not well formed.
306+ """
307+ gui_source = options.gui_source
308+ if gui_source is None:
309+ return
310+ error = 'invalid Juju GUI source: {}'.format(gui_source).encode('utf-8')
311+ try:
312+ user, branch = [i.strip() for i in gui_source.split('/')]
313+ except ValueError:
314+ return parser.error(error)
315+ if not (user and branch):
316+ return parser.error(error)
317+ options.gui_source = (user, branch)
318+
319+
320+def _validate_platform(platform, parser):
321+ """Validate the platform.
322+
323+ Exit with an error if platform is not supported by quickstart or is
324+ missing files.
325+ """
326+ try:
327+ platform_support.validate_platform(platform)
328+ except ValueError as err:
329+ return parser.error(bytes(err))
330+
331+
332+def _validate_port(port, parser):
333+ """Validate the given port if it is not None.
334+
335+ Exit with an error if the port is not in the TCP range.
336+ """
337+ if (port is not None) and (port < 1 or port > 65535):
338+ return parser.error(b'invalid Juju GUI port: not in range 1-65535')
339+
340+
341 def _retrieve_env_db(parser, env_file=None):
342 """Retrieve the environment database (or create an in-memory empty one)."""
343 if env_file is None:
344@@ -313,17 +354,6 @@
345 setattr(options, key, value.decode(encoding))
346
347
348-def _validate_platform(parser, platform):
349- """Validate the platform.
350- Exit with an error if platform is not supported by quickstart or is
351- missing files.
352- """
353- try:
354- platform_support.validate_platform(platform)
355- except ValueError as err:
356- return parser.error(bytes(err))
357-
358-
359 def setup():
360 """Set up the application options and logger.
361
362@@ -339,9 +369,11 @@
363 - env_file: the absolute path of the Juju environments.yaml file;
364 - env_name: the name of the Juju environment to use;
365 - env_type: the provider type of the selected Juju environment;
366+ - gui_source: the optional Juju GUI branch source on github, or None;
367 - interactive: whether to start the interactive session;
368 - open_browser: whether the GUI browser must be opened;
369 - platform: The host platform;
370+ - port: the optional Juju GUI port, or None;
371 - upload_tools: whether to upload local version of tools;
372 - upload_series: the comma-separated series list for which tools will
373 be uploaded, or None if not set.
374@@ -423,9 +455,16 @@
375 'e.g. "cs:~juju-gui/precise/juju-gui-162". This option is\n'
376 'ignored if the GUI is already present in the environment')
377 parser.add_argument(
378+ '--gui-port', dest='port', type=int,
379+ help='The Juju GUI service port. By default, the GUI listens on\n'
380+ 'ports 80 and 443, and insecure connections to port 80 are\n'
381+ 'redirected to port 443. If a port is specified then the\n'
382+ 'GUI service will only accept secure connections to that\n'
383+ 'port. This option is ignored if the GUI is already\n'
384+ 'present in the environment')
385+ parser.add_argument(
386 '--no-browser', action='store_false', dest='open_browser',
387 help='Avoid opening the browser to the GUI at the end of the\nprocess')
388-
389 parser.add_argument(
390 '--distro-only', action='store_true', dest='distro_only',
391 default=default_distro_only, help=distro_only_help)
392@@ -457,6 +496,15 @@
393 'They will also be set as default constraints on the\n'
394 'environment for all future machines, exactly as if the\n'
395 'constraints were set with "juju set-constraints".\n')
396+ # Hidden options.
397+ parser.add_argument(
398+ # The Juju GUI source branch (in github) to use when building the GUI.
399+ # It has the form "<user>/<branch>", e.g. "juju/develop".
400+ # It defaults to the local release included in the Juju GUI charm.
401+ # This option is present for development purposes only, and it is
402+ # ignored if the GUI is already present in the environment.
403+ '--gui-source', dest='gui_source', help=argparse.SUPPRESS)
404+
405 # Parse the provided arguments.
406 options = parser.parse_args()
407
408@@ -464,12 +512,14 @@
409 _configure_logging(logging.DEBUG if options.debug else logging.INFO)
410
411 # Validate and add in the platform for convenience.
412- _validate_platform(parser, platform)
413+ _validate_platform(platform, parser)
414 options.platform = platform
415
416 # Convert the provided string arguments to unicode.
417 _convert_options_to_unicode(options)
418 # Validate and process the provided arguments.
419+ _validate_port(options.port, parser)
420+ _validate_gui_source(options, parser)
421 _setup_env(options, parser)
422 if options.bundle_source is not None:
423 _validate_bundle(options, parser)
424@@ -553,24 +603,31 @@
425 charm_ref, machine, service_data, unit_data = app.check_environment(
426 env, settings.JUJU_GUI_SERVICE_NAME, options.charm_url,
427 env_type, bootstrap_node_series, already_bootstrapped)
428+ gui_options = jujugui.build_options(options.port, options.gui_source)
429 unit_name = app.deploy_gui(
430 env, settings.JUJU_GUI_SERVICE_NAME, charm_ref.id(), machine,
431- service_data, unit_data)
432+ gui_options, service_data, unit_data)
433
434- # Observe the deployment progress.
435+ # Observe the deployment progress, and retrieve information for connecting
436+ # to the Juju GUI.
437 address = app.watch(env, unit_name)
438+ print('the Juju GUI is ready: retrieving service configuration')
439+ gui_config = jujugui.normalize_config(
440+ app.get_service_config(env, settings.JUJU_GUI_SERVICE_NAME))
441 env.close()
442
443 # Print out Juju GUI unit and credential information.
444- url = 'https://{}'.format(address)
445+ url = utils.build_web_url(
446+ address, gui_config['port'], gui_config['secure'])
447 print('\nJuju GUI URL: {}\nusername: {}\npassword: {}\n'.format(
448 url, username, password))
449
450 # Connect to the GUI server WebSocket API.
451 print('connecting to the Juju GUI server')
452 gui_api_url = jujutools.get_api_url(
453- address + ':443', juju_version, env_uuid,
454- prefix='ws', charm_ref=charm_ref)
455+ '{}:{}'.format(address, gui_config['port']), juju_version, env_uuid,
456+ path_prefix='ws', charm_ref=charm_ref,
457+ insecure=not gui_config['secure'])
458 gui_env = app.connect(gui_api_url, username, password)
459
460 # Handle bundle deployment.
461
462=== modified file 'quickstart/platform_support.py'
463--- quickstart/platform_support.py 2015-03-10 10:46:46 +0000
464+++ quickstart/platform_support.py 2015-05-25 08:11:40 +0000
465@@ -44,7 +44,6 @@
466 settings.WINDOWS: 'juju-quickstart on Windows is not yet supported',
467 settings.UNKNOWN_PLATFORM: 'unable to determine the OS platform',
468 }
469-
470 error_msg = unsupported_messages.get(pf)
471 if error_msg is not None:
472 raise ValueError(error_msg.encode('utf-8'))
473
474=== modified file 'quickstart/tests/test_app.py'
475--- quickstart/tests/test_app.py 2015-04-21 10:22:46 +0000
476+++ quickstart/tests/test_app.py 2015-05-25 08:11:40 +0000
477@@ -1226,6 +1226,7 @@
478 unittest.TestCase):
479
480 charm_url = 'cs:trusty/juju-gui-42'
481+ options = None
482
483 def make_env(self, unit_name=None):
484 """Create and return a mock environment object.
485@@ -1245,11 +1246,13 @@
486 env = self.make_env(unit_name='my-gui/42')
487 service_data = unit_data = None
488 unit_name = app.deploy_gui(
489- env, 'my-gui', self.charm_url, '0', service_data, unit_data)
490+ env, 'my-gui', self.charm_url, '0', self.options,
491+ service_data, unit_data)
492 self.assertEqual('my-gui/42', unit_name)
493 env.assert_has_calls([
494 # The service has been deployed.
495- mock.call.deploy('my-gui', self.charm_url, num_units=0),
496+ mock.call.deploy(
497+ 'my-gui', self.charm_url, num_units=0, config=None),
498 # The service has been exposed.
499 mock.call.expose('my-gui'),
500 # One service unit has been added.
501@@ -1264,13 +1267,37 @@
502 mock.call('my-gui/42 deployment request accepted'),
503 ])
504
505+ def test_deployment_with_options(self, mock_print):
506+ # The function correctly deploys the service with the given
507+ # configuration options in the case the service is not present in the
508+ # environment.
509+ env = self.make_env(unit_name='my-gui/42')
510+ service_data = unit_data = None
511+ options = {'port': 4242, 'sandbox': True}
512+ app.deploy_gui(
513+ env, 'my-gui', self.charm_url, '0', options,
514+ service_data, unit_data)
515+ env.deploy.assert_called_once_with(
516+ 'my-gui', self.charm_url, num_units=0, config=options)
517+ self.assertEqual(6, mock_print.call_count)
518+ mock_print.assert_has_calls([
519+ mock.call('requesting my-gui deployment'),
520+ mock.call('using customized my-gui options:\n'
521+ ' port="4242"\n sandbox="True"'),
522+ mock.call('my-gui deployment request accepted'),
523+ mock.call('exposing service my-gui'),
524+ mock.call('requesting new unit deployment'),
525+ mock.call('my-gui/42 deployment request accepted'),
526+ ])
527+
528 def test_existing_service(self, mock_print):
529 # The deployment is executed reusing an already deployed service.
530 env = self.make_env(unit_name='my-gui/42')
531 service_data = self.make_service_data()
532 unit_data = None
533 unit_name = app.deploy_gui(
534- env, 'my-gui', self.charm_url, '0', service_data, unit_data)
535+ env, 'my-gui', self.charm_url, '0', self.options,
536+ service_data, unit_data)
537 self.assertEqual('my-gui/42', unit_name)
538 # One service unit has been added.
539 env.add_unit.assert_called_once_with('my-gui', machine_spec='0')
540@@ -1285,13 +1312,27 @@
541 mock.call('my-gui/42 deployment request accepted'),
542 ])
543
544+ def test_existing_service_with_options(self, mock_print):
545+ # The deployment is executed reusing an already deployed service.
546+ # In this case, the provided Juju GUI options are ignored.
547+ env = self.make_env(unit_name='my-gui/42')
548+ service_data = self.make_service_data()
549+ unit_data = None
550+ options = {'port': 4242, 'sandbox': True}
551+ app.deploy_gui(
552+ env, 'my-gui', self.charm_url, '0', options,
553+ service_data, unit_data)
554+ # The service is not re-deployed.
555+ self.assertFalse(env.deploy.called)
556+
557 def test_existing_service_unexposed(self, mock_print):
558 # The existing service is exposed if required.
559 env = self.make_env(unit_name='my-gui/42')
560 service_data = self.make_service_data({'Exposed': False})
561 unit_data = None
562 unit_name = app.deploy_gui(
563- env, 'my-gui', self.charm_url, '1', service_data, unit_data)
564+ env, 'my-gui', self.charm_url, '1', self.options,
565+ service_data, unit_data)
566 self.assertEqual('my-gui/42', unit_name)
567 env.assert_has_calls([
568 # The service has been exposed.
569@@ -1315,7 +1356,8 @@
570 service_data = self.make_service_data()
571 unit_data = self.make_unit_data()
572 unit_name = app.deploy_gui(
573- env, 'my-gui', self.charm_url, '0', service_data, unit_data)
574+ env, 'my-gui', self.charm_url, '0', self.options,
575+ service_data, unit_data)
576 self.assertEqual('my-gui/47', unit_name)
577 # The service is not re-deployed.
578 self.assertFalse(env.deploy.called)
579@@ -1334,11 +1376,13 @@
580 env = self.make_env(unit_name='my-gui/42')
581 service_data = unit_data = None
582 unit_name = app.deploy_gui(
583- env, 'my-gui', self.charm_url, None, service_data, unit_data)
584+ env, 'my-gui', self.charm_url, None, self.options,
585+ service_data, unit_data)
586 self.assertEqual('my-gui/42', unit_name)
587 env.assert_has_calls([
588 # The service has been deployed.
589- mock.call.deploy('my-gui', self.charm_url, num_units=0),
590+ mock.call.deploy(
591+ 'my-gui', self.charm_url, num_units=0, config=None),
592 # The service has been exposed.
593 mock.call.expose('my-gui'),
594 # One service unit has been added to a new machine.
595@@ -1352,10 +1396,10 @@
596 service_data = unit_data = None
597 with self.assert_program_exit('bad API response: bad wolf'):
598 app.deploy_gui(
599- env, 'another-gui', self.charm_url, '0',
600+ env, 'another-gui', self.charm_url, '0', self.options,
601 service_data, unit_data)
602 env.deploy.assert_called_once_with(
603- 'another-gui', self.charm_url, num_units=0)
604+ 'another-gui', self.charm_url, num_units=0, config=None)
605
606 def test_expose_error(self, mock_print):
607 # A ProgramExit is raised if an error occurs in the expose API call.
608@@ -1364,7 +1408,7 @@
609 service_data = unit_data = None
610 with self.assert_program_exit('bad API response: bad wolf'):
611 app.deploy_gui(
612- env, 'another-gui', self.charm_url, '0',
613+ env, 'another-gui', self.charm_url, '0', self.options,
614 service_data, unit_data)
615 env.expose.assert_called_once_with('another-gui')
616
617@@ -1375,7 +1419,7 @@
618 service_data = unit_data = None
619 with self.assert_program_exit('bad API response: bad wolf'):
620 app.deploy_gui(
621- env, 'another-gui', self.charm_url, '0',
622+ env, 'another-gui', self.charm_url, '0', self.options,
623 service_data, unit_data)
624 env.add_unit.assert_called_once_with('another-gui', machine_spec='0')
625
626@@ -1387,10 +1431,10 @@
627 service_data = unit_data = None
628 with self.assertRaises(ValueError) as context_manager:
629 app.deploy_gui(
630- env, 'juju-gui', self.charm_url, '0',
631+ env, 'juju-gui', self.charm_url, '0', self.options,
632 service_data, unit_data)
633 env.deploy.assert_called_once_with(
634- 'juju-gui', self.charm_url, num_units=0)
635+ 'juju-gui', self.charm_url, num_units=0, config=None)
636 env.expose.assert_called_once_with('juju-gui')
637 self.assertIs(error, context_manager.exception)
638
639@@ -1667,6 +1711,42 @@
640 self.assertFalse(mock_print.called)
641
642
643+class TestGetServiceConfig(ProgramExitTestsMixin, unittest.TestCase):
644+
645+ def test_success(self):
646+ # The configuration options for the service are correctly returned.
647+ env = mock.Mock()
648+ env.get_config.return_value = {
649+ 'port': {
650+ 'description': 'Supply a different port',
651+ 'type': 'int',
652+ 'value': 8080,
653+ },
654+ 'login-help': {
655+ 'default': True,
656+ 'description': 'The help text shown to the user',
657+ 'type': u'string',
658+ },
659+ 'secure': {
660+ 'description': 'Security mode',
661+ 'type': 'boolean',
662+ 'value': False,
663+ },
664+ }
665+ expected_config = {'port': 8080, 'login-help': None, 'secure': False}
666+ config = app.get_service_config(env, 'django')
667+ self.assertEqual(expected_config, config)
668+ env.get_config.assert_called_once_with('django')
669+
670+ def test_api_error(self):
671+ # A ProgramExit is raised if an error occurs while interacting with the
672+ # Juju API.
673+ env = mock.Mock()
674+ env.get_config.side_effect = self.make_env_error('bad wolf')
675+ with self.assert_program_exit('bad API server response: bad wolf'):
676+ app.get_service_config(env, 'my-service')
677+
678+
679 class TestDeployBundle(ProgramExitTestsMixin, unittest.TestCase):
680
681 bundle_data = {'services': {}}
682
683=== modified file 'quickstart/tests/test_juju.py'
684--- quickstart/tests/test_juju.py 2015-02-26 19:46:48 +0000
685+++ quickstart/tests/test_juju.py 2015-05-25 08:11:40 +0000
686@@ -222,6 +222,17 @@
687 mock_rpc.assert_called_once_with(expected)
688
689 @patch_rpc
690+ def test_get_config(self, mock_rpc):
691+ # The API call to retrieve service configuration is properly generated.
692+ self.env.get_config(self.service_name)
693+ expected = {
694+ 'Type': 'Client',
695+ 'Request': 'ServiceGet',
696+ 'Params': {'ServiceName': self.service_name},
697+ }
698+ mock_rpc.assert_called_once_with(expected)
699+
700+ @patch_rpc
701 def test_get_watcher(self, mock_rpc):
702 # Environment watching is correctly started.
703 self.env.login('Secret!')
704
705=== added file 'quickstart/tests/test_jujugui.py'
706--- quickstart/tests/test_jujugui.py 1970-01-01 00:00:00 +0000
707+++ quickstart/tests/test_jujugui.py 2015-05-25 08:11:40 +0000
708@@ -0,0 +1,123 @@
709+# This file is part of the Juju Quickstart Plugin, which lets users set up a
710+# Juju environment in very few steps (https://launchpad.net/juju-quickstart).
711+# Copyright (C) 2015 Canonical Ltd.
712+#
713+# This program is free software: you can redistribute it and/or modify it under
714+# the terms of the GNU Affero General Public License version 3, as published by
715+# the Free Software Foundation.
716+#
717+# This program is distributed in the hope that it will be useful, but WITHOUT
718+# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
719+# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
720+# Affero General Public License for more details.
721+#
722+# You should have received a copy of the GNU Affero General Public License
723+# along with this program. If not, see <http://www.gnu.org/licenses/>.
724+
725+"""Tests for the Quickstart Juju GUI charm and service helpers."""
726+
727+from __future__ import unicode_literals
728+
729+import unittest
730+
731+from jujubundlelib import references
732+import mock
733+
734+from quickstart import jujugui
735+from quickstart.tests import helpers
736+
737+
738+class TestBuildOptions(unittest.TestCase):
739+
740+ def test_no_options(self):
741+ # None is returned if there are no customized options.
742+ options = jujugui.build_options(None, None)
743+ self.assertIsNone(options)
744+
745+ def test_port(self):
746+ # The port option is correctly configured.
747+ options = jujugui.build_options(42, None)
748+ self.assertEqual({'port': 42}, options)
749+
750+ def test_gui_source(self):
751+ # The GUI source option is correctly configured.
752+ options = jujugui.build_options(None, ('who', 'develop'))
753+ expected_options = {
754+ 'juju-gui-source': 'https://github.com/who/juju-gui.git develop',
755+ }
756+ self.assertEqual(expected_options, options)
757+
758+ def test_all_options(self):
759+ # Multiple options are properly configured.
760+ options = jujugui.build_options(443, ('juju', 'beta'))
761+ expected_options = {
762+ 'juju-gui-source': 'https://github.com/juju/juju-gui.git beta',
763+ 'port': 443,
764+ }
765+ self.assertEqual(expected_options, options)
766+
767+
768+class TestNormalizeConfig(unittest.TestCase):
769+
770+ def test_normal_options(self):
771+ # The options are not modified if there is no need.
772+ original_options = {'port': 8080, 'secure': True}
773+ options = jujugui.normalize_config(original_options)
774+ self.assertEqual(original_options, options)
775+
776+ def test_port(self):
777+ # A default port is set if it is not present in the input options.
778+ options = jujugui.normalize_config({'port': None, 'secure': True})
779+ self.assertEqual({'port': 443, 'secure': True}, options)
780+
781+ def test_not_modified_in_place(self):
782+ # The input options are not modified in place.
783+ original_options = {'port': None, 'sandbox': False, 'secure': True}
784+ options = jujugui.normalize_config(original_options)
785+ options['sandbox'] = True
786+ self.assertEqual(
787+ {'port': 443, 'sandbox': True, 'secure': True}, options)
788+ self.assertEqual(
789+ {'port': None, 'sandbox': False, 'secure': True}, original_options)
790+
791+ def test_secure_warning(self):
792+ # A warning is logged if the GUI service is running in insecure mode.
793+ expected_log = 'the Juju GUI is running in insecure mode'
794+ with helpers.assert_logs([expected_log], level='warn'):
795+ jujugui.normalize_config({'port': 8080, 'secure': False})
796+
797+
798+@mock.patch('__builtin__.print', mock.Mock())
799+class TestParseGuiCharmUrl(unittest.TestCase):
800+
801+ def test_charm_instance_returned(self):
802+ # A charm reference instance is correctly returned.
803+ ref = jujugui.parse_charm_url('cs:trusty/juju-gui-42')
804+ self.assertIsInstance(ref, references.Reference)
805+ self.assertEqual('cs:trusty/juju-gui-42', ref.id())
806+
807+ def test_customized(self):
808+ # A customized charm reference is properly logged.
809+ expected = 'using a customized juju-gui charm'
810+ with helpers.assert_logs([expected], level='warn'):
811+ jujugui.parse_charm_url('cs:~juju-gui/precise/juju-gui-28')
812+
813+ def test_outdated(self):
814+ # An outdated charm reference is properly logged.
815+ expected = 'charm is outdated and may not support bundle deployments'
816+ with helpers.assert_logs([expected], level='warn'):
817+ jujugui.parse_charm_url('cs:precise/juju-gui-1')
818+
819+ def test_unexpected(self):
820+ # An unexpected charm reference is properly logged.
821+ expected = (
822+ 'unexpected URL for the juju-gui charm: the service may not work '
823+ 'as expected')
824+ with helpers.assert_logs([expected], level='warn'):
825+ jujugui.parse_charm_url('cs:precise/another-gui-42')
826+
827+ def test_official(self):
828+ # No warnings are logged if an up to date charm is passed.
829+ with mock.patch('logging.warn') as mock_warn:
830+ jujugui.parse_charm_url('cs:precise/juju-gui-100')
831+ self.assertFalse(mock_warn.called)
832
833=== modified file 'quickstart/tests/test_jujutools.py'
834--- quickstart/tests/test_jujutools.py 2015-04-21 10:22:46 +0000
835+++ quickstart/tests/test_jujutools.py 2015-05-25 08:11:40 +0000
836@@ -21,7 +21,6 @@
837 import unittest
838
839 from jujubundlelib import references
840-import mock
841 import yaml
842
843 from quickstart import jujutools
844@@ -35,37 +34,57 @@
845 url = jujutools.get_api_url('1.2.3.4:17070', (1, 22, 0), 'env-uuid')
846 self.assertEqual('wss://1.2.3.4:17070/environment/env-uuid/api', url)
847
848- def test_new_url_with_prefix(self):
849+ def test_new_url_with_path_prefix(self):
850 # The new Juju API endpoint is returned with the given path prefix.
851 url = jujutools.get_api_url(
852- '1.2.3.4:17070', (1, 22, 0), 'env-uuid', prefix='/my/path/')
853+ '1.2.3.4:17070', (1, 22, 0), 'env-uuid', path_prefix='/my/path/')
854 self.assertEqual(
855 'wss://1.2.3.4:17070/my/path/environment/env-uuid/api', url)
856
857+ def test_new_url_with_insecure(self):
858+ # The new Juju API endpoint is returned with insecure schema.
859+ url = jujutools.get_api_url(
860+ '1.2.3.4:17070', (1, 22, 0), 'env-uuid', insecure=True)
861+ self.assertEqual('ws://1.2.3.4:17070/environment/env-uuid/api', url)
862+
863 def test_old_juju(self):
864 # The old Juju API endpoint is returned if the Juju in use is not a
865 # recent version.
866 url = jujutools.get_api_url('1.2.3.4:17070', (1, 21, 7), 'env-uuid')
867 self.assertEqual('wss://1.2.3.4:17070', url)
868
869- def test_old_juju_with_prefix(self):
870+ def test_old_juju_with_path_prefix(self):
871 # The old Juju API endpoint is returned with the given path prefix.
872 url = jujutools.get_api_url(
873- '1.2.3.4:8888', (1, 21, 7), 'env-uuid', 'proxy/')
874+ '1.2.3.4:8888', (1, 21, 7), 'env-uuid', path_prefix='proxy/')
875 self.assertEqual('wss://1.2.3.4:8888/proxy', url)
876
877+ def test_old_juju_with_insecure(self):
878+ # The old Juju API endpoint is returned with insecure schema.
879+ url = jujutools.get_api_url(
880+ '1.2.3.4:8888', (1, 21, 7), 'env-uuid', insecure=True)
881+ self.assertEqual('ws://1.2.3.4:8888', url)
882+
883 def test_no_env_uuid(self):
884 # The old Juju API endpoint is returned if the environment unique
885 # identifier is unreachable.
886 url = jujutools.get_api_url('1.2.3.4:17070', (1, 23, 42), None)
887 self.assertEqual('wss://1.2.3.4:17070', url)
888
889- def test_no_env_uuid_with_prefix(self):
890+ def test_no_env_uuid_with_path_prefix(self):
891 # The old Juju API endpoint is returned with the given path prefix.
892+ # In this case the environment unique identifier is unreachable.
893 url = jujutools.get_api_url(
894- '1.2.3.4:17070', (1, 23, 42), None, 'my/prefix')
895+ '1.2.3.4:17070', (1, 23, 42), None, path_prefix='my/prefix')
896 self.assertEqual('wss://1.2.3.4:17070/my/prefix', url)
897
898+ def test_no_env_uuid_with_customized_schema(self):
899+ # The old Juju API endpoint is returned with insecure schema.
900+ # In this case the environment unique identifier is unreachable.
901+ url = jujutools.get_api_url(
902+ '1.2.3.4:17070', (1, 23, 42), None, insecure=True)
903+ self.assertEqual('ws://1.2.3.4:17070', url)
904+
905 def test_new_charm_old_juju(self):
906 # The old Juju API endpoints are used if and old version of Juju is in
907 # use, even if the Juju GUI charm is recent.
908@@ -99,7 +118,8 @@
909 ref = references.Reference.from_fully_qualified_url(
910 'local:precise/juju-gui-0')
911 url = jujutools.get_api_url(
912- 'example.com:17070', (1, 22, 2), 'uuid', prefix='/', charm_ref=ref)
913+ 'example.com:17070', (1, 22, 2), 'uuid', path_prefix='/',
914+ charm_ref=ref)
915 self.assertEqual('wss://example.com:17070/environment/uuid/api', url)
916
917 def test_customized_charm_unexpected_series(self):
918@@ -108,7 +128,8 @@
919 ref = references.Reference.from_fully_qualified_url(
920 'cs:vivid/juju-gui-0')
921 url = jujutools.get_api_url(
922- 'example.com:22', (1, 22, 2), 'uuid', prefix='ws', charm_ref=ref)
923+ 'example.com:22', (1, 22, 2), 'uuid', path_prefix='ws',
924+ charm_ref=ref)
925 self.assertEqual('wss://example.com:22/ws/environment/uuid/api', url)
926
927 def test_recent_precise_charm(self):
928@@ -142,19 +163,33 @@
929 ref = references.Reference.from_fully_qualified_url(
930 'cs:trusty/juju-gui-18')
931 url = jujutools.get_api_url(
932- '1.2.3.4:4747', (1, 42, 0), 'env-uuid', prefix='ws', charm_ref=ref)
933+ '1.2.3.4:4747', (1, 42, 0), 'env-uuid', path_prefix='ws',
934+ charm_ref=ref)
935 self.assertEqual('wss://1.2.3.4:4747/ws', url)
936
937- def test_recent_charm_and_prefix(self):
938+ def test_recent_charm_with_path_prefix(self):
939 # The new API endpoint is returned if a recent charm and a prefix are
940 # both provided. This test exercises the real case in which the GUI
941 # server API endpoint is returned.
942 ref = references.Reference.from_fully_qualified_url(
943 'cs:trusty/juju-gui-42')
944 url = jujutools.get_api_url(
945- '1.2.3.4:17070', (1, 22, 0), 'env-id', prefix='ws', charm_ref=ref)
946+ '1.2.3.4:17070', (1, 22, 0), 'env-id', path_prefix='ws',
947+ charm_ref=ref)
948 self.assertEqual('wss://1.2.3.4:17070/ws/environment/env-id/api', url)
949
950+ def test_recent_charm_with_path_prefix_and_schema(self):
951+ # The new API endpoint is returned if a recent charm, a path prefix and
952+ # insecure schema are provided. this test exercises the real case
953+ # in which the GUI server API endpoint is returned, and the GUI is
954+ # configured in insecure mode.
955+ ref = references.Reference.from_fully_qualified_url(
956+ 'cs:trusty/juju-gui-42')
957+ url = jujutools.get_api_url(
958+ '1.2.3.4:80', (1, 22, 0), 'env-id', insecure=True,
959+ path_prefix='ws', charm_ref=ref)
960+ self.assertEqual('ws://1.2.3.4:80/ws/environment/env-id/api', url)
961+
962
963 class TestGetServiceInfo(helpers.WatcherDataTestsMixin, unittest.TestCase):
964
965@@ -237,42 +272,6 @@
966 self.assertEqual(expected, jujutools.get_service_info([], 'my-gui'))
967
968
969-@mock.patch('__builtin__.print', mock.Mock())
970-class TestParseGuiCharmUrl(unittest.TestCase):
971-
972- def test_charm_instance_returned(self):
973- # A charm reference instance is correctly returned.
974- ref = jujutools.parse_gui_charm_url('cs:trusty/juju-gui-42')
975- self.assertIsInstance(ref, references.Reference)
976- self.assertEqual('cs:trusty/juju-gui-42', ref.id())
977-
978- def test_customized(self):
979- # A customized charm reference is properly logged.
980- expected = 'using a customized juju-gui charm'
981- with helpers.assert_logs([expected], level='warn'):
982- jujutools.parse_gui_charm_url('cs:~juju-gui/precise/juju-gui-28')
983-
984- def test_outdated(self):
985- # An outdated charm reference is properly logged.
986- expected = 'charm is outdated and may not support bundle deployments'
987- with helpers.assert_logs([expected], level='warn'):
988- jujutools.parse_gui_charm_url('cs:precise/juju-gui-1')
989-
990- def test_unexpected(self):
991- # An unexpected charm reference is properly logged.
992- expected = (
993- 'unexpected URL for the juju-gui charm: the service may not work '
994- 'as expected')
995- with helpers.assert_logs([expected], level='warn'):
996- jujutools.parse_gui_charm_url('cs:precise/another-gui-42')
997-
998- def test_official(self):
999- # No warnings are logged if an up to date charm is passed.
1000- with mock.patch('logging.warn') as mock_warn:
1001- jujutools.parse_gui_charm_url('cs:precise/juju-gui-100')
1002- self.assertFalse(mock_warn.called)
1003-
1004-
1005 class TestParseStatusOutput(helpers.ValueErrorTestsMixin, unittest.TestCase):
1006
1007 def test_invalid_yaml(self):
1008
1009=== modified file 'quickstart/tests/test_manage.py'
1010--- quickstart/tests/test_manage.py 2015-04-21 10:22:46 +0000
1011+++ quickstart/tests/test_manage.py 2015-05-25 08:11:40 +0000
1012@@ -334,19 +334,47 @@
1013 'the storage port field requires an integer value')
1014
1015
1016+class TestValidateGuiSource(unittest.TestCase):
1017+
1018+ def setUp(self):
1019+ # Set up a parser.
1020+ self.parser = mock.Mock()
1021+
1022+ def test_valid_source(self):
1023+ # The user and branch are stored if the source is valid.
1024+ options = mock.Mock(gui_source='juju/develop')
1025+ manage._validate_gui_source(options, self.parser)
1026+ self.assertFalse(self.parser.error.called)
1027+ self.assertEqual(('juju', 'develop'), options.gui_source)
1028+
1029+ def test_source_not_set(self):
1030+ # Nothing happens if the source is not set.
1031+ options = mock.Mock(gui_source=None)
1032+ manage._validate_gui_source(options, self.parser)
1033+ self.assertFalse(self.parser.error.called)
1034+ self.assertIsNone(options.gui_source)
1035+
1036+ def test_invalid_source(self):
1037+ # A parser error is invoked if the provided source is not valid.
1038+ for value in ('', 'develop', 'who/ ', '/value/not/valid', '/who'):
1039+ options = mock.Mock(gui_source=value)
1040+ manage._validate_gui_source(options, self.parser)
1041+ self.parser.error.assert_called_once_with(
1042+ 'invalid Juju GUI source: {}'.format(value))
1043+ self.parser.error.reset_mock()
1044+
1045+
1046 class TestValidatePlatform(unittest.TestCase):
1047
1048 def setUp(self):
1049- # Set up a parser, the environments metadata and a testing env_db.
1050+ # Set up a parser.
1051 self.parser = mock.Mock()
1052- self.env_type_db = envs.get_env_type_db()
1053- self.env_db = helpers.make_env_db()
1054
1055 def test_platform_validation_fails(self):
1056 # If the platform validation fails a parser error is given.
1057 path = 'quickstart.manage.platform_support.validate_platform'
1058 with mock.patch(path, side_effect=ValueError('Bad platform, yo')):
1059- manage._validate_platform(self.parser, settings.LINUX_RPM)
1060+ manage._validate_platform(settings.LINUX_RPM, self.parser)
1061 self.parser.error.assert_called_once_with(
1062 'Bad platform, yo')
1063
1064@@ -354,10 +382,33 @@
1065 # If the platform validation passes it returns None.
1066 path = 'quickstart.platform_support.validate_platform'
1067 with mock.patch(path, side_effect=None):
1068- result = manage._validate_platform(self.parser, settings.LINUX_RPM)
1069+ result = manage._validate_platform(settings.LINUX_RPM, self.parser)
1070 self.assertIsNone(result)
1071
1072
1073+class TestValidatePort(unittest.TestCase):
1074+
1075+ def setUp(self):
1076+ # Set up a parser.
1077+ self.parser = mock.Mock()
1078+
1079+ def test_valid_port(self):
1080+ # Nothing happens if the provided port is valid.
1081+ manage._validate_port(80, self.parser)
1082+ self.assertFalse(self.parser.error.called)
1083+
1084+ def test_port_not_set(self):
1085+ # Nothing happens if the port is not set.
1086+ manage._validate_port(None, self.parser)
1087+ self.assertFalse(self.parser.error.called)
1088+
1089+ def test_port_not_in_range(self):
1090+ # A parser error is invoked if the provided port is not valid.
1091+ manage._validate_port(-1, self.parser)
1092+ self.parser.error.assert_called_once_with(
1093+ 'invalid Juju GUI port: not in range 1-65535')
1094+
1095+
1096 class TestSetupEnv(
1097 helpers.EnvFileTestsMixin, helpers.JenvFileTestsMixin,
1098 unittest.TestCase):
1099@@ -645,10 +696,12 @@
1100 'bundle_source': None,
1101 'charm_url': None,
1102 'debug': False,
1103+ 'default_series': None,
1104 'env_name': 'aws',
1105 'env_type': 'ec2',
1106+ 'gui_source': None,
1107 'open_browser': True,
1108- 'default_series': None,
1109+ 'port': None,
1110 }
1111 options.update(kwargs)
1112 return mock.Mock(**options)
1113@@ -696,6 +749,8 @@
1114 'deploy_gui': 'juju-gui/0',
1115 # Watch the deployment progress and return the unit address.
1116 'watch': '1.2.3.5',
1117+ # Retrieve the Juju GUI service configuration.
1118+ 'get_service_config': {'port': None, 'secure': True},
1119 # Create the login token for the Juju GUI.
1120 'create_auth_token': 'TOKEN',
1121 }
1122@@ -751,7 +806,7 @@
1123 options.env_type, 'trusty', False)
1124 mock_app.deploy_gui.assert_called_once_with(
1125 env, settings.JUJU_GUI_SERVICE_NAME, 'cs:trusty/juju-gui-42',
1126- '0', {'Name': 'juju-gui'}, {'Name': 'juju-gui/0'})
1127+ '0', None, {'Name': 'juju-gui'}, {'Name': 'juju-gui/0'})
1128 mock_app.watch.assert_called_once_with(env, 'juju-gui/0')
1129 mock_app.create_auth_token.assert_called_once_with(env)
1130 mock_open.assert_called_once_with('https://1.2.3.5/?authtoken=TOKEN')
1131@@ -798,6 +853,54 @@
1132 mock.call().close(),
1133 ])
1134
1135+ def test_customized_gui_service_port(self, mock_app, mock_open):
1136+ # The GUI API connection and the Web browser URL reflect the customized
1137+ # Juju GUI service port option.
1138+ self.configure_app(
1139+ mock_app, get_service_config={'port': 8080, 'secure': True})
1140+ # Run the application.
1141+ options = self.make_options()
1142+ with self.patch_get_juju_command():
1143+ manage.run(options)
1144+ mock_app.connect.assert_has_calls([
1145+ mock.call(
1146+ 'wss://1.2.3.4:17070/environment/env-uuid/api',
1147+ 'MyUser',
1148+ 'Secret!'),
1149+ mock.call().close(),
1150+ mock.call(
1151+ 'wss://1.2.3.5:8080/ws/environment/env-uuid/api',
1152+ 'MyUser',
1153+ 'Secret!'),
1154+ mock.call().close(),
1155+ ])
1156+ mock_open.assert_called_once_with(
1157+ 'https://1.2.3.5:8080/?authtoken=TOKEN')
1158+
1159+ def test_insecure_gui_service(self, mock_app, mock_open):
1160+ # The GUI server API and the Web browser connections can be established
1161+ # in insecure mode.
1162+ self.configure_app(
1163+ mock_app, get_service_config={'port': 443, 'secure': False})
1164+ # Run the application.
1165+ options = self.make_options()
1166+ with self.patch_get_juju_command():
1167+ manage.run(options)
1168+ mock_app.connect.assert_has_calls([
1169+ mock.call(
1170+ 'wss://1.2.3.4:17070/environment/env-uuid/api',
1171+ 'MyUser',
1172+ 'Secret!'),
1173+ mock.call().close(),
1174+ mock.call(
1175+ 'ws://1.2.3.5:443/ws/environment/env-uuid/api',
1176+ 'MyUser',
1177+ 'Secret!'),
1178+ mock.call().close(),
1179+ ])
1180+ mock_open.assert_called_once_with(
1181+ 'http://1.2.3.5:443/?authtoken=TOKEN')
1182+
1183 def test_already_bootstrapped(self, mock_app, mock_open):
1184 # The application correctly reuses an already bootstrapped environment.
1185 env = self.configure_app(mock_app, check_bootstrapped='example.com')
1186@@ -845,6 +948,21 @@
1187 mock_app.create_auth_token.assert_called_once_with(env)
1188 mock_open.assert_called_once_with('https://1.2.3.5')
1189
1190+ def test_gui_options(self, mock_app, mock_open):
1191+ # The Juju GUI is deployed with customized options if required.
1192+ env = self.configure_app(mock_app)
1193+ # Run the application.
1194+ options = self.make_options(gui_source=('juju', 'develop'), port=4242)
1195+ with self.patch_get_juju_command():
1196+ manage.run(options)
1197+ expected_config = {
1198+ 'port': 4242,
1199+ 'juju-gui-source': u'https://github.com/juju/juju-gui.git develop',
1200+ }
1201+ mock_app.deploy_gui.assert_called_once_with(
1202+ env, settings.JUJU_GUI_SERVICE_NAME, 'cs:trusty/juju-gui-42',
1203+ '0', expected_config, {'Name': 'juju-gui'}, {'Name': 'juju-gui/0'})
1204+
1205 def test_bundle(self, mock_app, mock_open):
1206 # A bundle is correctly deployed by the application.
1207 env = self.configure_app(mock_app, create_auth_token=None)
1208
1209=== modified file 'quickstart/tests/test_utils.py'
1210--- quickstart/tests/test_utils.py 2015-03-10 10:46:46 +0000
1211+++ quickstart/tests/test_utils.py 2015-05-25 08:11:40 +0000
1212@@ -112,6 +112,29 @@
1213 'apt-get install error', bytes(context_manager.exception))
1214
1215
1216+class TestBuildWebUrl(unittest.TestCase):
1217+
1218+ def test_https(self):
1219+ # A default secure URL is returned.
1220+ url = utils.build_web_url('1.2.3.4', 443, True)
1221+ self.assertEqual('https://1.2.3.4', url)
1222+
1223+ def test_http(self):
1224+ # A default insecure URL is returned.
1225+ url = utils.build_web_url('4.3.2.1', 80, False)
1226+ self.assertEqual('http://4.3.2.1', url)
1227+
1228+ def test_https_customized_port(self):
1229+ # A secure URL with customized port is returned.
1230+ url = utils.build_web_url('1.2.3.4', 4242, True)
1231+ self.assertEqual('https://1.2.3.4:4242', url)
1232+
1233+ def test_http_customized_port(self):
1234+ # An insecure URL with customized port is returned.
1235+ url = utils.build_web_url('example.com', 8000, False)
1236+ self.assertEqual('http://example.com:8000', url)
1237+
1238+
1239 class TestCall(unittest.TestCase):
1240
1241 def test_success(self):
1242
1243=== modified file 'quickstart/utils.py'
1244--- quickstart/utils.py 2015-03-10 10:46:46 +0000
1245+++ quickstart/utils.py 2015-05-25 08:11:40 +0000
1246@@ -59,6 +59,20 @@
1247 raise OSError(error.encode('utf-8'))
1248
1249
1250+def build_web_url(address, port, secure):
1251+ """Build a Web URL based on the given schemaless address, port, security.
1252+
1253+ The secure argument specifies whether to use HTTP or HTTPS protocol.
1254+
1255+ Return the URL as a string.
1256+ """
1257+ schema, default_port = 'http', 80
1258+ if secure:
1259+ schema, default_port = 'https', 443
1260+ port_part = '' if port == default_port else ':{}'.format(port)
1261+ return '{}://{}{}'.format(schema, address, port_part)
1262+
1263+
1264 def call(command, *args):
1265 """Call a subprocess passing the given arguments.
1266

Subscribers

People subscribed via source and target branches