Merge lp:~frankban/charms/trusty/juju-gui/custom-port into lp:~juju-gui/charms/trusty/juju-gui/trunk
- Trusty Tahr (14.04)
- custom-port
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 214 |
Proposed branch: | lp:~frankban/charms/trusty/juju-gui/custom-port |
Merge into: | lp:~juju-gui/charms/trusty/juju-gui/trunk |
Diff against target: |
646 lines (+303/-30) 11 files modified
README.md (+5/-1) config.yaml (+9/-1) config/guiserver.conf.template (+3/-0) hooks/backend.py (+6/-5) hooks/utils.py (+43/-4) server/guiserver/__init__.py (+1/-1) server/guiserver/manage.py (+27/-3) server/guiserver/tests/test_manage.py (+50/-6) tests/20-functional.test (+11/-4) tests/test_backends.py (+17/-4) tests/test_utils.py (+131/-1) |
To merge this branch: | bzr merge lp:~frankban/charms/trusty/juju-gui/custom-port |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju GUI Hackers | Pending | ||
Review via email: mp+241802@code.launchpad.net |
Commit message
Description of the change
Support user defined port.
Allow specifying a port on which the
GUI server starts listening for requests.
Tests: `make unittest`.
QA:
- bootstrap an environment;
- `make deploy`;
- wait for the unit to be deployed;
- visit the GUI URL, ensure port 80 redirects to 443 as usual
and the GUI works as usual;
- switch to a customized port: `juju set juju-gui port=8080`;
- ensure the GUI is no longer reachable at port 80 or 443,
instead it works correctly if you visit `https://<GUI-URL>:8080/`;
- switch to a another customized port: `juju set juju-gui port=443`;
- this time the GUI is still listening on default port for HTTPS,
but port 80 no longer redirects to HTTPS;
- switch to an invalid port: `juju set juju-gui port=80000`;
- the GUI refuses to do that, and it falls back to using
the default configuration: ensure port 80 redirects to 443;
- switch again to a customized valid port: `juju set juju-gui port=12345`;
- ensure the GUI server is listening on that port;
- finally, unset the port value: `juju unset juju-gui port`;
- back to defaults: ensure port 80 redirects to 443;
- destroy the environment.
Done, thank you.
Francesco Banconi (frankban) wrote : | # |
Richard Harding (rharding) wrote : | # |
LGTM no QA. Jeff and Francesco thanks! People are going to love getting
this feature into their hands.
Jeff Pihach (hatch) wrote : | # |
LGTM QA OK
This looks great and works great, thanks for taking over!
Francesco Banconi (frankban) wrote : | # |
*** Submitted:
Support user defined port.
Allow specifying a port on which the
GUI server starts listening for requests.
Tests: `make unittest`.
QA:
- bootstrap an environment;
- `make deploy`;
- wait for the unit to be deployed;
- visit the GUI URL, ensure port 80 redirects to 443 as usual
and the GUI works as usual;
- switch to a customized port: `juju set juju-gui port=8080`;
- ensure the GUI is no longer reachable at port 80 or 443,
instead it works correctly if you visit `https://<GUI-URL>:8080/`;
- switch to a another customized port: `juju set juju-gui port=443`;
- this time the GUI is still listening on default port for HTTPS,
but port 80 no longer redirects to HTTPS;
- switch to an invalid port: `juju set juju-gui port=80000`;
- the GUI refuses to do that, and it falls back to using
the default configuration: ensure port 80 redirects to 443;
- switch again to a customized valid port: `juju set juju-gui
port=12345`;
- ensure the GUI server is listening on that port;
- finally, unset the port value: `juju unset juju-gui port`;
- back to defaults: ensure port 80 redirects to 443;
- destroy the environment.
Done, thank you.
R=rharding, jeff.pihach
CC=
https:/
Francesco Banconi (frankban) wrote : | # |
Thanks for the reviews!
Preview Diff
1 | === modified file 'README.md' |
2 | --- README.md 2014-04-22 09:27:01 +0000 |
3 | +++ README.md 2014-11-14 13:53:57 +0000 |
4 | @@ -191,6 +191,9 @@ |
5 | 7. It supports deploying local charms by proxying browser HTTPS connections to |
6 | the Juju HTTPS API backend. This also includes retrieving and listing local |
7 | charms' files. |
8 | +8. By default, it listens on port 443 for HTTPS secure connections, and |
9 | + redirects port 80 requests to port 443. The port where the server is |
10 | + listening can be changed using the charm configuration "port" option. |
11 | |
12 | ### Legacy server ### |
13 | |
14 | @@ -206,7 +209,8 @@ |
15 | Use it only if the charm is deployed on a precise machine. |
16 | * The legacy server only provides features 1-4 from the list above. This means |
17 | bundle deployments, timed authentication tokens and local charms are not |
18 | - available when using the legacy configuration. |
19 | + available when using the legacy configuration. Also changing the default port |
20 | + where the server is listening is not supported on the legacy server. |
21 | |
22 | ## Contacting the Developers ## |
23 | |
24 | |
25 | === modified file 'config.yaml' |
26 | --- config.yaml 2014-11-04 01:31:53 +0000 |
27 | +++ config.yaml 2014-11-14 13:53:57 +0000 |
28 | @@ -36,7 +36,8 @@ |
29 | 'https://github.com/mitechie/juju-gui.git @de5e61bf9fa') which will |
30 | clone the git repository and then check out the specified commit |
31 | before building the release. |
32 | - - a URL: The release found at the given URL (ex: http://...) will be deployed. |
33 | + - a URL: The release found at the given URL (ex: http://...) will be |
34 | + deployed. |
35 | type: string |
36 | default: local |
37 | juju-gui-debug: |
38 | @@ -49,6 +50,13 @@ |
39 | Whether or not the console should be enabled for the browser. |
40 | type: boolean |
41 | default: false |
42 | + port: |
43 | + description: | |
44 | + Supply a different port to host the GUI on besides the default 80 and |
45 | + 443. If the provided port is not a valid TCP port (ranging from 1 to |
46 | + 65535) the defaults are used. |
47 | + type: int |
48 | + default: |
49 | command-log-file: |
50 | description: | |
51 | The log file where stdout and stderr should be sent for all commands |
52 | |
53 | === modified file 'config/guiserver.conf.template' |
54 | --- config/guiserver.conf.template 2014-08-29 15:17:01 +0000 |
55 | +++ config/guiserver.conf.template 2014-11-14 13:53:57 +0000 |
56 | @@ -21,6 +21,9 @@ |
57 | --guiroot="{{gui_root}}" \ |
58 | --sslpath="{{ssl_cert_path}}" \ |
59 | --charmworldurl="{{charmworld_url}}" \ |
60 | + {{if port}} |
61 | + --port={{port}} \ |
62 | + {{endif}} |
63 | {{if sandbox}} |
64 | --sandbox \ |
65 | {{else}} |
66 | |
67 | === modified file 'hooks/backend.py' |
68 | --- hooks/backend.py 2014-02-04 20:07:31 +0000 |
69 | +++ hooks/backend.py 2014-11-14 13:53:57 +0000 |
70 | @@ -44,7 +44,6 @@ |
71 | |
72 | from charmhelpers import ( |
73 | log, |
74 | - open_port, |
75 | ) |
76 | |
77 | import utils |
78 | @@ -133,9 +132,10 @@ |
79 | cached_fonts=config['cached-fonts'], ga_key=config['ga-key'], |
80 | show_get_juju_button=config['show-get-juju-button'], |
81 | password=config.get('password')) |
82 | - # Expose the service. |
83 | - open_port(80) |
84 | - open_port(443) |
85 | + # Set up TCP ports. |
86 | + previous_port = backend.prev_config.get('port') |
87 | + current_port = backend.config.get('port') |
88 | + utils.setup_ports(previous_port, current_port) |
89 | |
90 | |
91 | class ServerInstallMixinBase(object): |
92 | @@ -198,7 +198,8 @@ |
93 | utils.start_builtin_server( |
94 | build_dir, config['ssl-cert-path'], config['serve-tests'], |
95 | config['sandbox'], config['builtin-server-logging'], |
96 | - not config['secure'], config['charmworld-url']) |
97 | + not config['secure'], config['charmworld-url'], |
98 | + port=config.get('port')) |
99 | |
100 | def stop(self, backend): |
101 | utils.stop_builtin_server() |
102 | |
103 | === modified file 'hooks/utils.py' |
104 | --- hooks/utils.py 2014-09-29 14:52:52 +0000 |
105 | +++ hooks/utils.py 2014-11-14 13:53:57 +0000 |
106 | @@ -46,6 +46,7 @@ |
107 | 'setup_apache_config', |
108 | 'setup_gui', |
109 | 'setup_haproxy_config', |
110 | + 'setup_ports', |
111 | 'start_builtin_server', |
112 | 'start_haproxy_apache', |
113 | 'stop_builtin_server', |
114 | @@ -71,8 +72,10 @@ |
115 | import tempita |
116 | |
117 | from charmhelpers import ( |
118 | + close_port, |
119 | get_config, |
120 | log, |
121 | + open_port, |
122 | RESTART, |
123 | service_control, |
124 | STOP, |
125 | @@ -413,6 +416,37 @@ |
126 | render_to_file('config.js.template', context, config_js_path) |
127 | |
128 | |
129 | +# Simple checker function for port ranges. |
130 | +port_in_range = lambda port: 1 <= port <= 65535 |
131 | + |
132 | + |
133 | +def setup_ports(previous_port, current_port): |
134 | + """Open or close ports based on the supplied ports. |
135 | + |
136 | + The given ports specify the previously provided and the current value. |
137 | + They can be int numbers if the ports are specified, None otherwise, in |
138 | + which case the default ones (80 and 443) are used. |
139 | + """ |
140 | + # If a custom port was previously defined we want to make sure we close it. |
141 | + if previous_port is not None and port_in_range(previous_port): |
142 | + log('Closing user provided port {}.'.format(previous_port)) |
143 | + close_port(previous_port) |
144 | + if current_port is not None: |
145 | + if port_in_range(current_port): |
146 | + # Ensure the default ports are closed when setting the custom one. |
147 | + log('Closing default ports 80 and 443.') |
148 | + close_port(80) |
149 | + close_port(443) |
150 | + # Open the custom defined port. |
151 | + log('Opening user provided port {}.'.format(current_port)) |
152 | + open_port(current_port) |
153 | + return |
154 | + log('Ignoring provided port {}: not in range.'.format(current_port)) |
155 | + log('Opening default ports 80 and 443.') |
156 | + open_port(80) |
157 | + open_port(443) |
158 | + |
159 | + |
160 | def setup_haproxy_config(ssl_cert_path, secure=True): |
161 | """Generate the haproxy configuration file.""" |
162 | log('Setting up haproxy Upstart file.') |
163 | @@ -513,7 +547,8 @@ |
164 | |
165 | def write_builtin_server_startup( |
166 | gui_root, ssl_cert_path, serve_tests=False, sandbox=False, |
167 | - builtin_server_logging='info', insecure=False, charmworld_url=''): |
168 | + builtin_server_logging='info', insecure=False, charmworld_url='', |
169 | + port=None): |
170 | """Generate the builtin server Upstart file.""" |
171 | log('Generating the builtin server Upstart file.') |
172 | context = { |
173 | @@ -526,7 +561,8 @@ |
174 | 'charmworld_url': charmworld_url, |
175 | 'http_proxy': os.environ.get('http_proxy'), |
176 | 'https_proxy': os.environ.get('https_proxy'), |
177 | - 'no_proxy': os.environ.get('no_proxy', os.environ.get('NO_PROXY')) |
178 | + 'no_proxy': os.environ.get('no_proxy', os.environ.get('NO_PROXY')), |
179 | + 'port': port, |
180 | } |
181 | if not sandbox: |
182 | api_url = 'wss://{}'.format(get_api_address()) |
183 | @@ -542,12 +578,15 @@ |
184 | |
185 | def start_builtin_server( |
186 | build_dir, ssl_cert_path, serve_tests, sandbox, builtin_server_logging, |
187 | - insecure, charmworld_url): |
188 | + insecure, charmworld_url, port=None): |
189 | """Start the builtin server.""" |
190 | + if (port is not None) and not port_in_range(port): |
191 | + # Do not use the user provided port if it is not valid. |
192 | + port = None |
193 | write_builtin_server_startup( |
194 | build_dir, ssl_cert_path, serve_tests=serve_tests, sandbox=sandbox, |
195 | builtin_server_logging=builtin_server_logging, insecure=insecure, |
196 | - charmworld_url=charmworld_url) |
197 | + charmworld_url=charmworld_url, port=port) |
198 | log('Starting the builtin server.') |
199 | with su('root'): |
200 | service_control(BUILTIN_SERVER, RESTART) |
201 | |
202 | === modified file 'server/guiserver/__init__.py' |
203 | --- server/guiserver/__init__.py 2014-04-09 13:16:08 +0000 |
204 | +++ server/guiserver/__init__.py 2014-11-14 13:53:57 +0000 |
205 | @@ -37,7 +37,7 @@ |
206 | which originally made the request. |
207 | """ |
208 | |
209 | -VERSION = (0, 4, 0) |
210 | +VERSION = (0, 4, 1) |
211 | |
212 | |
213 | def get_version(): |
214 | |
215 | === modified file 'server/guiserver/manage.py' |
216 | --- server/guiserver/manage.py 2014-01-24 15:38:37 +0000 |
217 | +++ server/guiserver/manage.py 2014-11-14 13:53:57 +0000 |
218 | @@ -73,6 +73,18 @@ |
219 | option_name, ', '.join(choices))) |
220 | |
221 | |
222 | +def _validate_range(option_name, min_value, max_value): |
223 | + """Ensure the numeric value passed for the given option is in range. |
224 | + |
225 | + The range is defined by min_value and max_value. |
226 | + Exit with an error if the value is not in the given range. |
227 | + """ |
228 | + value = options[option_name] |
229 | + if (value is not None) and not (min_value <= value <= max_value): |
230 | + sys.exit('error: the {} argument must be included between ' |
231 | + '{} and {}'.format(option_name, min_value, max_value)) |
232 | + |
233 | + |
234 | def _get_ssl_options(): |
235 | """Return a Tornado SSL options dict. |
236 | |
237 | @@ -120,11 +132,17 @@ |
238 | define( |
239 | 'charmworldurl', type=str, |
240 | help='The URL to use for Charmworld.') |
241 | + define( |
242 | + 'port', type=int, |
243 | + help='User defined port to run the server on. If no port is defined ' |
244 | + 'the server will be started on 80 and 443 as per the default ' |
245 | + 'port options from the charm.') |
246 | |
247 | # In Tornado, parsing the options also sets up the default logger. |
248 | parse_command_line() |
249 | _validate_required('guiroot') |
250 | _validate_choices('apiversion', ('go', 'python')) |
251 | + _validate_range('port', 1, 65535) |
252 | _add_debug(logging.getLogger()) |
253 | # Configure the asynchronous HTTP client used by proxy handlers. |
254 | AsyncHTTPClient.configure( |
255 | @@ -133,13 +151,19 @@ |
256 | |
257 | def run(): |
258 | """Run the server""" |
259 | + port = options.port |
260 | if options.insecure: |
261 | # Run the server over an insecure HTTP connection. |
262 | - server().listen(80) |
263 | + if port is None: |
264 | + port = 80 |
265 | + server().listen(port) |
266 | else: |
267 | # Default configuration: run the server over a secure HTTPS connection. |
268 | - server().listen(443, ssl_options=_get_ssl_options()) |
269 | - redirector().listen(80) |
270 | + if port is None: |
271 | + port = 443 |
272 | + redirector().listen(80) |
273 | + server().listen(port, ssl_options=_get_ssl_options()) |
274 | version = guiserver.get_version() |
275 | logging.info('starting Juju GUI server v{}'.format(version)) |
276 | + logging.info('listening on port {}'.format(port)) |
277 | IOLoop.instance().start() |
278 | |
279 | === modified file 'server/guiserver/tests/test_manage.py' |
280 | --- server/guiserver/tests/test_manage.py 2013-08-08 09:20:37 +0000 |
281 | +++ server/guiserver/tests/test_manage.py 2014-11-14 13:53:57 +0000 |
282 | @@ -126,6 +126,31 @@ |
283 | manage._validate_choices('arg1', self.choices) |
284 | |
285 | |
286 | +class TestValidateRange(ValidatorTestMixin, unittest.TestCase): |
287 | + |
288 | + value_range = (1, 10) |
289 | + error = 'error: the {} argument must be included between {} and {}' |
290 | + |
291 | + def test_success(self): |
292 | + # The validation passes if the value is included in the range. |
293 | + for value in range(1, 11): |
294 | + with mock.patch('guiserver.manage.options', {'arg1': value}): |
295 | + manage._validate_range('arg1', *self.value_range) |
296 | + |
297 | + def test_failure_invalid_range(self): |
298 | + # The validation fails if the value is not in range. |
299 | + error = self.error.format('arg1', *self.value_range) |
300 | + for value in (-50, 0, 11, 100): |
301 | + with mock.patch('guiserver.manage.options', {'arg1': value}): |
302 | + with self.assert_sysexit(error): |
303 | + manage._validate_range('arg1', *self.value_range) |
304 | + |
305 | + def test_success_missing(self): |
306 | + # The validation succeeds if the value is missing. |
307 | + with mock.patch('guiserver.manage.options', {'arg1': None}): |
308 | + manage._validate_range('arg1', *self.value_range) |
309 | + |
310 | + |
311 | class TestGetSslOptions(unittest.TestCase): |
312 | |
313 | def test_options(self): |
314 | @@ -141,6 +166,11 @@ |
315 | |
316 | class TestRun(LogTrapTestCase, unittest.TestCase): |
317 | |
318 | + expected_ssl_options = { |
319 | + 'certfile': '/my/sslpath/juju.crt', |
320 | + 'keyfile': '/my/sslpath/juju.key', |
321 | + } |
322 | + |
323 | def mock_and_run(self, **kwargs): |
324 | """Run the application after mocking the IO loop and the options/apps. |
325 | |
326 | @@ -149,6 +179,7 @@ |
327 | options = { |
328 | 'apiversion': 'go', |
329 | 'guiroot': '/my/guiroot', |
330 | + 'port': None, |
331 | 'sslpath': '/my/sslpath', |
332 | } |
333 | options.update(kwargs) |
334 | @@ -163,20 +194,33 @@ |
335 | def test_secure_mode(self): |
336 | # The application is correctly run in secure mode. |
337 | _, redirector_listen, server_listen = self.mock_and_run(insecure=False) |
338 | - expected_ssl_options = { |
339 | - 'certfile': '/my/sslpath/juju.crt', |
340 | - 'keyfile': '/my/sslpath/juju.key', |
341 | - } |
342 | redirector_listen.assert_called_once_with(80) |
343 | server_listen.assert_called_once_with( |
344 | - 443, ssl_options=expected_ssl_options) |
345 | + 443, ssl_options=self.expected_ssl_options) |
346 | |
347 | def test_insecure_mode(self): |
348 | # The application is correctly run in insecure mode. |
349 | _, redirector_listen, server_listen = self.mock_and_run(insecure=True) |
350 | - self.assertEqual(0, redirector_listen.call_count) |
351 | + self.assertFalse(redirector_listen.called) |
352 | server_listen.assert_called_once_with(80) |
353 | |
354 | + def test_customized_port_secure_mode(self): |
355 | + # If the user provided a port, the server starts listening on that port |
356 | + # and the redirector is not used. |
357 | + _, redirector_listen, server_listen = self.mock_and_run( |
358 | + insecure=False, port=8080) |
359 | + self.assertFalse(redirector_listen.called) |
360 | + server_listen.assert_called_once_with( |
361 | + 8080, ssl_options=self.expected_ssl_options) |
362 | + |
363 | + def test_customized_port_insecure_mode(self): |
364 | + # The application is correctly run in insecure mode with a user |
365 | + # provided port. |
366 | + _, redirector_listen, server_listen = self.mock_and_run( |
367 | + insecure=True, port=12345) |
368 | + self.assertFalse(redirector_listen.called) |
369 | + server_listen.assert_called_once_with(12345) |
370 | + |
371 | def test_ioloop_started(self): |
372 | # The IO loop instance is started when the application is run. |
373 | ioloop_start, _, _ = self.mock_and_run() |
374 | |
375 | === modified file 'tests/20-functional.test' |
376 | --- tests/20-functional.test 2014-10-15 07:49:26 +0000 |
377 | +++ tests/20-functional.test 2014-11-14 13:53:57 +0000 |
378 | @@ -83,8 +83,6 @@ |
379 | |
380 | class DeployTestMixin(object): |
381 | |
382 | - port = '443' |
383 | - |
384 | def setUp(self): |
385 | # Perform all graphical operations in memory. |
386 | vdisplay = Xvfb(width=1280, height=720) |
387 | @@ -115,14 +113,14 @@ |
388 | error='Browser warning dialog not found.') |
389 | continue_button.click() |
390 | |
391 | - def navigate_to(self, hostname, path='/'): |
392 | + def navigate_to(self, hostname, port=443, path='/'): |
393 | """Load a page using the current Selenium driver. |
394 | |
395 | The page URL is calculated using the provided *hostname* and *path*. |
396 | Retry loading the page until the page is found or a timeout exception |
397 | is raised. |
398 | """ |
399 | - base_url = 'https://{}:{}'.format(hostname, self.port) |
400 | + base_url = 'https://{}:{}'.format(hostname, port) |
401 | url = urlparse.urljoin(base_url, path) |
402 | |
403 | def page_ready(driver): |
404 | @@ -238,6 +236,15 @@ |
405 | self.assertIn('public', cache_directives) |
406 | self.assertIn('must-revalidate', cache_directives) |
407 | |
408 | + def test_different_port(self): |
409 | + # The port on which the server listens can be successfully changed. |
410 | + options = {'port': 8080} |
411 | + self.service_name, unit_info = juju_deploy_gui(options=options) |
412 | + hostname = unit_info['public-address'] |
413 | + self.navigate_to(hostname, port=8080) |
414 | + self.handle_browser_warning() |
415 | + self.assertEnvironmentIsConnected() |
416 | + |
417 | |
418 | class TestBuiltinServerLocalRelease(DeployTestMixin, unittest.TestCase): |
419 | |
420 | |
421 | === modified file 'tests/test_backends.py' |
422 | --- tests/test_backends.py 2014-02-04 20:07:31 +0000 |
423 | +++ tests/test_backends.py 2014-11-14 13:53:57 +0000 |
424 | @@ -169,12 +169,12 @@ |
425 | 'install_missing_packages': mock.patch( |
426 | 'backend.utils.install_missing_packages'), |
427 | 'log': mock.patch('backend.log'), |
428 | - 'open_port': mock.patch('backend.open_port'), |
429 | 'parse_source': mock.patch( |
430 | 'backend.utils.parse_source', mock_parse_source), |
431 | 'save_or_create_certificates': mock.patch( |
432 | 'backend.utils.save_or_create_certificates'), |
433 | 'setup_gui': mock.patch('backend.utils.setup_gui'), |
434 | + 'setup_ports': mock.patch('backend.utils.setup_ports'), |
435 | 'start_builtin_server': mock.patch( |
436 | 'backend.utils.start_builtin_server'), |
437 | 'start_haproxy_apache': mock.patch( |
438 | @@ -280,7 +280,7 @@ |
439 | mocks.compute_build_dir.assert_called_with( |
440 | config['juju-gui-debug'], config['serve-tests']) |
441 | self.assert_write_gui_config_called(mocks, config) |
442 | - mocks.open_port.assert_has_calls([mock.call(80), mock.call(443)]) |
443 | + mocks.setup_ports.assert_called_once_with(None, None) |
444 | mocks.start_haproxy_apache.assert_called_once_with( |
445 | mocks.compute_build_dir(), config['serve-tests'], |
446 | self.ssl_cert_path, config['secure']) |
447 | @@ -295,14 +295,27 @@ |
448 | mocks.compute_build_dir.assert_called_with( |
449 | config['juju-gui-debug'], config['serve-tests']) |
450 | self.assert_write_gui_config_called(mocks, config) |
451 | - mocks.open_port.assert_has_calls([mock.call(80), mock.call(443)]) |
452 | + mocks.setup_ports.assert_called_once_with(None, None) |
453 | mocks.start_builtin_server.assert_called_once_with( |
454 | mocks.compute_build_dir(), self.ssl_cert_path, |
455 | config['serve-tests'], config['sandbox'], |
456 | config['builtin-server-logging'], not config['secure'], |
457 | - config['charmworld-url']) |
458 | + config['charmworld-url'], port=None) |
459 | self.assertFalse(mocks.start_haproxy_apache.called) |
460 | |
461 | + def test_start_go_builtin_user_provided_port(self): |
462 | + # Start a juju backend with builtin server and a user provided port. |
463 | + config = self.make_config({'builtin-server': True, 'port': 8080}) |
464 | + test_backend = backend.Backend(config=config) |
465 | + with self.mock_all() as mocks: |
466 | + test_backend.start() |
467 | + mocks.setup_ports.assert_called_once_with(None, 8080) |
468 | + mocks.start_builtin_server.assert_called_once_with( |
469 | + mocks.compute_build_dir(), self.ssl_cert_path, |
470 | + config['serve-tests'], config['sandbox'], |
471 | + config['builtin-server-logging'], not config['secure'], |
472 | + config['charmworld-url'], port=8080) |
473 | + |
474 | def test_stop_go_legacy(self): |
475 | # Stop a juju-core backend with legacy server. |
476 | config = self.make_config({'builtin-server': False}) |
477 | |
478 | === modified file 'tests/test_utils.py' |
479 | --- tests/test_utils.py 2014-02-04 20:07:31 +0000 |
480 | +++ tests/test_utils.py 2014-11-14 13:53:57 +0000 |
481 | @@ -48,12 +48,14 @@ |
482 | install_missing_packages, |
483 | log_hook, |
484 | parse_source, |
485 | + port_in_range, |
486 | remove_apache_setup, |
487 | remove_haproxy_setup, |
488 | render_to_file, |
489 | save_or_create_certificates, |
490 | setup_apache_config, |
491 | setup_haproxy_config, |
492 | + setup_ports, |
493 | start_builtin_server, |
494 | start_haproxy_apache, |
495 | stop_builtin_server, |
496 | @@ -908,6 +910,16 @@ |
497 | self.assertNotIn('--sandbox', guiserver_conf) |
498 | self.assertIn('--charmworldurl="http://charmworld.example.com/"', |
499 | guiserver_conf) |
500 | + # By default the port is not provided to the GUI server. |
501 | + self.assertNotIn('--port', guiserver_conf) |
502 | + |
503 | + def test_write_builtin_server_startup_with_port(self): |
504 | + # The builtin server Upstart file is properly generate when a |
505 | + # customized port is provided. |
506 | + write_builtin_server_startup( |
507 | + JUJU_GUI_DIR, self.ssl_cert_path, port=8000) |
508 | + guiserver_conf = self.files['guiserver.conf'] |
509 | + self.assertIn('--port=8000', guiserver_conf) |
510 | |
511 | def test_write_builtin_server_startup_sandbox_and_logging(self): |
512 | # The upstart configuration file for the GUI server is correctly |
513 | @@ -927,7 +939,7 @@ |
514 | start_builtin_server( |
515 | JUJU_GUI_DIR, self.ssl_cert_path, serve_tests=False, sandbox=False, |
516 | builtin_server_logging='info', insecure=False, |
517 | - charmworld_url='http://charmworld.example.com/') |
518 | + charmworld_url='http://charmworld.example.com/', port=443) |
519 | self.assertEqual(self.svc_ctl_call_count, 1) |
520 | self.assertEqual(self.service_names, ['guiserver']) |
521 | self.assertEqual(self.actions, [charmhelpers.RESTART]) |
522 | @@ -1040,6 +1052,124 @@ |
523 | self.assertIn('cachedFonts: true', js_conf) |
524 | |
525 | |
526 | +class TestPortInRange(unittest.TestCase): |
527 | + |
528 | + def test_valid_port(self): |
529 | + # True is returned if the port is in range. |
530 | + for port in (1, 80, 443, 1234, 8080, 54321, 65535): |
531 | + self.assertTrue(port_in_range(port), port) |
532 | + |
533 | + def test_invalid_port(self): |
534 | + # False is returned if the port is not in range. |
535 | + for port in (-10, 0, 65536, 100000): |
536 | + self.assertFalse(port_in_range(port), port) |
537 | + |
538 | + |
539 | +@mock.patch('utils.close_port') |
540 | +@mock.patch('utils.open_port') |
541 | +@mock.patch('utils.log') |
542 | +class TestSetupPorts(unittest.TestCase): |
543 | + |
544 | + def test_default_ports(self, mock_log, mock_open_port, mock_close_port): |
545 | + # The default ports are properly opened. |
546 | + setup_ports(None, None) |
547 | + mock_log.assert_called_once_with('Opening default ports 80 and 443.') |
548 | + self.assertEqual(2, mock_open_port.call_count) |
549 | + mock_open_port.assert_has_calls([mock.call(80), mock.call(443)]) |
550 | + self.assertFalse(mock_close_port.called) |
551 | + |
552 | + def test_from_defaults_to_new_port( |
553 | + self, mock_log, mock_open_port, mock_close_port): |
554 | + # The user provides a customized port. |
555 | + setup_ports(None, 8080) |
556 | + self.assertEqual(2, mock_log.call_count) |
557 | + mock_log.assert_has_calls([ |
558 | + mock.call('Closing default ports 80 and 443.'), |
559 | + mock.call('Opening user provided port 8080.'), |
560 | + ]) |
561 | + mock_open_port.assert_called_once_with(8080) |
562 | + self.assertEqual(2, mock_close_port.call_count) |
563 | + mock_close_port.assert_has_calls([mock.call(80), mock.call(443)]) |
564 | + |
565 | + def test_from_previous_port_to_new_port( |
566 | + self, mock_log, mock_open_port, mock_close_port): |
567 | + # The user switches from a previously provided port to a new one. |
568 | + setup_ports(8080, 1234) |
569 | + self.assertEqual(3, mock_log.call_count) |
570 | + mock_log.assert_has_calls([ |
571 | + mock.call('Closing user provided port 8080.'), |
572 | + # Always close the default ports in those cases. |
573 | + mock.call('Closing default ports 80 and 443.'), |
574 | + mock.call('Opening user provided port 1234.') |
575 | + ]) |
576 | + mock_open_port.assert_called_once_with(1234) |
577 | + self.assertEqual(3, mock_close_port.call_count) |
578 | + mock_close_port.assert_has_calls([ |
579 | + mock.call(8080), mock.call(80), mock.call(443)]) |
580 | + |
581 | + def test_from_previous_port_to_defaults( |
582 | + self, mock_log, mock_open_port, mock_close_port): |
583 | + # The user provided a port and then switches back to defaults. |
584 | + setup_ports(1234, None) |
585 | + self.assertEqual(2, mock_log.call_count) |
586 | + mock_log.assert_has_calls([ |
587 | + mock.call('Closing user provided port 1234.'), |
588 | + mock.call('Opening default ports 80 and 443.'), |
589 | + ]) |
590 | + self.assertEqual(2, mock_open_port.call_count) |
591 | + mock_open_port.assert_has_calls([mock.call(80), mock.call(443)]) |
592 | + mock_close_port.assert_called_once_with(1234) |
593 | + |
594 | + def test_from_previous_port_to_invalid( |
595 | + self, mock_log, mock_open_port, mock_close_port): |
596 | + # The user switches from a previously provided port to an invalid one. |
597 | + setup_ports(8080, 0) |
598 | + self.assertEqual(3, mock_log.call_count) |
599 | + mock_log.assert_has_calls([ |
600 | + mock.call('Closing user provided port 8080.'), |
601 | + mock.call('Ignoring provided port 0: not in range.'), |
602 | + mock.call('Opening default ports 80 and 443.'), |
603 | + ]) |
604 | + self.assertEqual(2, mock_open_port.call_count) |
605 | + mock_open_port.assert_has_calls([mock.call(80), mock.call(443)]) |
606 | + mock_close_port.assert_called_once_with(8080) |
607 | + |
608 | + def test_from_defaults_to_invalid( |
609 | + self, mock_log, mock_open_port, mock_close_port): |
610 | + # The user provides an invalid port. |
611 | + setup_ports(None, 100000) |
612 | + self.assertEqual(2, mock_log.call_count) |
613 | + mock_log.assert_has_calls([ |
614 | + mock.call('Ignoring provided port 100000: not in range.'), |
615 | + mock.call('Opening default ports 80 and 443.'), |
616 | + ]) |
617 | + self.assertEqual(2, mock_open_port.call_count) |
618 | + mock_open_port.assert_has_calls([mock.call(80), mock.call(443)]) |
619 | + self.assertFalse(mock_close_port.called) |
620 | + |
621 | + def test_from_invalid_to_new_port( |
622 | + self, mock_log, mock_open_port, mock_close_port): |
623 | + # The user fixes a previously provided invalid port. |
624 | + setup_ports(123456, 8000) |
625 | + self.assertEqual(2, mock_log.call_count) |
626 | + mock_log.assert_has_calls([ |
627 | + mock.call('Closing default ports 80 and 443.'), |
628 | + mock.call('Opening user provided port 8000.') |
629 | + ]) |
630 | + mock_open_port.assert_called_once_with(8000) |
631 | + self.assertEqual(2, mock_close_port.call_count) |
632 | + mock_close_port.assert_has_calls([mock.call(80), mock.call(443)]) |
633 | + |
634 | + def test_from_invalid_to_defaults( |
635 | + self, mock_log, mock_open_port, mock_close_port): |
636 | + # The user switches back to default after providing an invalid port. |
637 | + setup_ports(0, None) |
638 | + mock_log.assert_called_once_with('Opening default ports 80 and 443.') |
639 | + self.assertEqual(2, mock_open_port.call_count) |
640 | + mock_open_port.assert_has_calls([mock.call(80), mock.call(443)]) |
641 | + self.assertFalse(mock_close_port.called) |
642 | + |
643 | + |
644 | @mock.patch('utils.run') |
645 | @mock.patch('utils.log') |
646 | @mock.patch('utils.cmd_log', mock.Mock()) |
Reviewers: mp+241802_ code.launchpad. net,
Message:
Please take a look.
Description:
Support user defined port.
Allow specifying a port on which the
GUI server starts listening for requests.
Tests: `make unittest`.
QA:
- bootstrap an environment;
- `make deploy`;
- wait for the unit to be deployed;
- visit the GUI URL, ensure port 80 redirects to 443 as usual
and the GUI works as usual;
- switch to a customized port: `juju set juju-gui port=8080`;
- ensure the GUI is no longer reachable at port 80 or 443,
instead it works correctly if you visit `https://<GUI-URL>:8080/`;
- switch to a another customized port: `juju set juju-gui port=443`;
- this time the GUI is still listening on default port for HTTPS,
but port 80 no longer redirects to HTTPS;
- switch to an invalid port: `juju set juju-gui port=80000`;
- the GUI refuses to do that, and it falls back to using
the default configuration: ensure port 80 redirects to 443;
- switch again to a customized valid port: `juju set juju-gui
port=12345`;
- ensure the GUI server is listening on that port;
- finally, unset the port value: `juju unset juju-gui port`;
- back to defaults: ensure port 80 redirects to 443;
- destroy the environment.
Done, thank you.
https:/ /code.launchpad .net/~frankban/ charms/ trusty/ juju-gui/ custom- port/+merge/ 241802
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/174170043/
Affected files (+305, -30 lines): guiserver. conf.template guiserver/ __init_ _.py guiserver/ manage. py guiserver/ tests/test_ manage. py functional. test backends. py
M README.md
A [revision details]
M config.yaml
M config/
M hooks/backend.py
M hooks/utils.py
M server/
M server/
M server/
M tests/20-
M tests/test_
M tests/test_utils.py