Merge lp:~frankban/juju-quickstart/handle-gui-ports into lp:juju-quickstart
- handle-gui-ports
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+260032@code.launchpad.net |
Commit message
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/
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/
- 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/
- destroy the environment;
- use quickstart again to server the GUI develop
branch on a customized port:
`devenv/
- check that everything works correctly;
- destroy the environment;
- done, thank you!
Francesco Banconi (frankban) wrote : | # |
- 141. By Francesco Banconi
-
Fix typo.
Francesco Banconi (frankban) wrote : | # |
Please take a look.
https:/
File quickstart/
https:/
quickstart/
This function was just moved.
Madison Scott-Clary (makyo) wrote : | # |
LGTM QA okay! Thanks for the work on this
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/
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/
- 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/
- destroy the environment;
- use quickstart again to server the GUI develop
branch on a customized port:
`devenv/
juju/develop`;
- check that everything works correctly;
- destroy the environment;
- done, thank you!
Preview Diff
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 |
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) my-feature- branch or juju/develop)
intended for development use only. This allows
for building a specific GUI github branch
(e.g. frankban/
when installing the Juju GUI, and can be useful
while coding/QAing GUI branches.
Tests: `make check`.
QA: bin/juju- quickstart` ; bin/juju- quickstart` ; bin/juju- quickstart --gui-port 8000 --gui-source
- use the GUI as usual, to check for regressions:
`devenv/
- 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/
- destroy the environment;
- use quickstart again to server the GUI develop
branch on a customized port:
`devenv/
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): __init_ _.py jujugui. py jujutools. py manage. py platform_ support. py tests/test_ app.py tests/test_ juju.py tests/test_ jujugui. py tests/test_ jujutools. py tests/test_ manage. py tests/test_ utils.py
A [revision details]
M quickstart/
M quickstart/app.py
A quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/
A quickstart/
M quickstart/
M quickstart/
M quickstart/
M quickstart/utils.py