diff --git a/charms/horizon-k8s/README.md b/charms/horizon-k8s/README.md index 2385b16c..a1e5ec2b 100644 --- a/charms/horizon-k8s/README.md +++ b/charms/horizon-k8s/README.md @@ -17,7 +17,7 @@ Now connect the horizon operator to existing database and Keystone operators: juju relate mysql:database horizon:database - juju relate keystone:cloud-credentials horizon:cloud-credentials + juju relate keystone:identity-credentials horizon:identity-credentials ### Configuration @@ -38,7 +38,7 @@ deployed then see file `actions.yaml`. horizon-k8s requires the following relations: `database`: To connect to MySQL -`cloud-credentials`: To register cloud users in Keystone +`identity-credentials`: To register cloud users in Keystone `ingress-internal`: To expose service on underlying internal network `ingress-public`: To expose service on public network diff --git a/charms/horizon-k8s/fetch-libs.sh b/charms/horizon-k8s/fetch-libs.sh index f26499f5..21c004aa 100755 --- a/charms/horizon-k8s/fetch-libs.sh +++ b/charms/horizon-k8s/fetch-libs.sh @@ -3,7 +3,7 @@ echo "INFO: Fetching libs from charmhub." charmcraft fetch-lib charms.data_platform_libs.v0.database_requires charmcraft fetch-lib charms.keystone_k8s.v1.identity_service -charmcraft fetch-lib charms.keystone_k8s.v1.cloud_credentials +charmcraft fetch-lib charms.keystone_k8s.v0.identity_credentials charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch charmcraft fetch-lib charms.traefik_k8s.v1.ingress diff --git a/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py new file mode 100644 index 00000000..162a46a8 --- /dev/null +++ b/charms/horizon-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py @@ -0,0 +1,439 @@ +"""IdentityCredentialsProvides and Requires module. + + +This library contains the Requires and Provides classes for handling +the identity_credentials interface. + +Import `IdentityCredentialsRequires` in your charm, with the charm object and the +relation name: + - self + - "identity_credentials" + +Also provide additional parameters to the charm object: + - service + - internal_url + - public_url + - admin_url + - region + - username + - vhost + +Two events are also available to respond to: + - connected + - ready + - goneaway + +A basic example showing the usage of this relation follows: + +``` +from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires + +class IdentityCredentialsClientCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + # IdentityCredentials Requires + self.identity_credentials = IdentityCredentialsRequires( + self, "identity_credentials", + service = "my-service" + internal_url = "http://internal-url" + public_url = "http://public-url" + admin_url = "http://admin-url" + region = "region" + ) + self.framework.observe( + self.identity_credentials.on.connected, self._on_identity_credentials_connected) + self.framework.observe( + self.identity_credentials.on.ready, self._on_identity_credentials_ready) + self.framework.observe( + self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway) + + def _on_identity_credentials_connected(self, event): + '''React to the IdentityCredentials connected event. + + This event happens when IdentityCredentials relation is added to the + model before credentials etc have been provided. + ''' + # Do something before the relation is complete + pass + + def _on_identity_credentials_ready(self, event): + '''React to the IdentityCredentials ready event. + + The IdentityCredentials interface will use the provided config for the + request to the identity server. + ''' + # IdentityCredentials Relation is ready. Do something with the completed relation. + pass + + def _on_identity_credentials_goneaway(self, event): + '''React to the IdentityCredentials goneaway event. + + This event happens when an IdentityCredentials relation is removed. + ''' + # IdentityCredentials Relation has goneaway. shutdown services or suchlike + pass +``` +""" + +import logging + +from ops.framework import ( + StoredState, + EventBase, + ObjectEvents, + EventSource, + Object, +) +from ops.model import ( + Relation, + SecretNotFoundError, +) + +# The unique Charmhub library identifier, never change it +LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 1 + +logger = logging.getLogger(__name__) + + +class IdentityCredentialsConnectedEvent(EventBase): + """IdentityCredentials connected Event.""" + + pass + + +class IdentityCredentialsReadyEvent(EventBase): + """IdentityCredentials ready for use Event.""" + + pass + + +class IdentityCredentialsGoneAwayEvent(EventBase): + """IdentityCredentials relation has gone-away Event""" + + pass + + +class IdentityCredentialsServerEvents(ObjectEvents): + """Events class for `on`""" + + connected = EventSource(IdentityCredentialsConnectedEvent) + ready = EventSource(IdentityCredentialsReadyEvent) + goneaway = EventSource(IdentityCredentialsGoneAwayEvent) + + +class IdentityCredentialsRequires(Object): + """ + IdentityCredentialsRequires class + """ + + on = IdentityCredentialsServerEvents() + _stored = StoredState() + + def __init__(self, charm, relation_name: str): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.framework.observe( + self.charm.on[relation_name].relation_joined, + self._on_identity_credentials_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_identity_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_departed, + self._on_identity_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_identity_credentials_relation_broken, + ) + + def _on_identity_credentials_relation_joined(self, event): + """IdentityCredentials relation joined.""" + logging.debug("IdentityCredentials on_joined") + self.on.connected.emit() + self.request_credentials() + + def _on_identity_credentials_relation_changed(self, event): + """IdentityCredentials relation changed.""" + logging.debug("IdentityCredentials on_changed") + try: + self.on.ready.emit() + except (AttributeError, KeyError): + logger.exception('Error when emitting event') + + def _on_identity_credentials_relation_broken(self, event): + """IdentityCredentials relation broken.""" + logging.debug("IdentityCredentials on_broken") + self.on.goneaway.emit() + + @property + def _identity_credentials_rel(self) -> Relation: + """The IdentityCredentials relation.""" + return self.framework.model.get_relation(self.relation_name) + + def get_remote_app_data(self, key: str) -> str: + """Return the value for the given key from remote app data.""" + data = self._identity_credentials_rel.data[self._identity_credentials_rel.app] + return data.get(key) + + @property + def api_version(self) -> str: + """Return the api_version.""" + return self.get_remote_app_data('api-version') + + @property + def auth_host(self) -> str: + """Return the auth_host.""" + return self.get_remote_app_data('auth-host') + + @property + def auth_port(self) -> str: + """Return the auth_port.""" + return self.get_remote_app_data('auth-port') + + @property + def auth_protocol(self) -> str: + """Return the auth_protocol.""" + return self.get_remote_app_data('auth-protocol') + + @property + def internal_host(self) -> str: + """Return the internal_host.""" + return self.get_remote_app_data('internal-host') + + @property + def internal_port(self) -> str: + """Return the internal_port.""" + return self.get_remote_app_data('internal-port') + + @property + def internal_protocol(self) -> str: + """Return the internal_protocol.""" + return self.get_remote_app_data('internal-protocol') + + @property + def credentials(self) -> str: + return self.get_remote_app_data('credentials') + + @property + def username(self) -> str: + credentials_id = self.get_remote_app_data('credentials') + if not credentials_id: + return None + + try: + credentials = self.charm.model.get_secret(id=credentials_id) + return credentials.get_content().get("username") + except SecretNotFoundError: + logger.warning(f"Secret {credentials_id} not found") + return None + + @property + def password(self) -> str: + credentials_id = self.get_remote_app_data('credentials') + if not credentials_id: + return None + + try: + credentials = self.charm.model.get_secret(id=credentials_id) + return credentials.get_content().get("password") + except SecretNotFoundError: + logger.warning(f"Secret {credentials_id} not found") + return None + + @property + def project_name(self) -> str: + """Return the project name.""" + return self.get_remote_app_data('project-name') + + @property + def project_id(self) -> str: + """Return the project id.""" + return self.get_remote_app_data('project-id') + + @property + def user_domain_name(self) -> str: + """Return the name of the user domain.""" + return self.get_remote_app_data('user-domain-name') + + @property + def user_domain_id(self) -> str: + """Return the id of the user domain.""" + return self.get_remote_app_data('user-domain-id') + + @property + def project_domain_name(self) -> str: + """Return the name of the project domain.""" + return self.get_remote_app_data('project-domain-name') + + @property + def project_domain_id(self) -> str: + """Return the id of the project domain.""" + return self.get_remote_app_data('project-domain-id') + + @property + def region(self) -> str: + """Return the region for the auth urls.""" + return self.get_remote_app_data('region') + + def request_credentials(self) -> None: + """Request credentials from the IdentityCredentials server.""" + if self.model.unit.is_leader(): + logging.debug(f'Requesting credentials for {self.charm.app.name}') + app_data = self._identity_credentials_rel.data[self.charm.app] + app_data['username'] = self.charm.app.name + + +class HasIdentityCredentialsClientsEvent(EventBase): + """Has IdentityCredentialsClients Event.""" + + pass + + +class ReadyIdentityCredentialsClientsEvent(EventBase): + """IdentityCredentialsClients Ready Event.""" + + def __init__(self, handle, relation_id, relation_name, username): + super().__init__(handle) + self.relation_id = relation_id + self.relation_name = relation_name + self.username = username + + def snapshot(self): + return { + "relation_id": self.relation_id, + "relation_name": self.relation_name, + "username": self.username, + } + + def restore(self, snapshot): + super().restore(snapshot) + self.relation_id = snapshot["relation_id"] + self.relation_name = snapshot["relation_name"] + self.username = snapshot["username"] + + +class IdentityCredentialsClientsGoneAwayEvent(EventBase): + """Has IdentityCredentialsClientsGoneAwayEvent Event.""" + + pass + + +class IdentityCredentialsClientEvents(ObjectEvents): + """Events class for `on`""" + + has_identity_credentials_clients = EventSource( + HasIdentityCredentialsClientsEvent + ) + ready_identity_credentials_clients = EventSource( + ReadyIdentityCredentialsClientsEvent + ) + identity_credentials_clients_gone = EventSource( + IdentityCredentialsClientsGoneAwayEvent + ) + + +class IdentityCredentialsProvides(Object): + """ + IdentityCredentialsProvides class + """ + + on = IdentityCredentialsClientEvents() + _stored = StoredState() + + def __init__(self, charm, relation_name): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.framework.observe( + self.charm.on[relation_name].relation_joined, + self._on_identity_credentials_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_identity_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_identity_credentials_relation_broken, + ) + + def _on_identity_credentials_relation_joined(self, event): + """Handle IdentityCredentials joined.""" + logging.debug("IdentityCredentialsProvides on_joined") + self.on.has_identity_credentials_clients.emit() + + def _on_identity_credentials_relation_changed(self, event): + """Handle IdentityCredentials changed.""" + logging.debug("IdentityCredentials on_changed") + REQUIRED_KEYS = ['username'] + + values = [ + event.relation.data[event.relation.app].get(k) + for k in REQUIRED_KEYS + ] + # Validate data on the relation + if all(values): + username = event.relation.data[event.relation.app]['username'] + self.on.ready_identity_credentials_clients.emit( + event.relation.id, + event.relation.name, + username, + ) + + def _on_identity_credentials_relation_broken(self, event): + """Handle IdentityCredentials broken.""" + logging.debug("IdentityCredentialsProvides on_departed") + self.on.identity_credentials_clients_gone.emit() + + def set_identity_credentials(self, relation_name: int, + relation_id: str, + api_version: str, + auth_host: str, + auth_port: str, + auth_protocol: str, + internal_host: str, + internal_port: str, + internal_protocol: str, + credentials: str, + project_name: str, + project_id: str, + user_domain_name: str, + user_domain_id: str, + project_domain_name: str, + project_domain_id: str, + region: str): + logging.debug("Setting identity_credentials connection information.") + _identity_credentials_rel = None + for relation in self.framework.model.relations[relation_name]: + if relation.id == relation_id: + _identity_credentials_rel = relation + if not _identity_credentials_rel: + # Relation has disappeared so don't send the data + return + app_data = _identity_credentials_rel.data[self.charm.app] + app_data["api-version"] = api_version + app_data["auth-host"] = auth_host + app_data["auth-port"] = str(auth_port) + app_data["auth-protocol"] = auth_protocol + app_data["internal-host"] = internal_host + app_data["internal-port"] = str(internal_port) + app_data["internal-protocol"] = internal_protocol + app_data["credentials"] = credentials + app_data["project-name"] = project_name + app_data["project-id"] = project_id + app_data["user-domain-name"] = user_domain_name + app_data["user-domain-id"] = user_domain_id + app_data["project-domain-name"] = project_domain_name + app_data["project-domain-id"] = project_domain_id + app_data["region"] = region diff --git a/charms/horizon-k8s/metadata.yaml b/charms/horizon-k8s/metadata.yaml index bebe5b64..8d9a8d60 100644 --- a/charms/horizon-k8s/metadata.yaml +++ b/charms/horizon-k8s/metadata.yaml @@ -37,7 +37,7 @@ requires: interface: ingress optional: true limit: 1 - cloud-credentials: + identity-credentials: interface: keystone-credentials limit: 1 diff --git a/charms/horizon-k8s/src/charm.py b/charms/horizon-k8s/src/charm.py index f4fb7d1d..92fc9215 100755 --- a/charms/horizon-k8s/src/charm.py +++ b/charms/horizon-k8s/src/charm.py @@ -81,7 +81,7 @@ class OpenstackDashboardOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): mandatory_relations = { "database", "ingress-public", - "cloud-credentials", + "identity-credentials", } @property diff --git a/charms/horizon-k8s/src/templates/local_settings.py.j2 b/charms/horizon-k8s/src/templates/local_settings.py.j2 index d65e40d8..0ccb2f76 100644 --- a/charms/horizon-k8s/src/templates/local_settings.py.j2 +++ b/charms/horizon-k8s/src/templates/local_settings.py.j2 @@ -191,12 +191,12 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' # ('http://cluster2.example.com:5000/v2.0', 'cluster2'), #] -OPENSTACK_HOST = "{{ cloud_credentials.internal_host }}" +OPENSTACK_HOST = "{{ identity_credentials.internal_host }}" OPENSTACK_KEYSTONE_DEFAULT_ROLE = "{{ options.default_role }}" -OPENSTACK_KEYSTONE_URL = "{{ cloud_credentials.internal_protocol }}://%s:{{ cloud_credentials.internal_port }}/v3" % OPENSTACK_HOST +OPENSTACK_KEYSTONE_URL = "{{ identity_credentials.internal_protocol }}://%s:{{ identity_credentials.internal_port }}/v3" % OPENSTACK_HOST OPENSTACK_API_VERSIONS = { "identity": 3, } OPENSTACK_KEYSTONE_MULTIDOMAIN_SUPPORT = True -OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = "{{ options.default_domain or cloud_credentials.project_domain_id }}" +OPENSTACK_KEYSTONE_DEFAULT_DOMAIN = "{{ options.default_domain or identity_credentials.project_domain_id }}" # Enables keystone web single-sign-on if set to True. #WEBSSO_ENABLED = False