diff --git a/requires.py b/requires.py index bfec631..b5d8861 100644 --- a/requires.py +++ b/requires.py @@ -23,6 +23,7 @@ class KeystoneAutoAccessors(type): Metaclass that converts fields referenced by ``auto_accessors`` into accessor methods with very basic doc strings. """ + def __new__(cls, name, parents, dct): for field in dct.get('auto_accessors', []): meth_name = field.replace('-', '_') @@ -37,24 +38,64 @@ class KeystoneAutoAccessors(type): @staticmethod def _accessor(field): - def __accessor(self): - return self.all_joined_units.received.get(field) - return __accessor + def _accessor_internal(self): + # Use remapped or transposed key for application + # data bag lookup for forwards compat + app_field = self._forward_compat_remaps.get( + field, + field.replace('_', '-') + ) + return self.relations[0].received_app_raw.get( + app_field, + self.all_joined_units.received.get(field) + ) + return _accessor_internal class KeystoneRequires(reactive.Endpoint, metaclass=KeystoneAutoAccessors): - auto_accessors = ['service_host', 'service_protocol', - 'service_port', 'service_tenant', 'service_username', - 'service_password', 'service_tenant_id', 'auth_host', - 'auth_protocol', 'auth_port', 'admin_token', 'ssl_key', - 'ca_cert', 'ssl_cert', 'https_keystone', - 'ssl_cert_admin', 'ssl_cert_internal', - 'ssl_cert_public', 'ssl_key_admin', 'ssl_key_internal', - 'ssl_key_public', 'api_version', 'service_domain', - 'service_domain_id', 'ep_changed', - 'admin_domain_id', 'admin_user_id', 'admin_project_id', - 'service_type'] + auto_accessors = [ + 'service_host', + 'service_protocol', + 'service_port', + 'service_tenant', + 'service_username', + 'service_password', + 'service_tenant_id', + 'auth_host', + 'auth_protocol', + 'auth_port', + 'admin_token', + 'ssl_key', + 'ca_cert', + 'ssl_cert', + 'https_keystone', + 'ssl_cert_admin', + 'ssl_cert_internal', + 'ssl_cert_public', + 'ssl_key_admin', + 'ssl_key_internal', + 'ssl_key_public', + 'api_version', + 'service_domain', + 'service_domain_id', + 'ep_changed', + 'admin_domain_id', + 'admin_user_id', + 'admin_project_id', + 'service_type', + 'public-auth-url', + 'internal-auth-url', + 'admin-auth-url', + ] + + _forward_compat_remaps = { + 'admin_user': 'admin-user-name', + 'service_username': 'service-user-name', + 'service_tenant': 'service-project-name', + 'service_tenant_id': 'service-project-id', + 'service_domain': 'service-domain-name', + } @reactive.when('endpoint.{endpoint_name}.joined') def joined(self): @@ -146,7 +187,9 @@ class KeystoneRequires(reactive.Endpoint, metaclass=KeystoneAutoAccessors): def register_endpoints(self, service, region, public_url, internal_url, admin_url, requested_roles=None, - add_role_to_admin=None): + add_role_to_admin=None, + service_type=None, + service_description=None): """ Register this service with keystone """ @@ -166,6 +209,26 @@ class KeystoneRequires(reactive.Endpoint, metaclass=KeystoneAutoAccessors): for relation in self.relations: relation.to_publish_raw.update(relation_info) + # NOTE: forwards compatible data presentation for keystone-k8s + if all((service_type, + service_description, + reactive.is_flag_set('leadership.is_leader'),)): + application_info = { + 'region': region, + 'service-endpoints': json.dumps([ + { + 'service_name': service, + 'type': service_type, + 'description': service_description, + 'internal_url': internal_url, + 'admin_url': admin_url, + 'public_url': public_url, + } + ], sort_keys=True) + } + for relation in self.relations: + relation.to_publish_app_raw.update(application_info) + def request_keystone_endpoint_information(self): self.register_endpoints('None', 'None', 'None', 'None', 'None') diff --git a/tox.ini b/tox.ini index 4d2b254..13219c0 100644 --- a/tox.ini +++ b/tox.ini @@ -81,4 +81,4 @@ commands = {posargs} [flake8] # E402 ignore necessary for path append before sys module import in actions -ignore = E402 +ignore = E402 W503 diff --git a/unit_tests/test_requires.py b/unit_tests/test_requires.py index 8ce85a3..a5ff6eb 100644 --- a/unit_tests/test_requires.py +++ b/unit_tests/test_requires.py @@ -10,6 +10,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json from unittest import mock @@ -19,6 +20,35 @@ import charms_openstack.test_utils as test_utils _hook_args = {} +IDENTITY_APP_DATA = { + 'api-version': '3', + 'auth-host': 'authhost', + 'auth-port': '5000', + 'auth-protocol': 'http', + 'internal-host': 'internalhost', + 'internal-port': '5000', + 'internal-protocol': 'http', + 'service-host': 'servicehost', + 'service-port': '5000', + 'service-protocol': 'http', + 'admin-domain-name': 'admin-domain', + 'admin-domain-id': 'ca9e66dd-920c-493c-8ebd-dcc893afcc3b', + 'admin-project-name': 'admin', + 'admin-project-id': '5c9fd12c-87eb-4688-931a-05da83db14ad', + 'admin-user-name': 'admin', + 'admin-user-id': 'cc28fa26-70bc-4acb-97a4-5614799257bb', + 'service-domain-name': 'service-domain', + 'service-domain-id': '8fa8e4c1-b9f6-44ae-b646-0626d44786c2', + 'service-project-name': 'services', + 'service-project-id': '0626e4d8-0846-4fd5-98c9-324fbbe24301', + 'service-user-name': 'gnocchi', + 'service-user-id': 'fa8c4a9a-f97c-41e7-a204-73571c5a7b51', + 'service-password': 'foobar', + 'internal-auth-url': 'http://internalhost:80/keystone', + 'admin-auth-url': 'http://adminhost:80/keystone', + 'public-auth-url': 'http://publichost:80/keystone', +} + class TestKeystoneRequires(test_utils.PatchHelper): @@ -73,6 +103,25 @@ class TestKeystoneRequires(test_utils.PatchHelper): self.service_tenant.return_value = None assert self.target.base_data_complete() is False + def test_app_data_complete(self): + relation = mock.MagicMock() + relation.received_app_raw.get.side_effect = ( + lambda k, d: IDENTITY_APP_DATA.get(k, d) + ) + self.target._relations = [relation] + self.assertEqual(self.target.service_host(), 'servicehost') + self.assertEqual(self.target.auth_host(), 'authhost') + self.assertEqual( + self.target.public_auth_url(), 'http://publichost:80/keystone') + self.assertEqual(self.target.service_tenant(), 'services') + self.assertEqual(self.target.service_password(), 'foobar') + self.assertEqual( + self.target.service_tenant_id(), + '0626e4d8-0846-4fd5-98c9-324fbbe24301') + self.assertTrue(self.target.base_data_complete()) + self.assertFalse(self.target.ssl_data_complete()) + self.assertFalse(self.target.ssl_data_complete_legacy()) + def test_ssl_data_complete(self): self.patch_target('ssl_cert_admin', '1') self.patch_target('ssl_cert_internal', '2') @@ -152,10 +201,14 @@ class TestKeystoneRequires(test_utils.PatchHelper): 'endpoint.some-relation.changed') def test_register_endpoints(self): + self.patch_object(requires.reactive, 'is_flag_set') + self.is_flag_set.return_value = True relation = mock.MagicMock() self.patch_target('_relations') self._relations.__iter__.return_value = [relation] - self.target.register_endpoints('s', 'r', 'p_url', 'i_url', 'a_url') + self.target.register_endpoints( + 's', 'r', 'p_url', 'i_url', 'a_url', + service_type='stype', service_description='sdesc') result = { 'service': 's', 'public_url': 'p_url', @@ -164,6 +217,21 @@ class TestKeystoneRequires(test_utils.PatchHelper): 'region': 'r', } relation.to_publish_raw.update.assert_called_once_with(result) + # This should only happen when the charm is the leader and + # register_endpoints is called with type and description + # information. + relation.to_publish_app_raw.update.assert_called_once_with({ + 'region': 'r', + 'service-endpoints': json.dumps([{ + "admin_url": "a_url", + "description": "sdesc", + "internal_url": "i_url", + "public_url": "p_url", + "service_name": "s", + "type": "stype"}], + sort_keys=True + ) + }) def test_register_endpoints_requested_roles(self): relation = mock.MagicMock()