From 6b8bf0578d4e47511fbbcfe2c192d6d9fecacabd Mon Sep 17 00:00:00 2001 From: Guillaume Boutry Date: Thu, 26 Oct 2023 08:44:45 +0200 Subject: [PATCH] Use UserIRR class to manage user Use ops_sunbeam common class to manage user. Change-Id: I27121d07d317132c1655ede6a81e7d4e25546dd8 --- charms/magnum-k8s/src/charm.py | 207 ++++-------------- .../tests/unit/test_magnum_charm.py | 21 +- 2 files changed, 44 insertions(+), 184 deletions(-) diff --git a/charms/magnum-k8s/src/charm.py b/charms/magnum-k8s/src/charm.py index 4e47392f..ba32d9bd 100755 --- a/charms/magnum-k8s/src/charm.py +++ b/charms/magnum-k8s/src/charm.py @@ -17,36 +17,24 @@ This charm provide Magnum services as part of an OpenStack deployment """ -import hashlib -import json import logging from typing import ( TYPE_CHECKING, List, ) -import charms.keystone_k8s.v0.identity_resource as identity_resource +import ops import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.config_contexts as sunbeam_config_contexts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core import ops_sunbeam.relation_handlers as sunbeam_rhandlers -import pwgen -from ops.charm import ( - RelationEvent, -) from ops.framework import ( StoredState, ) from ops.main import ( main, ) -from ops.model import ( - ModelError, - Relation, - SecretNotFoundError, - SecretRotate, -) logger = logging.getLogger(__name__) @@ -61,9 +49,17 @@ class MagnumConfigurationContext(sunbeam_config_contexts.ConfigContext): if TYPE_CHECKING: charm: "MagnumOperatorCharm" + @property + def ready(self) -> bool: + """Whether the context has all the data is needs.""" + return self.charm.user_id_ops.ready + def context(self) -> dict: """Magnum configuration context.""" - username, password = self.charm.domain_admin_credentials + credentials = self.charm.user_id_ops.get_config_credentials() + if credentials is None: + return {} + username, password = credentials return { "domain_name": self.charm.domain_name, "domain_admin_user": username, @@ -195,20 +191,24 @@ class MagnumOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): _cadapters.extend([MagnumConfigurationContext(self, "magnum")]) return _cadapters - def hash_ops(self, ops: list) -> str: - """Return the sha1 of the requested ops.""" - return hashlib.sha1(json.dumps(ops).encode()).hexdigest() - def get_relation_handlers(self) -> List[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = super().get_relation_handlers() - self.id_ops = sunbeam_rhandlers.IdentityResourceRequiresHandler( - self, - "identity-ops", - self.handle_keystone_ops, - mandatory="identity-ops" in self.mandatory_relations, + self.user_id_ops = ( + sunbeam_rhandlers.UserIdentityResourceRequiresHandler( + self, + "identity-ops", + self.configure_charm, + mandatory="identity-ops" in self.mandatory_relations, + name=self.domain_admin_user, + domain=self.domain_name, + role="admin", + add_suffix=True, + extra_ops=self._get_create_role_ops(), + extra_ops_process=self._handle_create_role_response, + ) ) - handlers.append(self.id_ops) + handlers.append(self.user_id_ops) return handlers @property @@ -265,159 +265,28 @@ class MagnumOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """User to manage users and projects in domain_name.""" return "magnum_domain_admin" - @property - def domain_admin_credentials(self) -> tuple: - """Credentials for domain admin user.""" - credentials_id = self._get_domain_admin_credentials_secret() - credentials = self.model.get_secret(id=credentials_id) - username = credentials.get_content().get("username") - user_password = credentials.get_content().get("password") - return (username, user_password) - - def _get_domain_admin_credentials_secret(self) -> str: - """Get domain admin secret.""" - label = f"{CREDENTIALS_SECRET_PREFIX}{self.domain_admin_user}" - credentials_id = self.peers.get_app_data(label) - - if not credentials_id: - credentials_id = self._retrieve_or_set_secret( - self.domain_admin_user, - ) - - return credentials_id - - def _grant_domain_admin_credentials_secret( - self, - relation: Relation, - ) -> None: - """Grant secret access to the related units.""" - credentials_id = None - try: - credentials_id = self._get_domain_admin_credentials_secret() - secret = self.model.get_secret(id=credentials_id) - logger.debug( - f"Granting access to secret {credentials_id} for relation " - f"{relation.app.name} {relation.name}/{relation.id}" - ) - secret.grant(relation) - except (ModelError, SecretNotFoundError) as e: - logger.debug( - f"Error during granting access to secret {credentials_id} for " - f"relation {relation.app.name} {relation.name}/{relation.id}: " - f"{str(e)}" - ) - - def _retrieve_or_set_secret( - self, - username: str, - rotate: SecretRotate = SecretRotate.NEVER, - add_suffix_to_username: bool = False, - ) -> str: - """Retrieve or create a secret.""" - label = f"{CREDENTIALS_SECRET_PREFIX}{username}" - credentials_id = self.peers.get_app_data(label) - if credentials_id: - return credentials_id - - password = pwgen.pwgen(12) - if add_suffix_to_username: - suffix = pwgen.pwgen(6) - username = f"{username}-{suffix}" - credentials_secret = self.model.app.add_secret( - {"username": username, "password": password}, - label=label, - rotate=rotate, - ) - self.peers.set_app_data( + def _get_create_role_ops(self) -> list: + """Generate ops request for create role.""" + return [ { - label: credentials_secret.id, + "name": "create_role", + "params": {"name": "magnum_domain_admin"}, } - ) - return credentials_secret.id - - def _get_magnum_domain_ops(self) -> list: - """Generate ops request for domain setup.""" - credentials_id = self._get_domain_admin_credentials_secret() - ops = [ - # Create domain magnum - { - "name": "create_domain", - "params": {"name": "magnum", "enable": True}, - }, - # Create role magnum_domain_admin - {"name": "create_role", "params": {"name": "magnum_domain_admin"}}, - # Create user magnum - { - "name": "create_user", - "params": { - "name": self.domain_admin_user, - "password": credentials_id, - "domain": "magnum", - }, - }, - # Grant role admin to magnum_domain_admin user - { - "name": "grant_role", - "params": { - "role": "admin", - "domain": "magnum", - "user": self.domain_admin_user, - "user_domain": "magnum", - }, - }, ] - return ops - def _handle_initial_magnum_domain_setup_response( - self, - event: RelationEvent, + def _handle_create_role_response( + self, event: ops.EventBase, response: dict ) -> None: - """Handle domain setup response from identity-ops.""" + """Handle response from identity-ops.""" + logger.info("%r", response) if { op.get("return-code") - for op in self.id_ops.interface.response.get( - "ops", - [], - ) + for op in response.get("ops", []) + if op.get("name") == "create_role" } == {0}: - logger.debug( - "Initial magnum domain setup commands completed," - " running configure charm" - ) - self.configure_charm(event) - - def handle_keystone_ops(self, event: RelationEvent) -> None: - """Event handler for identity ops.""" - if isinstance(event, identity_resource.IdentityOpsProviderReadyEvent): - self._state.identity_ops_ready = True - - if not self.unit.is_leader(): - return - - # Send op request only by leader unit - ops = self._get_magnum_domain_ops() - id_ = self.hash_ops(ops) - self._grant_domain_admin_credentials_secret(event.relation) - request = { - "id": id_, - "tag": "initial_magnum_domain_setup", - "ops": ops, - } - logger.debug(f"Sending ops request: {request}") - self.id_ops.interface.request_ops(request) - elif isinstance( - event, - identity_resource.IdentityOpsProviderGoneAwayEvent, - ): - self._state.identity_ops_ready = False - elif isinstance(event, identity_resource.IdentityOpsResponseEvent): - if not self.unit.is_leader(): - return - response = self.id_ops.interface.response - logger.debug(f"Got response from keystone: {response}") - request_tag = response.get("tag") - if request_tag == "initial_magnum_domain_setup": - self._handle_initial_magnum_domain_setup_response(event) + logger.debug("Magnum domain admin role has been created.") + else: + logger.warning("Magnum domain admin role creation failed.") if __name__ == "__main__": diff --git a/charms/magnum-k8s/tests/unit/test_magnum_charm.py b/charms/magnum-k8s/tests/unit/test_magnum_charm.py index 4d85d18e..6466f9a4 100644 --- a/charms/magnum-k8s/tests/unit/test_magnum_charm.py +++ b/charms/magnum-k8s/tests/unit/test_magnum_charm.py @@ -19,6 +19,9 @@ import json import ops_sunbeam.test_utils as test_utils +from mock import ( + Mock, +) from ops.testing import ( Harness, ) @@ -81,20 +84,8 @@ class TestMagnumOperatorCharm(test_utils.CharmTestCase): """Add complete Identity resource relation.""" rel_id = harness.add_relation("identity-ops", "keystone") harness.add_relation_unit(rel_id, "keystone/0") - ops = harness.charm._get_magnum_domain_ops() - id_ = harness.charm.hash_ops(ops) - harness.update_relation_data( - rel_id, - "keystone/0", - { - "request": json.dumps( - { - "id": id_, - "tag": "initial_magnum_domain_setup", - "ops": ops, - } - ) - }, + harness.charm.user_id_ops.get_config_credentials = Mock( + return_value=("test", "test") ) harness.update_relation_data( rel_id, @@ -102,7 +93,7 @@ class TestMagnumOperatorCharm(test_utils.CharmTestCase): { "response": json.dumps( { - "id": id_, + "id": 1, "tag": "initial_magnum_domain_setup", "ops": [{"name": "create_domain", "return-code": 0}], }