diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index 8c2ba4a53401b..456a7941d5e86 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -779,13 +779,6 @@ to allow direct connections to the manager nodes, you could set up a proxy that automatically forwards incoming requests to the currently active ceph-mgr instance. -.. note:: - Note that putting the dashboard behind a load-balancing proxy like `HAProxy - `_ currently has some limitations, particularly if - you require the traffic between the proxy and the dashboard to be encrypted - via SSL/TLS. See `BUG#24662 `_ for - details. - Configuring a URL Prefix ^^^^^^^^^^^^^^^^^^^^^^^^ @@ -800,6 +793,71 @@ to use hyperlinks that include your prefix, you can set the so you can access the dashboard at ``http://$IP:$PORT/$PREFIX/``. +Disable the redirection +^^^^^^^^^^^^^^^^^^^^^^^ + +If the dashboard is behind a load-balancing proxy like `HAProxy `_ +you might want to disable the redirection behaviour to prevent situations that +internal (unresolvable) URL's are published to the frontend client. Use the +following command to get the dashboard to respond with a HTTP error (500 by default) +instead of redirecting to the active dashboard:: + + $ ceph config set mgr mgr/dashboard/standby_behaviour "error" + +To reset the setting to the default redirection behaviour, use the following command:: + + $ ceph config set mgr mgr/dashboard/standby_behaviour "redirect" + +Configure the error status code +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When the redirection behaviour is disabled, then you want to customize the HTTP status +code of standby dashboards. To do so you need to run the command:: + + $ ceph config set mgr mgr/dashboard/standby_error_status_code 503 + +HAProxy example configuration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Below you will find an example configuration for SSL/TLS pass through using +`HAProxy `_. + +Please note that the configuration works under the following conditions. +If the dashboard fails over, the front-end client might receive a HTTP redirect +(303) response and will be redirected to an unresolvable host. This happens when +the failover occurs during two HAProxy health checks. In this situation the +previously active dashboard node will now respond with a 303 which points to +the new active node. To prevent that situation you should consider to disable +the redirection behaviour on standby nodes. + +:: + + defaults + log global + option log-health-checks + timeout connect 5s + timeout client 50s + timeout server 450s + + frontend dashboard_front + mode http + bind *:80 + option httplog + redirect scheme https code 301 if !{ ssl_fc } + + frontend dashboard_front_ssl + mode tcp + bind *:443 + option tcplog + default_backend dashboard_back_ssl + + backend dashboard_back_ssl + mode tcp + option httpchk GET / + http-check expect status 200 + server x : check-ssl check verify none + server y : check-ssl check verify none + server z : check-ssl check verify none .. _dashboard-auditing: diff --git a/qa/tasks/mgr/test_dashboard.py b/qa/tasks/mgr/test_dashboard.py index 3b778520da872..b0cf200d61be4 100644 --- a/qa/tasks/mgr/test_dashboard.py +++ b/qa/tasks/mgr/test_dashboard.py @@ -20,6 +20,14 @@ def setUp(self): self.mgr_cluster.mon_manager.raw_cluster_cmd("dashboard", "create-self-signed-cert") + def tearDown(self): + self.mgr_cluster.mon_manager.raw_cluster_cmd("config", "set", "mgr", + "mgr/dashboard/standby_behaviour", + "redirect") + self.mgr_cluster.mon_manager.raw_cluster_cmd("config", "set", "mgr", + "mgr/dashboard/standby_error_status_code", + "500") + def test_standby(self): original_active_id = self.mgr_cluster.get_active_id() original_uri = self._get_uri("dashboard") @@ -46,6 +54,42 @@ def test_standby(self): self.assertEqual(r.status_code, 303) self.assertEqual(r.headers['Location'], failed_over_uri) + def test_standby_disable_redirect(self): + self.mgr_cluster.mon_manager.raw_cluster_cmd("config", "set", "mgr", + "mgr/dashboard/standby_behaviour", + "error") + + original_active_id = self.mgr_cluster.get_active_id() + original_uri = self._get_uri("dashboard") + log.info("Originally running manager '{}' at {}".format( + original_active_id, original_uri)) + + # Force a failover and wait until the previously active manager + # is listed as standby. + self.mgr_cluster.mgr_fail(original_active_id) + self.wait_until_true( + lambda: original_active_id in self.mgr_cluster.get_standby_ids(), + timeout=30) + + failed_active_id = self.mgr_cluster.get_active_id() + failed_over_uri = self._get_uri("dashboard") + log.info("After failover running manager '{}' at {}".format( + failed_active_id, failed_over_uri)) + + self.assertNotEqual(original_uri, failed_over_uri) + + # Redirection should be disabled now, instead a 500 must be returned. + r = requests.get(original_uri, allow_redirects=False, verify=False) + self.assertEqual(r.status_code, 500) + + self.mgr_cluster.mon_manager.raw_cluster_cmd("config", "set", "mgr", + "mgr/dashboard/standby_error_status_code", + "503") + + # The customized HTTP status code (503) must be returned. + r = requests.get(original_uri, allow_redirects=False, verify=False) + self.assertEqual(r.status_code, 503) + def test_urls(self): base_uri = self._get_uri("dashboard") diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index 2486a164dc705..b080d17b0acd2 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -272,7 +272,11 @@ class Module(MgrModule, CherryPyConfig): Option(name='username', type='str', default=''), Option(name='key_file', type='str', default=''), Option(name='crt_file', type='str', default=''), - Option(name='ssl', type='bool', default=True) + Option(name='ssl', type='bool', default=True), + Option(name='standby_behaviour', type='str', default='redirect', + enum_allowed=['redirect', 'error']), + Option(name='standby_error_status_code', type='int', default=500, + min=400, max=599) ] MODULE_OPTIONS.extend(options_schema_list()) for options in PLUGIN_MANAGER.hook.get_options() or []: @@ -470,28 +474,32 @@ def serve(self): class Root(object): @cherrypy.expose def index(self): - active_uri = module.get_active_uri() - if active_uri: - module.log.info("Redirecting to active '%s'", active_uri) - raise cherrypy.HTTPRedirect(active_uri) + if module.get_module_option('standby_behaviour', 'redirect') == 'redirect': + active_uri = module.get_active_uri() + if active_uri: + module.log.info("Redirecting to active '%s'", active_uri) + raise cherrypy.HTTPRedirect(active_uri) + else: + template = """ + + + + Ceph + + + + No active ceph-mgr instance is currently running + the dashboard. A failover may be in progress. + Retrying in {delay} seconds... + + + """ + return template.format(delay=5) else: - template = """ - - - - Ceph - - - - No active ceph-mgr instance is currently running - the dashboard. A failover may be in progress. - Retrying in {delay} seconds... - - - """ - return template.format(delay=5) + status = module.get_module_option('standby_error_status_code', 500) + raise cherrypy.HTTPError(status, message="Keep on looking") cherrypy.tree.mount(Root(), "{}/".format(self.url_prefix), {}) self.log.info("Starting engine...")