Add config for extra regions

This is so we can register extra region endpoints in horizon,
in situations where the keystone for the extra regions cannot be
integrated via juju (for example, completely separate deployment).

Closes-Bug: #1714926

Change-Id: I52cecec88437fd2bc5a012653f24471039e6b819
This commit is contained in:
Samuel Allan 2024-03-21 10:49:41 +10:30
parent 63d0ffc157
commit 422611f034
No known key found for this signature in database
GPG Key ID: 622F8E99C893BD61
5 changed files with 197 additions and 0 deletions

View File

@ -512,3 +512,19 @@ options:
this option sets True as the default value, which is consistent with the
default value 'WSGISocketRotation On' in Apache. This option should be
used with caution. Please read the Apache doc page for more information.
extra-regions:
type: string
default: "{}"
description: |
Define extra regions to register in the region selector.
Only use this if it's not possible to integrate the keystone unit with juju.
It must be a json dictionary where the keys are region names,
and the values are keystone endpoint urls.
Example:
{
"cluster2": "https://cluster2.example.com/identity/v3",
"another cluster": "https://another.example.com/identity/v3"
}

View File

@ -16,6 +16,7 @@
import functools
import json
from typing import Set, Tuple
from charmhelpers.core.hookenv import (
config,
@ -55,6 +56,25 @@ SSL_CERT_FILE = '/etc/apache2/ssl/horizon/cert_dashboard'
SSL_KEY_FILE = '/etc/apache2/ssl/horizon/key_dashboard'
def get_extra_regions() -> Set[Tuple[str, str]]:
"""
Get extra regions from config, as a set of (name, url) tuples
Raises ValueError if parsing fails.
"""
extra_regions = set()
try:
extra_regions_config = json.loads(config("extra-regions"))
for title, endpoint in extra_regions_config.items():
if isinstance(title, str) and isinstance(endpoint, str):
extra_regions.add((endpoint, title))
else:
raise ValueError("keys and values must be strings")
except Exception as e:
raise ValueError(f"failed parsing extra-regions config: {e!r}")
return extra_regions
class HorizonHAProxyContext(OSContextGenerator):
def __call__(self):
'''
@ -203,6 +223,14 @@ class IdentityServiceContext(OSContextGenerator):
if len(ctxt) == 0:
ctxt = local_ctxt
try:
regions.update(get_extra_regions())
except ValueError as e:
log(
f"Ignoring 'extra-regions' config, it is invalid. Err: {e}",
WARNING
)
if len(regions) > 1:
avail_regions = map(lambda r: {'endpoint': r[0], 'title': r[1]},
regions)

View File

@ -114,6 +114,8 @@ from hooks.horizon_utils import (
update_plugin_packages_in_kv,
)
from hooks.horizon_contexts import get_extra_regions
hooks = Hooks()
# Note that CONFIGS is now set up via resolve_CONFIGS so that it is not a
@ -227,6 +229,12 @@ def config_changed():
application_dashboard_relation_changed()
dashboard_relation_changed()
# Provide a message to the user if extra regions config is invalid
try:
get_extra_regions()
except ValueError:
status_set("blocked", "Invalid 'extra-regions' config value")
@hooks.hook('identity-service-relation-joined')
def keystone_joined(rel_id=None):

View File

@ -1071,6 +1071,68 @@ class TestHorizonContexts(CharmTestCase):
{'endpoint': 'http://foo:5000/v2.0',
'title': 'regionTwo'}]})
@patch("hooks.horizon_contexts.format_ipv6_addr")
def test_IdentityServiceContext_multi_region_with_extra(
self, mock_format_ipv6_addr
):
mock_format_ipv6_addr.side_effect = lambda x: x
self.relation_ids.return_value = ['foo']
self.related_units.return_value = ['bar', 'baz']
self.relation_get.side_effect = self.test_relation.get
self.test_relation.set({'service_host': 'foo', 'service_port': 5000,
'internal_host': 'bar', 'internal_port': 5001,
'region': 'regionOne regionTwo'})
self.test_config.set(
'extra-regions', '{"regionThree": "http://example.com/v3"}'
)
self.maxDiff = None
self.context_complete.return_value = True
self.assertEqual(
with_regions_sorted(horizon_contexts.IdentityServiceContext()),
{'service_host': 'foo', 'service_port': 5000,
'service_protocol': 'http', 'api_version': '2',
'internal_host': 'bar', 'internal_port': 5001,
'internal_protocol': 'http',
'ks_host': 'foo', 'ks_port': 5000,
'ks_protocol': 'http',
'ks_endpoint_path': 'v2.0',
'default_role': 'member',
'regions': [{'endpoint': 'http://foo:5000/v2.0',
'title': 'regionOne'},
{'endpoint': 'http://example.com/v3',
'title': 'regionThree'},
{'endpoint': 'http://foo:5000/v2.0',
'title': 'regionTwo'}]})
@patch("hooks.horizon_contexts.format_ipv6_addr")
def test_IdentityServiceContext_multi_region_with_invalid_extra_regions(
self, mock_format_ipv6_addr
):
mock_format_ipv6_addr.side_effect = lambda x: x
self.relation_ids.return_value = ['foo']
self.related_units.return_value = ['bar', 'baz']
self.relation_get.side_effect = self.test_relation.get
self.test_relation.set({'service_host': 'foo', 'service_port': 5000,
'internal_host': 'bar', 'internal_port': 5001,
'region': 'regionOne regionTwo'})
self.test_config.set('extra-regions', '{{{{')
self.maxDiff = None
self.context_complete.return_value = True
self.assertEqual(
with_regions_sorted(horizon_contexts.IdentityServiceContext()),
{'service_host': 'foo', 'service_port': 5000,
'service_protocol': 'http', 'api_version': '2',
'internal_host': 'bar', 'internal_port': 5001,
'internal_protocol': 'http',
'ks_host': 'foo', 'ks_port': 5000,
'ks_protocol': 'http',
'ks_endpoint_path': 'v2.0',
'default_role': 'member',
'regions': [{'endpoint': 'http://foo:5000/v2.0',
'title': 'regionOne'},
{'endpoint': 'http://foo:5000/v2.0',
'title': 'regionTwo'}]})
@patch("hooks.horizon_contexts.format_ipv6_addr")
def test_IdentityServiceContext_multi_region_v3(self,
mock_format_ipv6_addr):

View File

@ -266,6 +266,7 @@ class TestHorizonHooks(CharmTestCase):
'action-managed-upgrade': False,
'webroot': '/horizon',
'site-name': 'local',
'extra-regions': '{}',
}[key]
self.config.side_effect = config_side_effect
_is_leader.return_value = True
@ -283,6 +284,88 @@ class TestHorizonHooks(CharmTestCase):
self.open_port.assert_has_calls([call(80), call(443)])
self.assertTrue(_custom_theme.called)
@patch('hooks.horizon_contexts.config')
@patch('hooks.horizon_hooks.check_custom_theme')
@patch('hooks.horizon_hooks.keystone_joined')
@patch('hooks.horizon_hooks.is_leader')
@patch('os.environ.get')
def test_config_changed_invalid_extra_regions(
self, _environ_get, _is_leader, _joined, _custom_theme, _config
):
self.relation_ids.side_effect = lambda _: []
self.config.side_effect = _config.side_effect = lambda key: {
'ssl_key': 'somekey',
'enforce-ssl': True,
'dns-ha': True,
'os-public-hostname': 'dashboard.intranet.test',
'prefer-ipv6': False,
'action-managed-upgrade': False,
'webroot': '/horizon',
'site-name': 'local',
'extra-regions': '{',
}[key]
_is_leader.return_value = True
_environ_get.return_value = ''
self.openstack_upgrade_available.return_value = False
self._call_hook('config-changed')
self.status_set.assert_has_calls([
call("blocked", "Invalid 'extra-regions' config value")
])
@patch('hooks.horizon_contexts.config')
@patch('hooks.horizon_hooks.check_custom_theme')
@patch('hooks.horizon_hooks.keystone_joined')
@patch('hooks.horizon_hooks.is_leader')
@patch('os.environ.get')
def test_config_changed_invalid_extra_regions2(
self, _environ_get, _is_leader, _joined, _custom_theme, _config
):
self.relation_ids.side_effect = lambda _: []
self.config.side_effect = _config.side_effect = lambda key: {
'ssl_key': 'somekey',
'enforce-ssl': True,
'dns-ha': True,
'os-public-hostname': 'dashboard.intranet.test',
'prefer-ipv6': False,
'action-managed-upgrade': False,
'webroot': '/horizon',
'site-name': 'local',
'extra-regions': '{"test": 2}',
}[key]
_is_leader.return_value = True
_environ_get.return_value = ''
self.openstack_upgrade_available.return_value = False
self._call_hook('config-changed')
self.status_set.assert_has_calls([
call("blocked", "Invalid 'extra-regions' config value")
])
@patch('hooks.horizon_contexts.config')
@patch('hooks.horizon_hooks.check_custom_theme')
@patch('hooks.horizon_hooks.keystone_joined')
@patch('hooks.horizon_hooks.is_leader')
@patch('os.environ.get')
def test_config_changed_valid_extra_regions(
self, _environ_get, _is_leader, _joined, _custom_theme, _config
):
self.relation_ids.side_effect = lambda _: []
self.config.side_effect = _config.side_effect = lambda key: {
'ssl_key': 'somekey',
'enforce-ssl': True,
'dns-ha': True,
'os-public-hostname': 'dashboard.intranet.test',
'prefer-ipv6': False,
'action-managed-upgrade': False,
'webroot': '/horizon',
'site-name': 'local',
'extra-regions': '{"test": "http://example.com/v3"}',
}[key]
_is_leader.return_value = True
_environ_get.return_value = ''
self.openstack_upgrade_available.return_value = False
self._call_hook('config-changed')
self.status_set.assert_not_called()
@patch('hooks.horizon_hooks.check_custom_theme')
@patch('hooks.horizon_hooks.is_leader')
def test_config_changed_do_upgrade(self, _is_leader, _custom_theme):